From 87e1d057f76ec5672fd1c0e37b2f54324a9fc529 Mon Sep 17 00:00:00 2001 From: devmobasa <4170275+devmobasa@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:26:12 +0200 Subject: [PATCH 1/2] refactor: standardize Rust style across the workspace --- AGENTS.md | 13 +- Cargo.toml | 18 + build.rs | 3 +- clippy.toml | 13 + configurator/src/app/blocking_jobs.rs | 29 +- configurator/src/app/component.rs | 9 +- configurator/src/app/component/effects.rs | 3 +- configurator/src/app/component/rebuild.rs | 16 +- configurator/src/app/component/shell.rs | 24 +- configurator/src/app/component/tests.rs | 49 +- configurator/src/app/component/view.rs | 3 +- configurator/src/app/daemon_setup/hyprland.rs | 11 +- .../src/app/daemon_setup/hyprland/tests.rs | 13 +- configurator/src/app/daemon_setup/mod.rs | 4 +- configurator/src/app/daemon_setup/service.rs | 6 +- configurator/src/app/daemon_setup/shortcut.rs | 6 +- configurator/src/app/daemon_workflow.rs | 8 +- configurator/src/app/document_workflow.rs | 4 +- configurator/src/app/io.rs | 3 +- configurator/src/app/pages/arrow.rs | 4 +- configurator/src/app/pages/boards.rs | 21 +- configurator/src/app/pages/boards/color.rs | 7 +- configurator/src/app/pages/boards/grid.rs | 6 +- configurator/src/app/pages/capture.rs | 14 +- configurator/src/app/pages/color_rows.rs | 11 +- configurator/src/app/pages/daemon/tests.rs | 9 +- configurator/src/app/pages/drawing.rs | 8 +- .../src/app/pages/drawing/default_color.rs | 4 +- .../src/app/pages/drawing/defaults.rs | 7 +- .../src/app/pages/drawing/drag_mapping.rs | 4 +- configurator/src/app/pages/drawing/font.rs | 32 +- .../src/app/pages/drawing/quick_colors.rs | 13 +- configurator/src/app/pages/keybindings/mod.rs | 10 +- .../src/app/pages/keybindings/recorder.rs | 53 +- configurator/src/app/pages/keybindings/row.rs | 18 +- .../src/app/pages/keybindings/text_editor.rs | 3 +- .../src/app/pages/keybindings/toolbar.rs | 5 +- .../src/app/pages/keybindings/widgets.rs | 7 +- configurator/src/app/pages/mod.rs | 12 +- configurator/src/app/pages/performance.rs | 2 +- configurator/src/app/pages/presets.rs | 8 +- configurator/src/app/pages/presets/color.rs | 4 +- configurator/src/app/pages/presets/rows.rs | 8 +- configurator/src/app/pages/render_profiles.rs | 16 +- .../src/app/pages/render_profiles/mapping.rs | 3 +- .../src/app/pages/session/catalog/card.rs | 61 +- .../src/app/pages/session/catalog/tests.rs | 8 +- .../src/app/pages/session/settings.rs | 6 +- configurator/src/app/pages/tablet.rs | 8 +- configurator/src/app/pages/tests.rs | 7 +- configurator/src/app/pages/ui/general.rs | 4 +- configurator/src/app/pages/ui/input_hud.rs | 4 +- .../src/app/pages/ui/presenter_mode.rs | 4 +- configurator/src/app/pages/ui/status_bar.rs | 2 +- configurator/src/app/pages/ui/toolbar.rs | 12 +- configurator/src/app/search/summary.rs | 26 +- configurator/src/app/search/tests.rs | 17 +- configurator/src/app/session_catalog.rs | 48 +- .../src/app/session_catalog/duplicate.rs | 66 +- .../src/app/session_catalog/move_file.rs | 170 +++-- configurator/src/app/session_catalog/tests.rs | 61 +- configurator/src/app/shortcut_workflow.rs | 6 +- configurator/src/app/startup.rs | 47 +- configurator/src/app/state.rs | 7 +- configurator/src/app/update/boards.rs | 4 +- configurator/src/app/update/color_picker.rs | 27 +- configurator/src/app/update/config/load.rs | 3 +- configurator/src/app/update/config/tests.rs | 38 +- .../src/app/update/config/tests/language.rs | 24 +- configurator/src/app/update/daemon.rs | 2 +- configurator/src/app/update/fields/drawing.rs | 10 +- configurator/src/app/update/fields/tests.rs | 6 +- configurator/src/app/update/mod.rs | 6 +- configurator/src/app/update/presets.rs | 8 +- .../src/app/update/render_profiles.rs | 4 +- .../src/app/update/session_catalog.rs | 4 +- .../src/app/update/session_catalog/tests.rs | 73 ++- configurator/src/app/update/shortcuts.rs | 27 +- .../src/app/update/shortcuts/tests.rs | 33 +- configurator/src/main.rs | 4 +- configurator/src/messages.rs | 3 +- configurator/src/models/color/input.rs | 7 +- configurator/src/models/color/named.rs | 9 +- configurator/src/models/config/boards.rs | 10 +- .../src/models/config/boards/mapping.rs | 16 +- .../src/models/config/draft/from_config.rs | 22 +- configurator/src/models/config/draft/mod.rs | 3 +- configurator/src/models/config/font_cycle.rs | 16 +- .../src/models/config/performance_fields.rs | 24 +- configurator/src/models/config/presets.rs | 2 +- .../src/models/config/render_profiles.rs | 13 +- configurator/src/models/config/setters.rs | 4 +- configurator/src/models/config/tests.rs | 107 +-- .../src/models/config/to_config/drawing.rs | 5 +- .../src/models/config/to_config/ui.rs | 4 +- configurator/src/models/daemon.rs | 8 +- configurator/src/models/fields/arrow.rs | 7 +- configurator/src/models/fields/capture.rs | 7 +- configurator/src/models/fields/eraser.rs | 15 +- configurator/src/models/fields/export.rs | 27 +- configurator/src/models/fields/font.rs | 11 +- configurator/src/models/fields/input_hud.rs | 11 +- configurator/src/models/fields/presenter.rs | 11 +- configurator/src/models/fields/pressure.rs | 11 +- configurator/src/models/fields/session.rs | 11 +- configurator/src/models/fields/status.rs | 7 +- configurator/src/models/fields/tests.rs | 34 +- configurator/src/models/fields/theme.rs | 20 +- configurator/src/models/fields/tool.rs | 15 +- configurator/src/models/fields/toolbar.rs | 23 +- .../src/models/keybindings/analysis.rs | 13 +- .../src/models/keybindings/conflicts.rs | 12 +- configurator/src/models/keybindings/parse.rs | 5 +- .../src/models/keybindings/recording.rs | 106 +-- configurator/src/models/keybindings/tests.rs | 14 +- configurator/src/models/session.rs | 33 +- configurator/src/models/session/tests.rs | 26 +- configurator/src/models/startup.rs | 4 +- configurator/src/models/tab.rs | 13 +- configurator/src/test_env.rs | 143 ++-- src/about_window/clipboard.rs | 22 +- src/about_window/content.rs | 21 +- src/about_window/diagnostics.rs | 27 +- src/about_window/handlers/compositor.rs | 6 +- src/about_window/handlers/dispatch.rs | 3 +- src/about_window/handlers/keyboard.rs | 6 +- src/about_window/handlers/output.rs | 3 +- src/about_window/handlers/pointer.rs | 3 +- src/about_window/handlers/seat.rs | 6 +- src/about_window/interaction.rs | 19 +- src/about_window/mod.rs | 57 +- src/about_window/render/draw.rs | 12 +- src/about_window/render/draw/tests.rs | 12 +- src/about_window/render/mod.rs | 3 +- src/about_window/render/widgets.rs | 30 +- src/about_window/state.rs | 30 +- src/app/env.rs | 23 +- src/app/languages.rs | 19 +- src/app/mod.rs | 119 ++-- src/app/session.rs | 45 +- src/app_id.rs | 10 +- src/backend/mod.rs | 13 +- .../wayland/backend/event_loop/capture.rs | 52 +- .../wayland/backend/event_loop/dispatch.rs | 8 +- .../wayland/backend/event_loop/interaction.rs | 10 +- src/backend/wayland/backend/event_loop/mod.rs | 73 ++- .../backend/event_loop/session_save.rs | 37 +- .../event_loop/session_save/diagnostics.rs | 4 +- .../event_loop/session_save/final_save.rs | 52 +- .../event_loop/session_save/interaction.rs | 60 +- .../event_loop/session_save/notifications.rs | 28 +- .../backend/event_loop/session_save/tests.rs | 65 +- src/backend/wayland/backend/helpers.rs | 69 +- src/backend/wayland/backend/mod.rs | 9 +- src/backend/wayland/backend/runtime_wake.rs | 18 +- src/backend/wayland/backend/setup.rs | 52 +- src/backend/wayland/backend/signals.rs | 27 +- .../wayland/backend/state_init/config.rs | 51 +- src/backend/wayland/backend/state_init/mod.rs | 100 +-- .../wayland/backend/state_init/session.rs | 11 +- src/backend/wayland/backend/surface.rs | 6 +- src/backend/wayland/backend/tray.rs | 13 +- src/backend/wayland/capture.rs | 69 +- src/backend/wayland/capture_preflight.rs | 19 +- .../wayland/capture_preflight/retry.rs | 6 +- src/backend/wayland/clipboard/file_list.rs | 37 +- src/backend/wayland/clipboard/system.rs | 10 +- .../wayland/clipboard/system/command.rs | 8 +- src/backend/wayland/clipboard/transfer.rs | 41 +- .../wayland/clipboard/transfer/tests.rs | 11 +- src/backend/wayland/config_edits.rs | 8 +- src/backend/wayland/config_edits/tests.rs | 77 ++- src/backend/wayland/frozen/capture.rs | 38 +- src/backend/wayland/frozen/ext_image_copy.rs | 48 +- src/backend/wayland/frozen/image.rs | 2 +- src/backend/wayland/frozen/mod.rs | 9 +- src/backend/wayland/frozen/portal.rs | 97 +-- src/backend/wayland/frozen/retry.rs | 22 +- src/backend/wayland/frozen/state.rs | 37 +- .../wayland/frozen/state/activation.rs | 8 +- src/backend/wayland/frozen/state/admission.rs | 6 +- src/backend/wayland/handlers/activation.rs | 6 +- src/backend/wayland/handlers/buffer.rs | 3 +- src/backend/wayland/handlers/compositor.rs | 10 +- .../wayland/handlers/ext_image_copy.rs | 22 +- src/backend/wayland/handlers/keyboard/mod.rs | 194 +++--- .../wayland/handlers/keyboard/press.rs | 21 +- src/backend/wayland/handlers/layer.rs | 6 +- src/backend/wayland/handlers/output.rs | 4 +- src/backend/wayland/handlers/pointer/axis.rs | 70 +- .../wayland/handlers/pointer/cursor.rs | 23 +- .../wayland/handlers/pointer/enter_leave.rs | 4 +- src/backend/wayland/handlers/pointer/mod.rs | 6 +- .../wayland/handlers/pointer/motion.rs | 8 +- src/backend/wayland/handlers/pointer/press.rs | 45 +- .../wayland/handlers/pointer/press/toolbar.rs | 9 +- .../wayland/handlers/pointer/release.rs | 13 +- src/backend/wayland/handlers/pointer/tests.rs | 65 +- .../wayland/handlers/pointer_constraints.rs | 11 +- src/backend/wayland/handlers/registry.rs | 10 +- .../wayland/handlers/relative_pointer.rs | 6 +- src/backend/wayland/handlers/screencopy.rs | 14 +- src/backend/wayland/handlers/seat.rs | 21 +- src/backend/wayland/handlers/tablet/frame.rs | 19 +- .../wayland/handlers/tablet/frame/tests.rs | 86 ++- src/backend/wayland/handlers/tablet/pad.rs | 11 +- .../wayland/handlers/tablet/pad_group.rs | 13 +- src/backend/wayland/handlers/tablet/seat.rs | 14 +- src/backend/wayland/handlers/tablet/tool.rs | 32 +- .../wayland/handlers/tablet/tool/tests.rs | 10 +- src/backend/wayland/handlers/test_support.rs | 105 +-- src/backend/wayland/handlers/text_input.rs | 9 +- src/backend/wayland/handlers/touch.rs | 6 +- src/backend/wayland/handlers/xdg.rs | 3 +- src/backend/wayland/input_monitor/monitor.rs | 22 +- .../wayland/input_monitor/monitor/worker.rs | 54 +- .../input_monitor/monitor/worker/repeat.rs | 15 +- .../input_monitor/monitor/worker/tests.rs | 4 +- src/backend/wayland/input_monitor/probe.rs | 56 +- .../wayland/input_monitor/translate.rs | 4 +- src/backend/wayland/portal_capture.rs | 37 +- src/backend/wayland/portal_raster.rs | 43 +- src/backend/wayland/portal_task.rs | 63 +- src/backend/wayland/runtime_operation.rs | 41 +- src/backend/wayland/runtime_ui_state.rs | 48 +- src/backend/wayland/runtime_ui_state/board.rs | 29 +- .../wayland/runtime_ui_state/coordinator.rs | 65 +- .../wayland/runtime_ui_state/lifecycle.rs | 13 +- .../wayland/runtime_ui_state/live_state.rs | 83 ++- .../wayland/runtime_ui_state/positions.rs | 12 +- .../wayland/runtime_ui_state/rollback.rs | 14 +- .../wayland/runtime_ui_state/seed_refresh.rs | 29 +- src/backend/wayland/runtime_ui_state/seeds.rs | 27 +- src/backend/wayland/runtime_ui_state/tests.rs | 37 +- .../tests/board_pin_resets.rs | 36 +- .../runtime_ui_state/tests/board_pins.rs | 62 +- .../runtime_ui_state/tests/drag_previews.rs | 38 +- .../runtime_ui_state/tests/layout_state.rs | 44 +- .../tests/preference_actions.rs | 31 +- .../runtime_ui_state/tests/preferences.rs | 40 +- .../runtime_ui_state/tests/recovery.rs | 86 +-- .../runtime_ui_state/tests/text_geometry.rs | 14 +- .../runtime_ui_state/tests/visibility.rs | 66 +- .../tests/visibility_recovery.rs | 40 +- .../wayland/runtime_ui_state/wayland.rs | 25 +- src/backend/wayland/session.rs | 22 +- src/backend/wayland/session/driver.rs | 18 +- src/backend/wayland/session/driver/tests.rs | 164 +++-- .../driver/tests/lifecycle_regressions.rs | 43 +- .../wayland/session/driver/tests/output.rs | 134 ++-- src/backend/wayland/session/home.rs | 3 +- src/backend/wayland/session/home/tests.rs | 131 ++-- src/backend/wayland/session/load_outcome.rs | 7 +- src/backend/wayland/session/persistence.rs | 62 +- .../session/persistence/test_support.rs | 3 +- .../wayland/session/persistence/worker.rs | 25 +- src/backend/wayland/session/runtime.rs | 78 ++- .../wayland/session/runtime/transaction.rs | 39 +- .../session/runtime/transaction/output.rs | 11 +- .../session/runtime/transaction/phases.rs | 31 +- src/backend/wayland/session/tests.rs | 436 +++++++------ src/backend/wayland/session/write_policy.rs | 15 +- src/backend/wayland/state.rs | 130 ++-- src/backend/wayland/state/acquisition.rs | 43 +- .../wayland/state/acquisition/transaction.rs | 17 +- .../state/acquisition/transaction/tests.rs | 17 +- src/backend/wayland/state/activation.rs | 4 +- src/backend/wayland/state/buffer_damage.rs | 8 +- src/backend/wayland/state/canvas_layer.rs | 45 +- src/backend/wayland/state/capture.rs | 45 +- src/backend/wayland/state/capture/backdrop.rs | 10 +- src/backend/wayland/state/capture/barrier.rs | 34 +- src/backend/wayland/state/capture/pdf.rs | 26 +- src/backend/wayland/state/capture/retry.rs | 25 +- src/backend/wayland/state/clipboard.rs | 30 +- .../wayland/state/clipboard/fallback_save.rs | 145 +++-- .../wayland/state/clipboard/probe_tests.rs | 32 +- .../wayland/state/clipboard/session_paste.rs | 14 +- .../state/clipboard/session_paste/tests.rs | 3 +- .../wayland/state/clipboard_runtime.rs | 46 +- src/backend/wayland/state/color_picker.rs | 16 +- src/backend/wayland/state/core/accessors.rs | 7 +- src/backend/wayland/state/core/init.rs | 23 +- src/backend/wayland/state/core/output.rs | 49 +- .../wayland/state/core/output/focus.rs | 13 +- .../wayland/state/core/output/identity.rs | 16 +- .../wayland/state/core/output/session_ops.rs | 18 +- .../wayland/state/core/output/tests.rs | 135 ++-- .../wayland/state/core/output/transition.rs | 41 +- src/backend/wayland/state/core/overlay.rs | 9 +- src/backend/wayland/state/core/session.rs | 12 +- .../wayland/state/core/session_home.rs | 2 +- src/backend/wayland/state/desktop_open.rs | 36 +- src/backend/wayland/state/eyedropper.rs | 85 ++- src/backend/wayland/state/focus.rs | 9 +- src/backend/wayland/state/font_catalog.rs | 7 +- src/backend/wayland/state/gtk_toolbar.rs | 43 +- src/backend/wayland/state/helper_launch.rs | 36 +- src/backend/wayland/state/helpers.rs | 32 +- src/backend/wayland/state/input_actions.rs | 14 +- src/backend/wayland/state/input_hud.rs | 43 +- src/backend/wayland/state/key_repeat.rs | 3 +- src/backend/wayland/state/keybindings.rs | 13 - .../state/keybindings/implementation.rs | 13 +- .../state/keybindings/implementation/tests.rs | 3 +- .../implementation/tests/persistence.rs | 37 +- src/backend/wayland/state/ocr.rs | 99 ++- src/backend/wayland/state/onboarding.rs | 25 +- src/backend/wayland/state/onboarding/card.rs | 3 +- .../wayland/state/onboarding/first_run.rs | 92 ++- .../state/onboarding/first_run_card.rs | 3 +- src/backend/wayland/state/onboarding/tests.rs | 123 ++-- src/backend/wayland/state/pdf_export.rs | 22 +- src/backend/wayland/state/pdf_export/tests.rs | 4 +- src/backend/wayland/state/perf.rs | 27 +- .../state/perf_modules/damage_diagnostics.rs | 4 +- .../wayland/state/perf_modules/metrics.rs | 25 +- .../state/perf_modules/metrics/tests.rs | 4 + .../state/perf_modules/render_breakdown.rs | 13 +- src/backend/wayland/state/pointer_runtime.rs | 35 +- .../wayland/state/pointer_runtime/chrome.rs | 9 +- .../wayland/state/pointer_runtime/contacts.rs | 28 +- .../wayland/state/pointer_runtime/tests.rs | 19 +- .../wayland/state/preference_stores.rs | 16 +- src/backend/wayland/state/protocol_globals.rs | 22 +- src/backend/wayland/state/region_capture.rs | 34 +- .../state/region_capture/active_state.rs | 17 +- .../wayland/state/region_capture/board.rs | 9 +- .../state/region_capture/cut_preview/apply.rs | 15 +- .../region_capture/cut_preview/scheduler.rs | 10 +- .../region_capture/cut_preview/snapshot.rs | 14 +- .../state/region_capture/cut_preview/tests.rs | 103 ++- .../region_capture/cut_review/actions.rs | 40 +- .../state/region_capture/cut_review/model.rs | 14 +- .../state/region_capture/cut_review/tests.rs | 55 +- .../wayland/state/region_capture/delivery.rs | 24 +- .../wayland/state/region_capture/events.rs | 59 +- .../wayland/state/region_capture/geometry.rs | 11 +- .../wayland/state/region_capture/intent.rs | 11 +- .../wayland/state/region_capture/measure.rs | 3 +- .../wayland/state/region_capture/picker.rs | 64 +- .../wayland/state/region_capture/render.rs | 32 +- .../state/region_capture/review_state.rs | 13 +- .../wayland/state/region_capture/runtime.rs | 92 ++- .../state/region_capture/selection_state.rs | 6 +- .../state/region_capture/source_guard.rs | 4 +- .../region_capture/tests/capture_selection.rs | 6 +- .../tests/event_characterization.rs | 6 +- .../tests/lifecycle_and_measure.rs | 19 +- .../wayland/state/region_capture/tests/ocr.rs | 26 +- .../state/region_capture/tests/picker.rs | 117 ++-- .../state/region_capture/tests/review.rs | 23 +- .../state/region_capture/window_snap.rs | 4 +- .../state/region_capture/window_snap/query.rs | 44 +- .../wayland/state/render/canvas/background.rs | 63 +- .../wayland/state/render/canvas/context.rs | 10 +- .../wayland/state/render/canvas/mod.rs | 72 ++- .../wayland/state/render/canvas/overlays.rs | 23 +- .../state/render/canvas/resource_tests.rs | 49 +- .../canvas/resource_tests/grid_performance.rs | 19 +- .../wayland/state/render/canvas/text.rs | 68 +- .../wayland/state/render/canvas/text/ime.rs | 14 +- .../wayland/state/render/measure_badge.rs | 18 +- src/backend/wayland/state/render/mod.rs | 10 +- src/backend/wayland/state/render/paint.rs | 23 +- src/backend/wayland/state/render/prepare.rs | 27 +- src/backend/wayland/state/render/profile.rs | 10 +- .../wayland/state/render/profile/tests.rs | 12 +- .../wayland/state/render/recognition_chip.rs | 12 +- src/backend/wayland/state/render/runtime.rs | 90 +-- src/backend/wayland/state/render/submit.rs | 11 +- .../wayland/state/render/tool_preview.rs | 20 +- src/backend/wayland/state/render/ui.rs | 135 ++-- .../wayland/state/render/ui_effect_damage.rs | 118 ++-- src/backend/wayland/state/screen_image.rs | 45 +- src/backend/wayland/state/tablet_runtime.rs | 27 +- .../wayland/state/tablet_runtime/pressure.rs | 17 +- src/backend/wayland/state/tests.rs | 3 +- src/backend/wayland/state/text_clipboard.rs | 6 +- src/backend/wayland/state/text_input.rs | 8 +- src/backend/wayland/state/toolbar.rs | 14 +- src/backend/wayland/state/toolbar/chrome.rs | 33 +- .../wayland/state/toolbar/drag/base.rs | 3 +- .../wayland/state/toolbar/drag/clamp.rs | 5 +- .../wayland/state/toolbar/drag/handoff.rs | 7 +- src/backend/wayland/state/toolbar/drag/mod.rs | 4 +- .../wayland/state/toolbar/drag/move_drag.rs | 5 +- .../wayland/state/toolbar/drag/relative.rs | 4 +- .../wayland/state/toolbar/drag/state.rs | 5 +- src/backend/wayland/state/toolbar/events.rs | 42 +- .../wayland/state/toolbar/events/presets.rs | 51 +- .../state/toolbar/events/quick_colors.rs | 48 +- .../wayland/state/toolbar/events/session.rs | 71 +- .../state/toolbar/events/session/dialog.rs | 53 +- .../state/toolbar/events/session/home.rs | 3 +- .../state/toolbar/events/session/queued.rs | 13 +- .../state/toolbar/events/session/tests.rs | 19 +- .../wayland/state/toolbar/events/tests.rs | 199 +++--- src/backend/wayland/state/toolbar/fade.rs | 41 +- src/backend/wayland/state/toolbar/geometry.rs | 19 +- .../wayland/state/toolbar/inline/drag.rs | 6 +- .../wayland/state/toolbar/inline/focus.rs | 2 +- .../wayland/state/toolbar/inline/input.rs | 11 +- .../wayland/state/toolbar/inline/mod.rs | 10 +- .../wayland/state/toolbar/inline/render.rs | 8 +- src/backend/wayland/state/toolbar/scroll.rs | 12 +- .../state/toolbar/visibility/access.rs | 2 +- .../wayland/state/toolbar/visibility/mod.rs | 3 +- .../state/toolbar/visibility/pointer.rs | 9 +- .../wayland/state/toolbar/visibility/sync.rs | 30 +- src/backend/wayland/state/zoom.rs | 7 +- src/backend/wayland/surface.rs | 34 +- .../wayland/surface/output_membership.rs | 8 +- src/backend/wayland/surface/placement.rs | 15 +- src/backend/wayland/tablet_types.rs | 14 +- src/backend/wayland/toolbar/hit.rs | 15 +- .../wayland/toolbar/layout/spec/top.rs | 21 +- .../wayland/toolbar/layout/tests/mod.rs | 237 +++---- src/backend/wayland/toolbar/main/lifecycle.rs | 16 +- src/backend/wayland/toolbar/main/render.rs | 5 +- src/backend/wayland/toolbar/main/state.rs | 3 +- src/backend/wayland/toolbar/mod.rs | 4 +- src/backend/wayland/toolbar/render.rs | 5 +- src/backend/wayland/toolbar/render/paint.rs | 66 +- .../wayland/toolbar/render/top_strip/tests.rs | 22 +- .../wayland/toolbar/render/widgets/buttons.rs | 17 +- .../wayland/toolbar/surfaces/lifecycle.rs | 13 +- .../wayland/toolbar/surfaces/render.rs | 14 +- src/backend/wayland/toolbar/surfaces/state.rs | 17 +- .../wayland/toolbar/surfaces/structs.rs | 23 +- src/backend/wayland/toolbar/view/node.rs | 22 +- src/backend/wayland/toolbar/view/top.rs | 8 +- .../toolbar/view/top/arrow_menu/tests.rs | 3 +- .../wayland/toolbar/view/top/layout_menu.rs | 3 +- src/backend/wayland/toolbar/view/top/menus.rs | 9 +- src/backend/wayland/toolbar/view/top/meter.rs | 32 +- .../toolbar/view/top/pen_feel/tests.rs | 9 +- src/backend/wayland/toolbar/view/top/tests.rs | 469 +++++--------- .../wayland/toolbar/view/top/tests/engine.rs | 9 +- src/backend/wayland/toolbar/view/tree.rs | 3 +- src/backend/wayland/zoom/capture.rs | 28 +- src/backend/wayland/zoom/mod.rs | 9 +- src/backend/wayland/zoom/portal.rs | 137 ++-- src/backend/wayland/zoom/retry.rs | 22 +- src/backend/wayland/zoom/state.rs | 14 +- src/backend/wayland/zoom/state/source.rs | 20 +- src/base64.rs | 3 +- src/build_info.rs | 8 +- src/canvas_export/mod.rs | 21 +- src/canvas_export/page.rs | 57 +- src/canvas_export/pdf.rs | 19 +- src/canvas_export/pdf/tests.rs | 36 +- src/canvas_export/pdf_labels.rs | 5 +- src/canvas_export/png.rs | 4 +- src/canvas_export/region.rs | 87 +-- src/canvas_export/tests/board_grid.rs | 20 +- src/canvas_export/tests/cache_tests.rs | 32 +- src/capture/band_cut.rs | 19 +- src/capture/clipboard.rs | 18 +- src/capture/dependencies.rs | 16 +- src/capture/desktop_backdrop.rs | 27 +- src/capture/file.rs | 53 +- src/capture/manager.rs | 61 +- src/capture/pipeline.rs | 25 +- src/capture/png.rs | 2 +- src/capture/portal.rs | 75 +-- src/capture/portal/tests/transport.rs | 32 +- src/capture/sources/frozen.rs | 6 +- src/capture/sources/hyprland.rs | 13 +- src/capture/sources/portal.rs | 10 +- src/capture/sources/reader.rs | 46 +- src/capture/tests/desktop_backdrop.rs | 3 +- src/capture/tests/fixtures.rs | 23 +- src/capture/tests/manager.rs | 98 ++- src/capture/tests/perform_capture.rs | 28 +- src/capture/types.rs | 41 +- src/capture/window_geometry/mod.rs | 6 +- src/capture/window_geometry/types.rs | 9 +- src/cli.rs | 10 +- src/clipboard_text.rs | 11 +- src/config/action_meta/entries/tools.rs | 28 +- src/config/action_meta/mod.rs | 28 +- src/config/action_meta/tests.rs | 18 +- src/config/core.rs | 2 + src/config/document.rs | 28 +- src/config/document/lock.rs | 21 +- src/config/enums.rs | 15 +- src/config/field_metadata.rs | 4 + src/config/io.rs | 71 +- src/config/keybindings.rs | 8 +- src/config/keybindings/authorship.rs | 2 + src/config/keybindings/binding.rs | 61 +- src/config/keybindings/config/map/edit.rs | 4 +- src/config/keybindings/config/map/mod.rs | 25 +- .../config/types/bindings/board.rs | 10 +- .../config/types/bindings/capture.rs | 11 +- .../config/types/bindings/colors.rs | 6 +- .../keybindings/config/types/bindings/core.rs | 5 +- .../config/types/bindings/presets.rs | 7 +- .../config/types/bindings/selection.rs | 10 +- .../config/types/bindings/tools.rs | 17 +- .../keybindings/config/types/bindings/ui.rs | 11 +- .../keybindings/config/types/bindings/zoom.rs | 5 +- src/config/keybindings/shortcut.rs | 88 ++- src/config/keybindings/tests.rs | 72 ++- src/config/migration.rs | 7 + src/config/mod.rs | 6 +- src/config/salvage.rs | 5 +- src/config/schema.rs | 6 + src/config/test_helpers.rs | 28 +- src/config/tests/board_grid.rs | 7 +- src/config/tests/document.rs | 50 +- src/config/tests/file_io.rs | 18 +- src/config/tests/finite.rs | 19 +- src/config/tests/load.rs | 101 ++- src/config/tests/validate.rs | 34 +- src/config/tests/write_lock.rs | 15 +- src/config/tests/write_target.rs | 46 +- src/config/types/boards.rs | 5 + src/config/types/capture.rs | 4 +- src/config/types/drawing.rs | 32 + src/config/types/input_hud.rs | 4 + src/config/types/mod.rs | 9 +- src/config/types/presets.rs | 9 +- src/config/types/session.rs | 4 +- src/config/types/spotlight.rs | 4 +- src/config/types/status_bar.rs | 2 + src/config/types/toolbar/display.rs | 1 + src/config/types/toolbar/items.rs | 14 +- src/config/types/toolbar/items/definitions.rs | 11 +- src/config/types/toolbar/items/order.rs | 10 +- src/config/types/toolbar/items/tests.rs | 10 +- src/config/types/toolbar/mode.rs | 2 + src/config/types/toolbar/overrides.rs | 1 + src/config/types/toolbar/rebind.rs | 2 + src/config/types/toolbar/visibility.rs | 5 + src/config/types/ui.rs | 1 + src/config/types/updates.rs | 11 +- src/config/validate/arrow.rs | 7 +- src/config/validate/board.rs | 7 +- src/config/validate/boards.rs | 12 +- src/config/validate/drawing.rs | 11 +- src/config/validate/keybindings.rs | 18 + src/config/validate/mod.rs | 1 + src/config/validate/presets.rs | 3 +- src/config/validate/render_profiles.rs | 4 +- src/config/validate/save.rs | 24 +- src/config/validate/spotlight.rs | 13 +- src/config/validate/tablet.rs | 11 +- src/config/validate/ui/styles.rs | 7 +- src/configurator_destination.rs | 12 + src/daemon/binary_conflict.rs | 30 +- src/daemon/control.rs | 69 +- src/daemon/control/queue.rs | 14 +- src/daemon/control/response.rs | 8 +- src/daemon/control/runtime.rs | 7 +- src/daemon/control/tests.rs | 178 +++-- src/daemon/core.rs | 80 ++- src/daemon/core/tests.rs | 144 ++--- src/daemon/core/toggles.rs | 17 +- src/daemon/global_shortcuts.rs | 90 +-- src/daemon/icons.rs | 5 +- src/daemon/language.rs | 23 +- src/daemon/overlay/launch.rs | 7 +- src/daemon/overlay/lifecycle.rs | 19 +- src/daemon/overlay/lifecycle/stop.rs | 10 +- src/daemon/overlay/spawn.rs | 37 +- src/daemon/overlay/tests.rs | 52 +- src/daemon/overlay/tests/fake_overlay.rs | 73 ++- src/daemon/protocol_v2/action.rs | 41 +- src/daemon/protocol_v2/action/abandon.rs | 8 +- src/daemon/protocol_v2/action/claim.rs | 10 +- src/daemon/protocol_v2/action/claimed.rs | 11 +- src/daemon/protocol_v2/action/open.rs | 23 +- src/daemon/protocol_v2/action/tests.rs | 14 +- src/daemon/protocol_v2/child.rs | 174 ++--- src/daemon/protocol_v2/command/claimed.rs | 11 +- src/daemon/protocol_v2/command/client.rs | 20 +- src/daemon/protocol_v2/command/layout.rs | 21 +- src/daemon/protocol_v2/command/owner.rs | 8 +- src/daemon/protocol_v2/command/tests.rs | 10 +- src/daemon/protocol_v2/digest.rs | 2 +- src/daemon/protocol_v2/linux.rs | 58 +- src/daemon/protocol_v2/mod.rs | 24 +- src/daemon/protocol_v2/runtime.rs | 28 +- src/daemon/protocol_v2/session_target.rs | 35 +- .../protocol_v2/session_target/tests.rs | 34 +- src/daemon/protocol_v2/tests.rs | 71 +- src/daemon/protocol_v2/wire.rs | 6 +- src/daemon/setup.rs | 18 +- src/daemon/tests.rs | 65 +- src/daemon/tray/helpers.rs | 58 +- src/daemon/tray/icon_theme_path.rs | 13 +- src/daemon/tray/ksni.rs | 9 +- src/daemon/tray/mod.rs | 12 +- src/daemon/tray/runtime.rs | 25 +- src/daemon/tray/shortcut_hint_io.rs | 6 +- src/daemon/types.rs | 56 +- src/daemon/update_watch.rs | 70 +- src/desktop_open.rs | 21 +- src/domain/action.rs | 1 + src/domain/board.rs | 1 + src/domain/board_grid.rs | 4 + src/domain/board_validation.rs | 2 + src/domain/color.rs | 17 +- src/domain/drawing.rs | 1 + src/domain/language.rs | 6 + src/domain/tests.rs | 42 +- src/domain/tool.rs | 4 + src/draw/canvas_set/pages.rs | 12 +- src/draw/dirty.rs | 5 +- src/draw/font.rs | 21 +- src/draw/frame/core.rs | 18 +- src/draw/frame/frame_storage.rs | 4 +- src/draw/frame/history/frame/apply.rs | 4 +- src/draw/frame/history/frame/mod.rs | 3 + src/draw/frame/shapes.rs | 9 +- src/draw/frame/tests/history/basics.rs | 5 +- src/draw/frame/tests/history/limits.rs | 6 +- src/draw/frame/tests/history/prune.rs | 5 +- src/draw/frame/tests/history/validate.rs | 3 +- src/draw/frame/tests/serialization.rs | 6 +- src/draw/frame/types.rs | 4 + src/draw/mod.rs | 6 +- src/draw/render/backdrop_probe.rs | 2 + src/draw/render/background.rs | 8 +- src/draw/render/board_grid.rs | 9 +- src/draw/render/board_grid/performance.rs | 12 +- src/draw/render/board_grid/tests.rs | 12 +- src/draw/render/context.rs | 32 +- src/draw/render/context/text_tests.rs | 6 +- src/draw/render/highlight.rs | 6 +- src/draw/render/image.rs | 22 +- src/draw/render/mod.rs | 3 +- src/draw/render/pressure_strokes.rs | 10 +- src/draw/render/primitives.rs | 17 +- src/draw/render/selection.rs | 10 +- src/draw/render/shapes.rs | 24 +- src/draw/render/spotlight.rs | 48 +- src/draw/render/strokes.rs | 4 +- src/draw/render/text.rs | 63 +- src/draw/render/types.rs | 6 +- src/draw/shape/arrow_label.rs | 3 +- src/draw/shape/bounds.rs | 6 +- src/draw/shape/polygon.rs | 8 +- src/draw/shape/smoothing.rs | 7 +- src/draw/shape/step_marker.rs | 6 +- src/draw/shape/stroke_points.rs | 13 +- src/draw/shape/tests.rs | 16 +- src/draw/shape/text.rs | 7 +- src/draw/shape/text_cache.rs | 3 +- src/draw/shape/text_cache/cursor.rs | 7 +- src/draw/shape/text_cache/owner/tests.rs | 9 +- src/draw/shape/text_cache/tests.rs | 16 +- src/draw/shape/transform.rs | 4 +- src/draw/shape/transform/tests.rs | 4 +- src/draw/shape/types.rs | 23 +- src/draw/spotlight.rs | 4 + src/durable_io.rs | 22 +- src/durable_io/model.rs | 16 +- src/durable_io/tests.rs | 47 +- src/help_overlay_interaction.rs | 2 + src/i18n.rs | 30 +- src/i18n/tests.rs | 37 +- src/image_decode.rs | 11 +- src/input/boards.rs | 1 + src/input/boards/appearance.rs | 1 + src/input/boards/color.rs | 8 +- src/input/boards/core.rs | 19 + src/input/boards/mapping.rs | 11 +- src/input/boards/naming.rs | 4 +- src/input/boards/operations.rs | 18 +- src/input/hit_test/mod.rs | 3 +- src/input/hit_test/shapes.rs | 5 +- src/input/hit_test/tests.rs | 18 +- src/input/modifiers.rs | 35 +- src/input/state/actions/action_board_pages.rs | 4 +- .../state/actions/action_capture_zoom.rs | 4 +- src/input/state/actions/action_colors.rs | 10 +- src/input/state/actions/action_core.rs | 6 +- src/input/state/actions/action_dispatch.rs | 7 +- src/input/state/actions/action_history.rs | 14 +- src/input/state/actions/action_presets.rs | 3 +- src/input/state/actions/action_selection.rs | 9 +- src/input/state/actions/action_tools.rs | 8 +- src/input/state/actions/action_ui.rs | 36 +- .../state/actions/key_press/caret_edit.rs | 14 +- src/input/state/actions/key_press/mod.rs | 21 +- src/input/state/actions/key_press/panels.rs | 15 +- .../state/actions/key_press/text_input.rs | 17 +- .../key_press/text_input/measurement_tests.rs | 4 +- .../state/core/base/input_effect_outbox.rs | 9 +- src/input/state/core/base/mod.rs | 9 +- src/input/state/core/base/state/init.rs | 31 +- src/input/state/core/base/state/modifiers.rs | 41 +- src/input/state/core/base/state/structs.rs | 70 +- src/input/state/core/base/toast_queue.rs | 72 ++- src/input/state/core/base/types.rs | 23 +- src/input/state/core/board/appearance.rs | 7 +- src/input/state/core/board/delete_restore.rs | 4 +- .../state/core/board/delete_restore/page.rs | 10 +- .../state/core/board/delete_restore/tests.rs | 38 +- src/input/state/core/board/pages.rs | 11 +- src/input/state/core/board/switch.rs | 30 +- .../state/core/board_picker/appearance.rs | 28 +- .../core/board_picker/appearance/tests.rs | 49 +- .../layout/compute/content_metrics.rs | 6 +- .../core/board_picker/layout/hit_test.rs | 4 +- src/input/state/core/board_picker/search.rs | 14 +- .../state/core/board_picker/state/actions.rs | 25 +- .../state/core/board_picker/state/drag.rs | 4 +- .../state/core/board_picker/state/edit.rs | 20 +- .../core/board_picker/state/lifecycle.rs | 9 +- .../state/core/board_picker/state/nav.rs | 5 +- src/input/state/core/captured_image.rs | 23 +- .../state/core/color_picker_popup/mod.rs | 21 + .../state/core/color_picker_popup/state.rs | 6 +- src/input/state/core/command_palette/input.rs | 32 +- src/input/state/core/command_palette/mod.rs | 611 ++++++------------ .../state/core/command_palette/search.rs | 12 +- .../command_palette/search/language_tests.rs | 35 +- src/input/state/core/command_palette/state.rs | 28 +- src/input/state/core/dirty.rs | 11 +- src/input/state/core/editing.rs | 12 +- src/input/state/core/editing/tests.rs | 4 +- src/input/state/core/eyedropper.rs | 18 +- src/input/state/core/font_cycle.rs | 17 +- src/input/state/core/font_picker/input.rs | 13 +- src/input/state/core/font_picker/layout.rs | 6 +- src/input/state/core/font_picker/mod.rs | 11 +- src/input/state/core/font_picker/state.rs | 3 +- src/input/state/core/font_picker/tests.rs | 55 +- src/input/state/core/help_overlay/tests.rs | 117 ++-- src/input/state/core/history.rs | 9 +- src/input/state/core/ime.rs | 13 +- src/input/state/core/index.rs | 5 +- src/input/state/core/index/grid.rs | 6 +- .../state/core/index/measurement_tests.rs | 4 +- src/input/state/core/index/owner.rs | 14 +- src/input/state/core/input_hud_controls.rs | 3 +- src/input/state/core/keymap.rs | 4 +- src/input/state/core/laser_controls.rs | 9 +- src/input/state/core/menus/commands.rs | 38 +- src/input/state/core/menus/context_menu.rs | 3 +- src/input/state/core/menus/focus.rs | 8 +- src/input/state/core/menus/hover.rs | 4 +- src/input/state/core/menus/lifecycle.rs | 15 +- src/input/state/core/menus/types.rs | 2 + src/input/state/core/mod.rs | 22 +- src/input/state/core/modal.rs | 67 +- src/input/state/core/modes.rs | 73 ++- src/input/state/core/pointer.rs | 8 +- src/input/state/core/presets.rs | 9 +- .../apply_selection/actions/arrow.rs | 50 +- .../apply_selection/actions/color.rs | 20 +- .../apply_selection/actions/fill.rs | 11 +- .../apply_selection/actions/preset.rs | 2 +- .../apply_selection/actions/spotlight.rs | 12 +- .../apply_selection/actions/stroke.rs | 8 +- .../apply_selection/actions/text.rs | 11 +- .../properties/apply_selection/helpers.rs | 2 +- .../apply_selection/helpers/tests.rs | 20 +- src/input/state/core/properties/entries.rs | 15 +- src/input/state/core/properties/metrics.rs | 7 +- src/input/state/core/properties/panel.rs | 8 +- .../core/properties/panel_layout/focus.rs | 7 +- .../core/properties/panel_layout/geometry.rs | 3 +- .../core/properties/panel_layout/layout.rs | 16 +- .../core/properties/panel_layout/tests.rs | 37 +- src/input/state/core/properties/slider.rs | 4 +- src/input/state/core/properties/state.rs | 4 +- src/input/state/core/properties/summary.rs | 6 +- src/input/state/core/properties/types.rs | 8 +- src/input/state/core/properties/utils.rs | 12 +- src/input/state/core/radial_menu/compass.rs | 9 +- src/input/state/core/radial_menu/mod.rs | 4 +- src/input/state/core/radial_menu/size_ring.rs | 2 + src/input/state/core/radial_menu/state.rs | 41 +- src/input/state/core/recognition_chip.rs | 8 +- src/input/state/core/region_select.rs | 44 +- src/input/state/core/selection/clipboard.rs | 7 +- .../core/selection_actions/arrow_bend.rs | 30 +- .../selection_actions/arrow_bend/tests.rs | 78 +-- .../state/core/selection_actions/clipboard.rs | 3 +- .../clipboard/image_paste.rs | 3 +- .../state/core/selection_actions/delete.rs | 3 +- .../core/selection_actions/delete/tests.rs | 10 +- .../state/core/selection_actions/geometry.rs | 3 +- .../state/core/selection_actions/handles.rs | 4 +- .../core/selection_actions/handles/tests.rs | 15 +- .../selection_actions/measurement_tests.rs | 3 +- .../measurement_tests/mutations.rs | 9 +- .../state/core/selection_actions/reorder.rs | 3 +- .../state/core/selection_actions/resize.rs | 3 +- .../resize/resize_helpers.rs | 3 +- .../state/core/selection_actions/spotlight.rs | 23 +- .../state/core/selection_actions/state.rs | 3 +- .../state/core/selection_actions/text/edit.rs | 4 +- .../core/selection_actions/text/handles.rs | 3 +- .../state/core/selection_actions/text/wrap.rs | 3 +- .../core/selection_actions/translation/mod.rs | 3 +- .../selection_actions/translation/restore.rs | 3 +- .../selection_actions/translation/undo.rs | 4 +- src/input/state/core/session.rs | 43 +- src/input/state/core/session_flags.rs | 4 +- src/input/state/core/session_preflight.rs | 13 +- .../state/core/session_preflight_exact.rs | 10 +- src/input/state/core/status_hud.rs | 54 +- src/input/state/core/status_hud/state.rs | 3 +- src/input/state/core/style.rs | 32 +- src/input/state/core/text_editing.rs | 29 +- src/input/state/core/text_font.rs | 7 +- .../core/tool_controls/precision_entry.rs | 34 +- src/input/state/core/tool_controls/presets.rs | 8 +- .../state/core/tool_controls/settings.rs | 28 +- src/input/state/core/tool_controls/toolbar.rs | 132 ++-- src/input/state/core/toolbar/apply/actions.rs | 24 +- src/input/state/core/toolbar/apply/delays.rs | 4 +- src/input/state/core/toolbar/apply/layout.rs | 92 +-- src/input/state/core/toolbar/apply/mod.rs | 48 +- src/input/state/core/toolbar/apply/pages.rs | 3 +- src/input/state/core/toolbar/apply/tools.rs | 29 +- src/input/state/core/toolbar/state.rs | 25 +- .../state/core/utility/capture_feedback.rs | 17 +- src/input/state/core/utility/focus_mode.rs | 7 +- src/input/state/core/utility/frozen_zoom.rs | 4 +- src/input/state/core/utility/help_overlay.rs | 34 +- src/input/state/core/utility/interaction.rs | 20 +- src/input/state/core/utility/launcher.rs | 8 +- src/input/state/core/utility/light_mode.rs | 9 +- src/input/state/core/utility/ocr_scan.rs | 12 +- src/input/state/core/utility/pending.rs | 12 +- .../state/core/utility/presenter_mode.rs | 49 +- .../state/core/utility/restore_notice.rs | 15 +- src/input/state/core/utility/sequence.rs | 7 +- src/input/state/core/utility/toasts.rs | 10 +- src/input/state/core/zoom_chip.rs | 12 +- src/input/state/core/zoom_chip/state.rs | 4 +- src/input/state/from_config.rs | 61 +- src/input/state/highlight/settings.rs | 1 + src/input/state/highlight/state/mod.rs | 5 +- src/input/state/input_hud/label.rs | 89 +-- src/input/state/input_hud/state/mod.rs | 23 +- src/input/state/interaction/actions.rs | 4 +- .../state/interaction/adapters/actions.rs | 4 +- .../interaction/adapters/active_motion.rs | 8 +- .../state/interaction/adapters/keyboard.rs | 25 +- .../state/interaction/adapters/pointer.rs | 31 +- src/input/state/interaction/keyboard.rs | 10 +- src/input/state/interaction/mod.rs | 59 +- src/input/state/interaction/outcome.rs | 6 +- src/input/state/interaction/pointer.rs | 15 +- src/input/state/laser/mod.rs | 3 +- src/input/state/mod.rs | 55 +- src/input/state/mouse/motion.rs | 16 +- src/input/state/mouse/press.rs | 48 +- src/input/state/mouse/press/panels.rs | 13 +- src/input/state/mouse/press/polygon.rs | 22 +- src/input/state/mouse/release/drawing.rs | 31 +- src/input/state/mouse/release/mod.rs | 31 +- src/input/state/mouse/release/panels.rs | 8 +- src/input/state/mouse/release/selection.rs | 29 +- src/input/state/mouse/release/text.rs | 5 +- src/input/state/render.rs | 51 +- src/input/state/spotlight.rs | 48 +- src/input/state/spotlight/selection.rs | 8 +- src/input/state/tests/action_bindings.rs | 76 ++- src/input/state/tests/basics.rs | 95 ++- src/input/state/tests/board_picker.rs | 263 ++++---- src/input/state/tests/boards.rs | 40 +- src/input/state/tests/delete_restore.rs | 20 +- src/input/state/tests/drawing.rs | 96 +-- src/input/state/tests/erase.rs | 18 +- src/input/state/tests/focus_mode.rs | 83 +-- src/input/state/tests/helpers.rs | 13 +- src/input/state/tests/hit_testing.rs | 19 +- src/input/state/tests/input_hud.rs | 27 +- src/input/state/tests/laser.rs | 19 +- src/input/state/tests/light_mode.rs | 40 +- src/input/state/tests/menus/clear.rs | 4 +- src/input/state/tests/menus/clipboard.rs | 4 +- src/input/state/tests/menus/context_menu.rs | 68 +- src/input/state/tests/menus/history.rs | 22 +- src/input/state/tests/menus/submenu.rs | 24 +- src/input/state/tests/menus/zoomed.rs | 7 +- src/input/state/tests/mod.rs | 3 +- src/input/state/tests/modal.rs | 22 +- src/input/state/tests/pages.rs | 22 +- src/input/state/tests/presenter_mode.rs | 45 +- src/input/state/tests/pressure_modes.rs | 18 +- src/input/state/tests/properties_panel.rs | 63 +- .../state/tests/properties_panel_controls.rs | 39 +- src/input/state/tests/radial_menu.rs | 50 +- src/input/state/tests/selection/actions.rs | 7 +- src/input/state/tests/selection/damage.rs | 9 +- src/input/state/tests/selection/deletion.rs | 6 +- .../state/tests/selection/double_click.rs | 10 +- src/input/state/tests/selection/duplicate.rs | 58 +- .../state/tests/selection/mid_drag_actions.rs | 9 +- src/input/state/tests/selection/reorder.rs | 4 +- src/input/state/tests/shape_pen.rs | 27 +- src/input/state/tests/shape_pen_feedback.rs | 26 +- src/input/state/tests/spotlight.rs | 90 +-- src/input/state/tests/status_hud.rs | 59 +- .../tests/status_hud/engine_mutations.rs | 58 +- .../state/tests/text_edit/commit_cancel.rs | 10 +- src/input/state/tests/text_edit/resize.rs | 4 +- src/input/state/tests/text_input/actions.rs | 27 +- src/input/state/tests/text_input/editing.rs | 27 +- src/input/state/tests/text_input/idle.rs | 4 +- src/input/state/tests/text_input/ime.rs | 4 +- src/input/state/tests/text_input/placement.rs | 7 +- src/input/state/tests/text_input/text_mode.rs | 18 +- src/input/state/tests/tool_controls.rs | 146 ++--- src/input/state/tests/toolbar_display.rs | 159 +++-- src/input/state/tests/transform.rs | 51 +- src/input/state/tests/zoom_chip.rs | 32 +- src/input/state/text_resources.rs | 6 +- src/input/state/text_resources/tests.rs | 20 +- src/input/tablet/mod.rs | 32 +- src/input/tool/drawing.rs | 64 +- src/input/tool/drawing/polygon.rs | 18 +- src/input/tool/live_shape.rs | 18 +- src/input/tool/live_shape/ellipse.rs | 12 +- src/input/tool/live_shape/overlap.rs | 10 +- src/input/tool/live_shape/tests.rs | 38 +- src/input/tool/presets.rs | 6 + src/input/tool/settings.rs | 15 +- src/input/tool/tests.rs | 6 +- src/launch_environment.rs | 5 +- src/launch_environment/tests.rs | 14 +- src/lib.rs | 7 +- src/logger.rs | 169 +++-- src/logger/file.rs | 18 +- src/logger/panic.rs | 18 +- src/main.rs | 4 +- src/notification.rs | 27 +- src/ocr/controller.rs | 33 +- src/ocr/mod.rs | 25 +- src/ocr/tesseract.rs | 77 ++- src/onboarding.rs | 26 +- src/onboarding/tests.rs | 59 +- src/palette_recents.rs | 21 +- src/palette_recents/worker.rs | 18 +- src/paths/mod.rs | 24 +- src/paths/tests.rs | 233 ++++--- src/process_broker/bootstrap.rs | 43 +- src/process_broker/client.rs | 63 +- src/process_broker/error.rs | 11 +- src/process_broker/execution.rs | 71 +- src/process_broker/manifest.rs | 37 +- src/process_broker/server.rs | 50 +- src/process_broker/tests.rs | 218 +++---- src/process_broker/transport.rs | 32 +- src/process_broker/wire.rs | 6 +- src/render_profiles.rs | 15 +- src/runtime_capabilities.rs | 8 +- src/runtime_ui_state/controller.rs | 17 +- .../controller/construction.rs | 21 +- src/runtime_ui_state/controller/internals.rs | 19 +- src/runtime_ui_state/controller/lifecycle.rs | 11 +- src/runtime_ui_state/controller/mutations.rs | 14 +- src/runtime_ui_state/controller/resets.rs | 22 +- .../controller/source_mutation.rs | 28 +- src/runtime_ui_state/model.rs | 3 +- src/runtime_ui_state/pipeline.rs | 6 +- src/runtime_ui_state/pipeline/integration.rs | 17 +- src/runtime_ui_state/pipeline/queue.rs | 6 +- src/runtime_ui_state/pipeline/staging.rs | 21 +- src/runtime_ui_state/preview.rs | 18 +- src/runtime_ui_state/recovery.rs | 17 +- src/runtime_ui_state/recovery/attempt.rs | 39 +- src/runtime_ui_state/recovery/authority.rs | 24 +- src/runtime_ui_state/recovery/lifecycle.rs | 54 +- src/runtime_ui_state/recovery/preserve.rs | 11 +- src/runtime_ui_state/recovery/protocol.rs | 18 +- src/runtime_ui_state/seeds.rs | 15 +- src/runtime_ui_state/source_revision.rs | 14 +- src/runtime_ui_state/store.rs | 2 +- src/runtime_ui_state/store/fs.rs | 6 +- src/runtime_ui_state/store/inspection.rs | 38 +- src/runtime_ui_state/store/mutation.rs | 28 +- .../store/mutation/conflict.rs | 24 +- .../store/mutation/replace.rs | 32 +- src/runtime_ui_state/store/mutation/reset.rs | 25 +- src/runtime_ui_state/store/mutation/result.rs | 21 +- src/runtime_ui_state/store/tests.rs | 79 +-- src/runtime_ui_state/tests.rs | 4 +- .../tests/invalid_recovery.rs | 8 +- src/runtime_ui_state/tests/permits.rs | 48 +- src/runtime_ui_state/tests/recovery_basics.rs | 8 +- .../tests/recovery_cleanup.rs | 22 +- .../tests/recovery_conflicts.rs | 8 +- .../tests/recovery_lifecycle.rs | 2 +- src/runtime_ui_state/tests/reset_preview.rs | 16 +- src/runtime_ui_state/types.rs | 9 +- src/runtime_ui_state/wire.rs | 5 +- src/runtime_ui_state/wire/v1.rs | 13 +- src/runtime_ui_state/writer.rs | 36 +- src/runtime_ui_state/writer/tests.rs | 28 +- src/screen_pixels.rs | 13 +- src/session/artifacts.rs | 13 +- src/session/artifacts/corrupt_copies.rs | 15 +- src/session/artifacts/kind.rs | 1 + src/session/artifacts/move_file.rs | 14 +- src/session/artifacts/move_file/tests.rs | 106 ++- src/session/artifacts/save_as_quarantine.rs | 27 +- .../artifacts/save_as_quarantine/tests.rs | 30 +- src/session/artifacts/tests.rs | 82 +-- src/session/catalog.rs | 23 +- src/session/catalog/hooks.rs | 16 +- src/session/catalog/identity.rs | 10 +- src/session/catalog/tests.rs | 86 +-- src/session/options/config.rs | 1 + src/session/options/tests.rs | 56 +- src/session/options/types.rs | 12 + src/session/options/validation.rs | 11 +- src/session/primary.rs | 6 +- src/session/snapshot/appearance.rs | 3 +- src/session/snapshot/appearance/tests.rs | 34 +- src/session/snapshot/apply.rs | 21 +- src/session/snapshot/capture.rs | 4 +- src/session/snapshot/compat_tests.rs | 17 +- src/session/snapshot/compression.rs | 8 +- src/session/snapshot/generation/probe.rs | 10 +- src/session/snapshot/generation/tests.rs | 19 +- src/session/snapshot/generation/view.rs | 6 +- src/session/snapshot/load.rs | 43 +- src/session/snapshot/load/artifact_outcome.rs | 14 +- src/session/snapshot/load/corrupt.rs | 77 ++- src/session/snapshot/load/fallback.rs | 16 +- src/session/snapshot/load/markers.rs | 13 +- src/session/snapshot/load/named_candidate.rs | 19 +- src/session/snapshot/load/payload.rs | 19 +- src/session/snapshot/save.rs | 62 +- src/session/snapshot/save/estimate.rs | 10 +- src/session/snapshot/save/model.rs | 4 +- src/session/snapshot/save/payload.rs | 21 +- src/session/snapshot/save/recovery.rs | 42 +- src/session/snapshot/save/save_as.rs | 30 +- .../snapshot/save/save_as/transaction.rs | 4 +- .../save/save_as/transaction/tests.rs | 18 +- src/session/snapshot/save/tests.rs | 4 +- src/session/snapshot/tests.rs | 416 ++++++------ src/session/storage/clear.rs | 6 +- src/session/storage/inspect.rs | 10 +- src/session/storage/tests.rs | 125 ++-- src/session/storage/tool_state.rs | 4 +- src/session/tests/board_appearance.rs | 29 +- src/session/tests/helpers.rs | 3 +- src/session/tests/history.rs | 17 +- src/session/tests/limits.rs | 45 +- src/session/tests/options.rs | 20 +- src/session/tests/roundtrip.rs | 14 +- src/session/tests/snapshot.rs | 51 +- src/shortcut_hint.rs | 19 +- src/systemd_user_service.rs | 10 +- src/test_env.rs | 133 ++-- src/test_fake_helper.rs | 40 +- src/time_utils.rs | 5 +- src/toolbar_gtk/bridge.rs | 92 ++- src/toolbar_gtk/css.rs | 13 +- src/toolbar_gtk/icons.rs | 3 +- src/toolbar_gtk/mod.rs | 7 +- src/toolbar_gtk/runtime.rs | 19 +- src/toolbar_gtk/select.rs | 3 +- src/toolbar_gtk/view/capture_proof.rs | 12 +- src/toolbar_gtk/view/capture_suppression.rs | 58 +- .../capture_suppression/native_popovers.rs | 22 +- .../view/capture_suppression/quiescence.rs | 14 +- .../capture_suppression/tests/presentation.rs | 36 +- .../view/capture_suppression/tests/process.rs | 7 +- .../tests/wayland_proxy.rs | 19 +- .../tests/wayland_proxy/protocol.rs | 21 +- .../tests/wayland_proxy/relay.rs | 29 +- .../tests/wayland_proxy/wire.rs | 12 +- src/toolbar_gtk/view/drag.rs | 15 +- src/toolbar_gtk/view/mod.rs | 133 ++-- src/toolbar_gtk/view/sections/mod.rs | 6 +- src/toolbar_gtk/view/sections/session_pane.rs | 11 +- .../view/sections/settings_pane.rs | 8 +- src/toolbar_gtk/view/top_bar.rs | 93 +-- src/toolbar_gtk/view/top_bar/arrow_menu.rs | 24 +- src/toolbar_gtk/view/top_bar/controls.rs | 99 ++- src/toolbar_gtk/view/top_bar/drag.rs | 43 +- src/toolbar_gtk/view/top_bar/layout_menu.rs | 11 +- src/toolbar_gtk/view/top_bar/meter.rs | 23 +- src/toolbar_gtk/view/top_bar/pen_feel.rs | 28 +- src/toolbar_gtk/view/top_bar/popover_owner.rs | 5 +- src/toolbar_gtk/view/top_bar/popovers.rs | 34 +- src/toolbar_gtk/view/top_bar/stepper.rs | 21 +- src/toolbar_gtk/view/top_bar/strip.rs | 44 +- src/toolbar_gtk/view/top_bar/style_pill.rs | 35 +- src/toolbar_gtk/view/top_bar/tests.rs | 9 +- .../view/top_bar/tests/builtin_contract.rs | 54 +- .../view/top_bar/tests/builtin_style_nodes.rs | 22 +- .../view/top_bar/tests/capture_render.rs | 49 +- .../top_bar/tests/capture_render/lifecycle.rs | 17 +- .../top_bar/tests/capture_render/popups.rs | 27 +- .../tests/capture_render/subprocess.rs | 11 +- .../top_bar/tests/capture_render/support.rs | 35 +- .../view/top_bar/tests/expectations.rs | 8 +- .../view/top_bar/tests/gtk_contract.rs | 43 +- .../view/top_bar/tests/interactions.rs | 59 +- .../view/top_bar/tests/pane_popovers.rs | 39 +- .../view/top_bar/tests/pen_feel.rs | 39 +- .../view/top_bar/tests/pen_feel_render.rs | 36 +- .../view/top_bar/tests/structure.rs | 62 +- .../view/top_bar/tests/toolbar_popovers.rs | 52 +- .../view/top_bar/tests/widget_support.rs | 19 +- src/toolbar_gtk/widgets.rs | 56 +- src/toolbar_gtk/widgets/key_relay.rs | 21 +- src/toolbar_gtk/widgets/slider.rs | 108 ++-- src/toolbar_gtk/widgets/tests.rs | 37 +- src/toolbar_icons/actions.rs | 4 +- src/toolbar_icons/arrow_style_preview.rs | 5 +- src/toolbar_icons/meter_dot.rs | 6 +- src/toolbar_icons/mod.rs | 32 +- src/toolbar_icons/opacity_preview.rs | 8 +- src/toolbar_icons/smoothing_preview.rs | 15 +- src/toolbar_icons/svg.rs | 6 +- src/tray_action.rs | 26 +- src/ui.rs | 14 +- src/ui/anim.rs | 18 +- src/ui/arrow_bend_handle.rs | 4 +- src/ui/board_picker.rs | 26 +- src/ui/board_picker/appearance.rs | 17 +- src/ui/board_picker/helpers.rs | 3 +- src/ui/board_picker/page_panel.rs | 5 +- .../page_panel/thumbnail/cache.rs | 10 +- .../thumbnail/cache/spotlight_tests.rs | 4 +- .../page_panel/thumbnail/cache/tests.rs | 17 +- .../page_panel/thumbnail/cards.rs | 48 +- .../page_panel/thumbnail/content.rs | 125 ++-- .../page_panel/thumbnail/types.rs | 34 +- src/ui/board_picker/palette.rs | 3 +- src/ui/board_picker/rows.rs | 22 +- src/ui/board_picker/tests.rs | 44 +- src/ui/color_picker_popup.rs | 37 +- src/ui/color_picker_popup/controls.rs | 42 +- src/ui/color_picker_popup/tests/engine.rs | 12 +- src/ui/command_palette.rs | 14 +- src/ui/command_palette/command_palette_row.rs | 25 +- src/ui/command_palette/modals.rs | 18 +- src/ui/command_palette/tests/engine.rs | 63 +- src/ui/context_menu.rs | 12 +- src/ui/font_picker.rs | 18 +- src/ui/help_overlay/content.rs | 88 +-- src/ui/help_overlay/grid.rs | 22 +- src/ui/help_overlay/keycaps.rs | 12 +- src/ui/help_overlay/layout.rs | 4 +- src/ui/help_overlay/mod.rs | 2 +- src/ui/help_overlay/nav/render.rs | 8 +- src/ui/help_overlay/nav/state.rs | 9 +- src/ui/help_overlay/render/cache.rs | 9 +- src/ui/help_overlay/render/entry.rs | 45 +- src/ui/help_overlay/render/footer.rs | 26 +- src/ui/help_overlay/render/header.rs | 26 +- src/ui/help_overlay/render/mod.rs | 35 +- src/ui/help_overlay/render/state.rs | 6 +- src/ui/help_overlay/render/tests/cache.rs | 83 ++- src/ui/help_overlay/render/tests/layout.rs | 23 +- src/ui/help_overlay/search.rs | 6 +- src/ui/help_overlay/sections/bindings.rs | 13 +- src/ui/help_overlay/sections/builder.rs | 26 +- .../help_overlay/sections/builder/sections.rs | 15 +- src/ui/help_overlay/sections/mod.rs | 1 + src/ui/help_overlay/sections/tests.rs | 70 +- src/ui/help_overlay/types.rs | 11 +- src/ui/input_hud.rs | 4 +- src/ui/input_hud/tests.rs | 23 +- src/ui/ocr_scan.rs | 10 +- src/ui/onboarding_card.rs | 10 +- src/ui/onboarding_card/tests.rs | 4 +- src/ui/precision_entry.rs | 5 +- src/ui/precision_entry/tests.rs | 12 +- src/ui/primitives.rs | 7 +- src/ui/properties_panel.rs | 9 +- src/ui/properties_panel/actions.rs | 4 +- src/ui/properties_panel/controls.rs | 6 +- src/ui/radial_menu/cache.rs | 28 +- src/ui/radial_menu/cache/owner_tests.rs | 33 +- src/ui/radial_menu/mod.rs | 18 +- src/ui/region_action_bar/render.rs | 6 +- src/ui/region_capture_picker.rs | 10 +- src/ui/region_capture_picker/tests.rs | 22 +- src/ui/region_capture_picker/types.rs | 20 +- src/ui/render_context.rs | 8 +- src/ui/spotlight_control.rs | 12 +- src/ui/status/badges.rs | 12 +- src/ui/status/bar.rs | 32 +- src/ui/status/bar/content.rs | 33 +- src/ui/status/bar/helpers.rs | 6 +- src/ui/status/bar/measurement.rs | 22 +- src/ui/status/bar/render.rs | 15 +- src/ui/status/bar/tests.rs | 4 +- src/ui/status/bar/tests/width_budget.rs | 30 +- src/ui/status/mod.rs | 6 +- src/ui/status/tests.rs | 7 +- src/ui/status/zoom_chip.rs | 26 +- src/ui/tests/theme_compatibility.rs | 15 +- src/ui/text_highlight.rs | 4 +- src/ui/theme.rs | 42 +- src/ui/theme/css.rs | 7 + src/ui/theme/swatch.rs | 9 +- src/ui/toasts.rs | 10 +- src/ui/toasts/layout.rs | 8 +- src/ui/toasts/tests/engine.rs | 15 +- src/ui/toolbar/bindings.rs | 16 +- src/ui/toolbar/events.rs | 19 +- src/ui/toolbar/model/activation.rs | 7 +- src/ui/toolbar/model/control.rs | 3 +- src/ui/toolbar/model/event_policy.rs | 16 +- src/ui/toolbar/model/header.rs | 7 +- src/ui/toolbar/model/mod.rs | 43 +- src/ui/toolbar/model/settings.rs | 50 +- src/ui/toolbar/model/settings/helpers.rs | 12 +- src/ui/toolbar/model/style_pill.rs | 15 +- src/ui/toolbar/model/style_pill/arrow_menu.rs | 9 +- src/ui/toolbar/model/style_pill/control.rs | 26 +- src/ui/toolbar/model/style_pill/meter.rs | 12 +- src/ui/toolbar/model/style_pill/pen_feel.rs | 9 +- src/ui/toolbar/model/style_pill/slider.rs | 8 +- src/ui/toolbar/model/style_pill/stepper.rs | 11 +- .../model/style_pill/tests/selection.rs | 4 +- .../model/style_pill/tests/stroke_controls.rs | 8 +- .../model/style_pill/tests/tool_states.rs | 78 +-- src/ui/toolbar/model/top_spec.rs | 22 +- src/ui/toolbar/model/top_spec/control.rs | 22 +- src/ui/toolbar/model/top_spec/control_meta.rs | 29 +- src/ui/toolbar/model/top_spec/spec.rs | 12 +- .../toolbar/model/top_spec/tests/behavior.rs | 25 +- .../toolbar/model/top_spec/tests/structure.rs | 8 +- src/ui/toolbar/snapshot/build.rs | 49 +- src/ui/toolbar/snapshot/fade.rs | 5 + src/ui/toolbar/snapshot/selection_tests.rs | 14 +- src/ui/toolbar/snapshot/types.rs | 45 +- src/ui_text.rs | 12 +- src/ui_text/tests.rs | 2 +- src/unix_signals.rs | 37 +- src/unix_signals/listener.rs | 42 +- src/unix_signals/tests.rs | 23 +- src/update_check/cache.rs | 103 +-- src/update_check/fetch.rs | 13 +- src/update_check/mod.rs | 115 ++-- src/util/arrow.rs | 3 +- src/util/colors.rs | 8 +- src/util/geometry.rs | 6 + src/util/text.rs | 1 + src/zbus_stream.rs | 3 +- tests/AGENTS.md | 2 +- tests/cli.rs | 129 ++-- tests/daemon_v1_fixture.rs | 20 +- tests/i18n.rs | 6 +- tests/repository_guards/config_writers.rs | 64 +- tests/repository_guards/main.rs | 1 + tests/repository_guards/no_python.rs | 5 +- .../repository_guards/shared_dependencies.rs | 10 +- tests/repository_guards/source.rs | 43 +- tests/repository_guards/translations.rs | 7 +- tests/repository_guards/use_statements.rs | 146 +++++ tests/ui.rs | 116 ++-- 1262 files changed, 18863 insertions(+), 14849 deletions(-) create mode 100644 tests/repository_guards/use_statements.rs diff --git a/AGENTS.md b/AGENTS.md index 2909f17c6..d0f32d79f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,13 +30,24 @@ - Review changed functions after `cargo fmt`. Formatter success verifies mechanical layout; also verify visually that logical chunks are clearly separated and easy to scan. - Apply these rules to new and materially changed code. Do not reformat unrelated existing code solely to adopt them. +## Rust Style +- Rust code follows the [Google Rust style guide](https://google.github.io/styleguide/rustguide.html) as application code. Its rules that Clippy can check are workspace lints in `Cargo.toml`, configured in `clippy.toml`. Fix a warning rather than suppressing it. When a site truly cannot comply, use `#[expect(lint, reason = "...")]` with the concrete constraint, never `#[allow]`. +- Each `use` statement imports from one module: `use foo::bar::{Bar, Baz};`, not `use foo::{Foo, bar::Bar};`. `tests/repository_guards/use_statements.rs` checks this because rustfmt's `imports_granularity` is unstable. +- Do not use glob imports, except `use super::*` in test modules and third-party preludes. +- Import type and trait names and use them unqualified; call functions through their module (`fs::read`). Keep ambiguous names such as `io::Error` and `fmt::Result` module-qualified. A gated import carries the same `#[cfg(...)]` as the code that uses it. +- Borrow instead of cloning or taking ownership (`&str`, `&[T]`, `Option<&T>`). Replace more than three bool parameters with enums or an options struct. +- Use `expect` with a reason instead of `unwrap` in production code. Give assertions messages that explain the invariant, and document panics in `# Panics`. +- Never reach inside a poisoned lock. Use `expect` when the critical section cannot panic, or a fallback that leaves the data alone. Tests that serialize on process-wide state use `test_env::lock()` or a `test_env::Lease`. +- `catch_unwind` is reserved for the reviewed worker-isolation sites that carry an `#[expect]` reason. Tests observe panics through a joined scoped thread. +- Getters are `foo()`/`foo_mut()`, a fallible constructor is `try_new`, and type lifetimes have descriptive names. Every `unsafe` block has a `// SAFETY:` comment that says why it is sound. + ## Coupled Changes - Config changes often require `config.example.toml`, `docs/CONFIG.md`, configurator models/views, schema behavior, and tests. - Tool/action/keybinding changes often require config defaults, action metadata, help/command UI, toolbar UI, configurator labels/search, docs, and tests. - Daemon, service, shortcut, path, and packaging changes often need updates across `src/daemon/`, `src/paths/`, `src/systemd_user_service.rs`, `src/shortcut_hint.rs`, `configurator/src/app/daemon_setup/`, and `packaging/`. ## Validation -- Full local CI is `./tools/lint-and-test.sh`. It needs the .NET SDK selected by `global.json`. Without it, `cargo test` still runs the Rust source guards in `tests/repository_guards` (process sites, config writers, shared dependencies, no Python), but not the C# checks, including the source-coverage check that finds `.rs` files Cargo never compiles. +- Full local CI is `./tools/lint-and-test.sh`. It needs the .NET SDK selected by `global.json`. Without it, `cargo test` still runs the Rust source guards in `tests/repository_guards` (process sites, config writers, shared dependencies, use statements, no Python), but not the C# checks, including the source-coverage check that finds `.rs` files Cargo never compiles. - That script runs the C# repository checks (assets, version, nixpkgs recipe, source coverage, legacy tools), the C# tool tests (which also run the packaging shell contracts), `cargo fmt --all -- --check`, clippy with all targets/features, all-feature tests, and no-default-feature tests. - Run `git diff --check` before handoff. When relevant files are untracked and the index must stay unchanged, check those files directly as well. - For docs-only `AGENTS.md` edits, make new files visible to Git before whitespace checks, for example `rg --files --hidden -g AGENTS.md -0 | xargs -0 git add -N --` followed by `git diff --check`. diff --git a/Cargo.toml b/Cargo.toml index 9d5ec6fee..e93732de4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,24 @@ default-members = ["."] [workspace.lints.clippy] cognitive_complexity = "warn" +# Rules from the Google Rust style guide that Clippy can check. CI promotes +# these warnings to errors; clippy.toml holds their settings. +absolute_paths = "warn" +enum_glob_use = "warn" +fn_params_excessive_bools = "warn" +missing_assert_message = "warn" +missing_panics_doc = "warn" +must_use_candidate = "warn" +needless_for_each = "warn" +needless_pass_by_value = "warn" +redundant_clone = "warn" +ref_option = "warn" +return_self_not_must_use = "warn" +string_add = "warn" +undocumented_unsafe_blocks = "warn" +unnecessary_safety_comment = "warn" +unwrap_used = "warn" +wildcard_imports = "warn" [lints] workspace = true diff --git a/build.rs b/build.rs index 20bdd9f08..2af24e3f6 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,6 @@ -use std::env; -use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +use std::{env, fs}; fn main() { println!("cargo:rerun-if-env-changed=WAYSCRIBER_RELEASE_VERSION"); diff --git a/clippy.toml b/clippy.toml index c81a6d756..ffaa221b0 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,3 +1,16 @@ # Keep functions within the agreed cognitive-complexity budget. Clippy emits a # warning above this value, and CI promotes warnings to errors. cognitive-complexity-threshold = 20 + +# Tests may panic for assertions, so `unwrap_used` applies to production code. +allow-unwrap-in-tests = true + +# Never reach inside a poisoned lock, and do not rely on catching panics. +disallowed-methods = [ + { path = "std::panic::catch_unwind", reason = "panics are bugs; recover only as defense in depth with a reviewed `#[expect]`" }, + { path = "std::sync::PoisonError::get_mut", reason = "a poisoned lock may hold broken invariants" }, + { path = "std::sync::PoisonError::get_ref", reason = "a poisoned lock may hold broken invariants" }, + { path = "std::sync::PoisonError::into_inner", reason = "a poisoned lock may hold broken invariants" }, + { path = "std::sync::Mutex::clear_poison", reason = "a poisoned lock may hold broken invariants" }, + { path = "std::sync::RwLock::clear_poison", reason = "a poisoned lock may hold broken invariants" }, +] diff --git a/configurator/src/app/blocking_jobs.rs b/configurator/src/app/blocking_jobs.rs index 8db414734..505dfe078 100644 --- a/configurator/src/app/blocking_jobs.rs +++ b/configurator/src/app/blocking_jobs.rs @@ -1,9 +1,9 @@ use std::fmt; -use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::{Arc, LazyLock}; use std::time::{Duration, Instant}; use tokio::sync::Semaphore; +use tokio::task; const PRODUCTION_BLOCKING_JOB_LIMIT: usize = 2; const SLOW_JOB_THRESHOLD: Duration = Duration::from_millis(250); @@ -65,21 +65,21 @@ impl BlockingJobRunner { .map_err(|_| format!("{kind} blocking job queue is unavailable"))?; report_slow_phase(kind, "queue wait", queued_at.elapsed()); - let joined = tokio::task::spawn_blocking(move || { + let joined = task::spawn_blocking(move || { let _permit = permit; let started_at = Instant::now(); - let outcome = catch_unwind(AssertUnwindSafe(job)); + let result = job(); report_slow_phase(kind, "execution", started_at.elapsed()); - outcome + result }) .await; - let outcome = - joined.map_err(|err| format!("{kind} blocking job did not complete: {err}"))?; - - match outcome { + // A panicking job unwinds its blocking thread, which releases the + // permit; tokio reports the panic through the join error. + match joined { Ok(result) => result, - Err(_) => Err(format!("{kind} blocking job panicked")), + Err(err) if err.is_panic() => Err(format!("{kind} blocking job panicked")), + Err(err) => Err(format!("{kind} blocking job did not complete: {err}")), } } } @@ -106,6 +106,7 @@ mod tests { use std::sync::mpsc; use std::thread; + use tokio::sync::oneshot; use tokio::time::{Duration, timeout}; use super::*; @@ -134,7 +135,7 @@ mod tests { async fn blocking_work_is_offloaded_while_executor_work_progresses() { let runner = BlockingJobRunner::new(1); let caller_thread = thread::current().id(); - let (started_tx, started_rx) = tokio::sync::oneshot::channel(); + let (started_tx, started_rx) = oneshot::channel(); let (release_tx, release_rx) = mpsc::channel(); let job = tokio::spawn(async move { @@ -169,7 +170,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn one_permit_prevents_a_second_closure_from_starting() { let runner = BlockingJobRunner::new(1); - let (first_started_tx, first_started_rx) = tokio::sync::oneshot::channel(); + let (first_started_tx, first_started_rx) = oneshot::channel(); let (first_release_tx, first_release_rx) = mpsc::channel(); let first_runner = runner.clone(); let first = tokio::spawn(async move { @@ -186,7 +187,7 @@ mod tests { .expect("first closure should start") .expect("first closure should report startup"); - let (second_started_tx, mut second_started_rx) = tokio::sync::oneshot::channel(); + let (second_started_tx, mut second_started_rx) = oneshot::channel(); let second = tokio::spawn(async move { runner .run(BlockingJobKind::SessionCatalogLoad, move || { @@ -238,7 +239,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn dropping_the_waiter_does_not_release_a_running_jobs_permit() { let runner = BlockingJobRunner::new(1); - let (first_started_tx, first_started_rx) = tokio::sync::oneshot::channel(); + let (first_started_tx, first_started_rx) = oneshot::channel(); let (first_release_tx, first_release_rx) = mpsc::channel(); let first_runner = runner.clone(); let first = tokio::spawn(async move { @@ -268,7 +269,7 @@ mod tests { "the detached blocking closure must retain its permit" ); - let (second_started_tx, mut second_started_rx) = tokio::sync::oneshot::channel(); + let (second_started_tx, mut second_started_rx) = oneshot::channel(); let second = tokio::spawn(async move { runner .run(BlockingJobKind::SessionCatalogLoad, move || { diff --git a/configurator/src/app/component.rs b/configurator/src/app/component.rs index 11b193865..49c939eac 100644 --- a/configurator/src/app/component.rs +++ b/configurator/src/app/component.rs @@ -31,7 +31,7 @@ fn load_app_stylesheet() { // overlay crate's `v4_12` and then deprecates this method. #[allow(deprecated)] provider.load_from_data(include_str!("style.css")); - if let Some(display) = gtk::gdk::Display::default() { + if let Some(display) = Display::default() { gtk::style_context_add_provider_for_display( &display, &provider, @@ -46,6 +46,9 @@ fn load_app_stylesheet() { const APP_ID: &str = "org.wayscriber.Configurator"; use effects::spawn_effect; +use gtk::gdk::Display; +use gtk::gio::ApplicationFlags; +use wayscriber::domain::Language; pub(crate) fn run(startup: StartupRequest) { // Every launch is its own window, as it was under Iced: the overlay and @@ -56,7 +59,7 @@ pub(crate) fn run(startup: StartupRequest) { // safe: the guarded `ConfigDocument` save revision arbitrates writes. let application = adw::Application::builder() .application_id(APP_ID) - .flags(gtk::gio::ApplicationFlags::NON_UNIQUE) + .flags(ApplicationFlags::NON_UNIQUE) .build(); let app = RelmApp::from_app(application); // GTK must not parse our argv: `--open ` is ours and was @@ -92,7 +95,7 @@ pub(crate) struct AppWidgets { bindings: Vec, /// The language the pages were built in. When the active language moves /// away from it, the next refresh rebuilds them. - language: wayscriber::domain::Language, + language: Language, chrome: shell::Chrome, } diff --git a/configurator/src/app/component/effects.rs b/configurator/src/app/component/effects.rs index 3ab97af54..2891bdf27 100644 --- a/configurator/src/app/component/effects.rs +++ b/configurator/src/app/component/effects.rs @@ -1,4 +1,5 @@ use relm4::ComponentSender; +use relm4::adw::ApplicationWindow; use crate::messages::CommandMessage; @@ -11,7 +12,7 @@ use super::super::{daemon_setup, io, session_catalog}; pub(super) fn spawn_effect( effect: Effect, sender: &ComponentSender, - root: &relm4::adw::ApplicationWindow, + root: &ApplicationWindow, ) { use relm4::gtk::prelude::*; match effect { diff --git a/configurator/src/app/component/rebuild.rs b/configurator/src/app/component/rebuild.rs index c0b8d9f66..d0021f413 100644 --- a/configurator/src/app/component/rebuild.rs +++ b/configurator/src/app/component/rebuild.rs @@ -10,10 +10,14 @@ //! it, and how far the visible page is scrolled. This module carries those //! across. +use std::iter; + use relm4::{ComponentSender, gtk}; use gtk::glib; -use gtk::prelude::*; +use gtk::glib::GString; +use gtk::prelude::{GtkWindowExt, *}; +use wayscriber::i18n; use super::super::pages; use super::super::state::ConfiguratorApp; @@ -56,7 +60,7 @@ impl WidgetPath { } fn children(widget: >k::Widget) -> impl Iterator { - std::iter::successors(widget.first_child(), gtk::Widget::next_sibling) + iter::successors(widget.first_child(), gtk::Widget::next_sibling) } /// The caret and selection of an editable control, in characters. @@ -69,7 +73,7 @@ struct Caret { /// View state from the pages being replaced, applied to the new ones. pub(super) struct ViewState { /// The stack name of the page the state was read from. - page: Option, + page: Option, /// Set when focus was inside the visible page; focus elsewhere sits in /// persistent chrome, which the rebuild leaves alone. focus: Option<(Option, Option)>, @@ -84,7 +88,7 @@ pub(super) fn rebuild_if_language_changed( widgets: &mut AppWidgets, sender: &ComponentSender, ) -> Option { - let language = wayscriber::i18n::language(); + let language = i18n::language(); if widgets.language == language { return None; } @@ -123,7 +127,7 @@ fn capture(widgets: &AppWidgets) -> ViewState { }; }; - let focus = gtk::prelude::GtkWindowExt::focus(&widgets.root) + let focus = GtkWindowExt::focus(&widgets.root) .filter(|focused| focused.is_ancestor(&page)) .map(|focused| { let caret = focused @@ -157,7 +161,7 @@ fn capture(widgets: &AppWidgets) -> ViewState { fn descendants(widget: >k::Widget) -> impl Iterator { let mut pending = vec![widget.clone()]; - std::iter::from_fn(move || { + iter::from_fn(move || { let next = pending.pop()?; pending.extend(children(&next)); Some(next) diff --git a/configurator/src/app/component/shell.rs b/configurator/src/app/component/shell.rs index c4c1cedb0..21b89a939 100644 --- a/configurator/src/app/component/shell.rs +++ b/configurator/src/app/component/shell.rs @@ -2,7 +2,10 @@ use relm4::{ComponentSender, adw, gtk}; use adw::prelude::*; +use gtk::gdk::{Key, ModifierType}; +use gtk::glib::Propagation; use wayscriber::i18n::{tr, trf}; +use wayscriber::{build_info, i18n}; use crate::messages::Message; use crate::models::TabId; @@ -39,7 +42,7 @@ pub(super) fn build( let sender = sender.clone(); root.connect_close_request(move |_| { sender.input(Message::CloseRequested); - gtk::glib::Propagation::Stop + Propagation::Stop }); } // ---- Sidebar ---------------------------------------------------- @@ -267,13 +270,11 @@ pub(super) fn build( // lets widgets such as SearchEntry perform their own Escape behavior. controller.set_propagation_phase(gtk::PropagationPhase::Capture); controller.connect_key_pressed(move |_, key, _, modifiers| { - if modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK) - && matches!(key, gtk::gdk::Key::f | gtk::gdk::Key::F) - { + if modifiers.contains(ModifierType::CONTROL_MASK) && matches!(key, Key::f | Key::F) { sender.input(Message::SearchFocusRequested); - return gtk::glib::Propagation::Stop; + return Propagation::Stop; } - if key == gtk::gdk::Key::Escape { + if key == Key::Escape { // The recorder owns Escape while it is open so a recorded // Escape becomes a binding. Confirmation cancel is the // fallback when nothing is recording. @@ -281,10 +282,10 @@ pub(super) fn build( } // Tab is the user moving focus deliberately; a still-pending // startup search focus must not steal it back later. - if matches!(key, gtk::gdk::Key::Tab | gtk::gdk::Key::ISO_Left_Tab) { + if matches!(key, Key::Tab | Key::ISO_Left_Tab) { sender.input(Message::StartupInteractionObserved); } - gtk::glib::Propagation::Proceed + Propagation::Proceed }); root.add_controller(controller); } @@ -325,7 +326,7 @@ pub(super) fn build( search_entry, seen_focus_serial: 0, bindings, - language: wayscriber::i18n::language(), + language: i18n::language(), chrome: Chrome { build_identity, content_page, @@ -385,10 +386,7 @@ pub(super) fn retitle(widgets: &AppWidgets) { } chrome.build_identity.set_label(&trf( "Version {} · commit {}", - &[ - &wayscriber::build_info::version(), - &wayscriber::build_info::commit_hash(), - ], + &[&build_info::version(), &build_info::commit_hash()], )); widgets .window_title diff --git a/configurator/src/app/component/tests.rs b/configurator/src/app/component/tests.rs index 082f47e30..285349d7c 100644 --- a/configurator/src/app/component/tests.rs +++ b/configurator/src/app/component/tests.rs @@ -17,17 +17,23 @@ //! every supported language, whichever language is active. use std::path::Path; +use std::process::Command; use std::time::{Duration, Instant}; +use std::{env, fs, iter, thread}; use relm4::prelude::*; use relm4::{adw, gtk}; use adw::prelude::*; +use gtk::glib::MainContext; +use gtk::prelude::GtkWindowExt; use wayscriber::domain::Language; +use wayscriber::{env_vars, i18n}; use crate::messages::{CommandMessage, Message}; use crate::models::{KeybindingField, LanguageOption, StartupRequest, TabId, UiTabId}; +use crate::test_temp; use super::super::state::ConfiguratorApp; @@ -37,14 +43,14 @@ const CHILD_ENV: &str = "WAYSCRIBER_CONFIGURATOR_GTK_CHILD"; #[test] fn language_changes_rebuild_the_window_in_place() { // A window is allowed only on the dedicated test compositor. - if std::env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none() - || std::env::var("WAYLAND_DISPLAY").as_deref() != Ok("wayscriber-widget-tests") + if env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none() + || env::var("WAYLAND_DISPLAY").as_deref() != Ok("wayscriber-widget-tests") { eprintln!("skipping configurator GTK test: not on the private test compositor"); return; } - if std::env::var_os(CHILD_ENV).is_none() { + if env::var_os(CHILD_ENV).is_none() { run_in_child(); return; } @@ -57,8 +63,8 @@ fn language_changes_rebuild_the_window_in_place() { /// homes, so nothing reads or writes the user's files, no setup tool runs, /// and the catalogs load from the embedded copies. fn run_in_child() { - let homes = crate::test_temp::tempdir().expect("child homes"); - let mut command = std::process::Command::new(std::env::current_exe().expect("test binary")); + let homes = test_temp::tempdir().expect("child homes"); + let mut command = Command::new(env::current_exe().expect("test binary")); command .args([ concat!( @@ -72,21 +78,21 @@ fn run_in_child() { "--nocapture", ]) .env(CHILD_ENV, "1") - .env_remove(wayscriber::env_vars::LANGUAGE_OVERRIDE_ENV); + .env_remove(env_vars::LANGUAGE_OVERRIDE_ENV); for (key, directory) in [ ("XDG_CONFIG_HOME", "config"), ("XDG_DATA_HOME", "data"), ("XDG_STATE_HOME", "state"), ("XDG_CACHE_HOME", "cache"), - (wayscriber::env_vars::LOCALE_DIR_ENV, "locales"), + (env_vars::LOCALE_DIR_ENV, "locales"), // No setup tool (systemctl, gsettings) is reachable from here. ("PATH", "bin"), ] { let path = homes.path().join(directory); - std::fs::create_dir_all(&path).expect("child home"); + fs::create_dir_all(&path).expect("child home"); command.env(key, path); } - for key in wayscriber::env_vars::LOCALE_ENV_KEYS { + for key in env_vars::LOCALE_ENV_KEYS { command.env_remove(key); } write_config(&homes.path().join("config"), "[ui]\nlanguage = \"en\"\n"); @@ -97,8 +103,8 @@ fn run_in_child() { fn write_config(config_home: &Path, contents: &str) { let directory = config_home.join("wayscriber"); - std::fs::create_dir_all(&directory).expect("config directory"); - std::fs::write(directory.join("config.toml"), contents).expect("write config"); + fs::create_dir_all(&directory).expect("config directory"); + fs::write(directory.join("config.toml"), contents).expect("write config"); } fn run_scenario() { @@ -111,7 +117,7 @@ fn run_scenario() { !app.model().document.is_loading() }); settle(); - assert_eq!(wayscriber::i18n::language(), Language::English); + assert_eq!(i18n::language(), Language::English); assert_eq!(sidebar_label(&app, TabId::Drawing), "Drawing"); let pages = app.widgets().stack.observe_children().n_items(); let bindings = app.widgets().bindings.len(); @@ -199,7 +205,7 @@ fn service_buttons_describe_service_actions_in_each_language(app: &Controller = visible_page_descendants(app) .into_iter() .filter(|widget| { - std::iter::successors(Some(widget.clone()), gtk::Widget::parent) + iter::successors(Some(widget.clone()), gtk::Widget::parent) .take_while(|ancestor| *ancestor != page) .all(|ancestor| ancestor.is_visible()) }) @@ -430,7 +436,7 @@ fn a_load_that_changes_the_language_keeps_its_resets(app: &Controller) -> Option { - gtk::prelude::GtkWindowExt::focus(app.widget()) + GtkWindowExt::focus(app.widget()) } fn visible_page_descendants(app: &Controller) -> Vec { @@ -504,7 +509,7 @@ fn descendants(root: gtk::Widget) -> Vec { let mut found = Vec::new(); let mut pending = vec![root]; while let Some(widget) = pending.pop() { - pending.extend(std::iter::successors( + pending.extend(iter::successors( widget.first_child(), gtk::Widget::next_sibling, )); @@ -542,25 +547,25 @@ fn scrollable(app: &Controller) -> Option /// Runs the main loop until `done`, failing after a generous timeout. fn pump_until(what: &str, mut done: impl FnMut() -> bool) { - let context = gtk::glib::MainContext::default(); + let context = MainContext::default(); let deadline = Instant::now() + Duration::from_secs(30); while !done() { assert!(Instant::now() < deadline, "timed out waiting for {what}"); while context.pending() { context.iteration(false); } - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } } /// Lets queued messages, refreshes and a few frames run. fn settle() { - let context = gtk::glib::MainContext::default(); + let context = MainContext::default(); let until = Instant::now() + Duration::from_millis(300); while Instant::now() < until { while context.pending() { context.iteration(false); } - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } } diff --git a/configurator/src/app/component/view.rs b/configurator/src/app/component/view.rs index f9857eb18..cfbec76d7 100644 --- a/configurator/src/app/component/view.rs +++ b/configurator/src/app/component/view.rs @@ -2,6 +2,7 @@ use relm4::{ComponentSender, adw, gtk}; use adw::prelude::*; +use gtk::prelude::GtkWindowExt; use wayscriber::i18n::tr; use super::super::pages; @@ -78,7 +79,7 @@ fn write_widgets(app: &ConfiguratorApp, widgets: &mut AppWidgets) { let leave_pending = app.pending_leave().is_some(); let leave_was_pending = widgets.leave_actions.get_visible(); if leave_pending && !leave_was_pending { - widgets.leave_previous_focus = gtk::prelude::GtkWindowExt::focus(&widgets.root); + widgets.leave_previous_focus = GtkWindowExt::focus(&widgets.root); } set_visible(&widgets.leave_actions, leave_pending); if leave_pending && !leave_was_pending { diff --git a/configurator/src/app/daemon_setup/hyprland.rs b/configurator/src/app/daemon_setup/hyprland.rs index bdee5148e..e6a6213f2 100644 --- a/configurator/src/app/daemon_setup/hyprland.rs +++ b/configurator/src/app/daemon_setup/hyprland.rs @@ -1,4 +1,5 @@ use wayscriber::i18n::{tr, trf}; +use wayscriber::{durable_io, paths}; use std::fs; use std::io::ErrorKind; @@ -85,7 +86,7 @@ impl HyprlandLightControlsInstallResult { } pub(super) fn read_light_controls_status() -> HyprlandLightControlsStatus { - let Some(config_root) = wayscriber::paths::config_dir() else { + let Some(config_root) = paths::config_dir() else { return HyprlandLightControlsStatus { include_path: None, include_exists: false, @@ -111,7 +112,7 @@ fn read_light_controls_status_from_config_root(config_root: &Path) -> HyprlandLi } pub(super) fn install_light_controls() -> Result { - let config_root = wayscriber::paths::config_dir().ok_or_else(|| { + let config_root = paths::config_dir().ok_or_else(|| { tr("Cannot resolve config directory; failed to determine Hyprland config path.").to_string() })?; let binary_path = resolve_wayscriber_binary_path()?; @@ -151,7 +152,7 @@ fn write_light_controls( })?; let include_path = light_controls_include_path(config_root); - wayscriber::durable_io::write_text_atomic( + durable_io::write_text_atomic( &include_path, &render_light_controls(binary_path), AtomicWriteOptions::user_config_file(), @@ -174,7 +175,7 @@ fn write_light_controls( source_configured = true; source_updated = changed; if changed { - wayscriber::durable_io::write_text_atomic( + durable_io::write_text_atomic( &main_config_path, &updated_content, AtomicWriteOptions::user_config_file(), @@ -326,7 +327,7 @@ fn strip_matching_quotes(value: &str) -> &str { fn normalize_source_target(target: &str) -> PathBuf { let expanded = if let Some(stripped) = target.strip_prefix("~/") { - wayscriber::paths::home_dir() + paths::home_dir() .map(|home| home.join(stripped)) .unwrap_or_else(|| PathBuf::from(target)) } else { diff --git a/configurator/src/app/daemon_setup/hyprland/tests.rs b/configurator/src/app/daemon_setup/hyprland/tests.rs index 64f6c9f53..ca83dfd26 100644 --- a/configurator/src/app/daemon_setup/hyprland/tests.rs +++ b/configurator/src/app/daemon_setup/hyprland/tests.rs @@ -1,3 +1,6 @@ +use crate::test_env::EnvGuard; +use crate::test_temp; + use super::*; use wayscriber::env_vars::HOME_ENV; @@ -76,9 +79,9 @@ fn has_source_line_matches_quoted_and_inline_commented_targets() { #[test] fn has_source_line_matches_tilde_target() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let home = tmp.path(); - let _env = crate::test_env::EnvGuard::set(&[(HOME_ENV, home.as_os_str())]); + let _env = EnvGuard::set(&[(HOME_ENV, home.as_os_str())]); let absolute = home .join(".config") @@ -93,7 +96,7 @@ fn has_source_line_matches_tilde_target() { #[test] fn write_light_controls_writes_include_and_sources_existing_main() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let hypr_dir = tmp.path().join(HYPRLAND_DIR); fs::create_dir_all(&hypr_dir).unwrap(); let main = hypr_dir.join(MAIN_CONFIG); @@ -113,7 +116,7 @@ fn write_light_controls_writes_include_and_sources_existing_main() { #[test] fn write_light_controls_is_idempotent_for_existing_source() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let hypr_dir = tmp.path().join(HYPRLAND_DIR); fs::create_dir_all(&hypr_dir).unwrap(); let main = hypr_dir.join(MAIN_CONFIG); @@ -131,7 +134,7 @@ fn write_light_controls_is_idempotent_for_existing_source() { #[test] fn write_light_controls_handles_missing_main_config() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let result = write_light_controls(tmp.path(), Path::new("/tmp/wayscriber")).unwrap(); diff --git a/configurator/src/app/daemon_setup/mod.rs b/configurator/src/app/daemon_setup/mod.rs index 0bc38acf6..3bfa35701 100644 --- a/configurator/src/app/daemon_setup/mod.rs +++ b/configurator/src/app/daemon_setup/mod.rs @@ -7,7 +7,7 @@ use wayscriber::i18n::{tr, trf}; use crate::models::{ DaemonAction, DaemonActionResult, DaemonRuntimeStatus, DesktopEnvironment, - LightShortcutApplyCapability, ShortcutBackend, + LightShortcutApplyCapability, ShortcutApplyCapability, ShortcutBackend, }; use command::command_available; @@ -139,7 +139,7 @@ pub(super) fn load_daemon_runtime_status_sync() -> Result Result { })?; let contents = render_user_service_file(&binary_path); - wayscriber::durable_io::write_text_atomic( + durable_io::write_text_atomic( &service_path, &contents, AtomicWriteOptions::user_config_file(), diff --git a/configurator/src/app/daemon_setup/shortcut.rs b/configurator/src/app/daemon_setup/shortcut.rs index d91b903d1..4986a584d 100644 --- a/configurator/src/app/daemon_setup/shortcut.rs +++ b/configurator/src/app/daemon_setup/shortcut.rs @@ -1,6 +1,8 @@ +use wayscriber::durable_io; use wayscriber::i18n::{tr, trf}; use std::fs; +use std::path::PathBuf; use crate::models::{DesktopEnvironment, ShortcutApplyCapability, ShortcutBackend}; use wayscriber::durable_io::AtomicWriteOptions; @@ -176,7 +178,7 @@ fn run_gsettings_command(args: &[&str]) -> Result<(), String> { Ok(()) } -fn write_portal_shortcut_dropin(shortcut: &str) -> Result { +fn write_portal_shortcut_dropin(shortcut: &str) -> Result { let dropin_path = portal_shortcut_dropin_path().ok_or_else(|| { tr("Cannot resolve home directory; failed to determine systemd drop-in path.").to_string() })?; @@ -193,7 +195,7 @@ fn write_portal_shortcut_dropin(shortcut: &str) -> Result &'static str { #[cfg(test)] mod tests { + use wayscriber::i18n; + + use crate::test_env; + use super::*; use wayscriber::domain::Language; @@ -298,7 +302,7 @@ mod tests { #[test] fn feedback_renders_in_the_language_active_when_it_is_drawn() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::feedback_renders_in_the_language_active_when_it_is_drawn" @@ -313,7 +317,7 @@ mod tests { let detecting = DaemonFeedback::Status(StatusFeedback::Detecting); let english = (failed.text(), detecting.text()); - wayscriber::i18n::set_language(Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); assert_eq!( english.0, diff --git a/configurator/src/app/document_workflow.rs b/configurator/src/app/document_workflow.rs index 1c4b760a1..c71c905ff 100644 --- a/configurator/src/app/document_workflow.rs +++ b/configurator/src/app/document_workflow.rs @@ -1,4 +1,5 @@ //! Owns document transfer to effects and mutually exclusive load/save phases. +use std::mem; use std::path::PathBuf; use wayscriber::config::{ConfigDocument, ConfigValidationReport}; @@ -78,8 +79,7 @@ impl DocumentWorkflow { if !self.is_saving() { return None; } - let DocumentPhase::Saving(context) = - std::mem::replace(&mut self.phase, DocumentPhase::Idle) + let DocumentPhase::Saving(context) = mem::replace(&mut self.phase, DocumentPhase::Idle) else { return None; }; diff --git a/configurator/src/app/io.rs b/configurator/src/app/io.rs index 5242992d3..93e3af512 100644 --- a/configurator/src/app/io.rs +++ b/configurator/src/app/io.rs @@ -1,4 +1,5 @@ use wayscriber::config::{Config, ConfigDocument}; +use wayscriber::i18n; use crate::messages::ConfigSaveResult; @@ -10,7 +11,7 @@ pub(super) async fn load_config_from_disk() -> Result<(Box, Opti // Every catalog, whatever language the file names: the Language row // can preview any of them later, and reading a user override is file // work that must not happen on the GTK thread. - wayscriber::i18n::prepare_catalogs(); + i18n::prepare_catalogs(); ConfigDocument::load_for_editing() .map(|(document, warning)| (Box::new(document), warning)) .map_err(|err| format!("{err:#}")) diff --git a/configurator/src/app/pages/arrow.rs b/configurator/src/app/pages/arrow.rs index e91181a30..58bd326f7 100644 --- a/configurator/src/app/pages/arrow.rs +++ b/configurator/src/app/pages/arrow.rs @@ -45,10 +45,10 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { "Shape of the next arrow drawn. Every arrow keeps its own style, so \ changing this never restyles existing drawings.", ArrowStyleOption::list(), - ArrowStyleOption::list() + &ArrowStyleOption::list() .iter() .map(|option| option.label().to_string()) - .collect(), + .collect::>(), |app| app.draft.arrow_style, Message::ArrowStyleChanged, ); diff --git a/configurator/src/app/pages/boards.rs b/configurator/src/app/pages/boards.rs index 7df1b2935..0e526a4e1 100644 --- a/configurator/src/app/pages/boards.rs +++ b/configurator/src/app/pages/boards.rs @@ -34,6 +34,7 @@ use wayscriber::i18n::tr; use relm4::prelude::*; use relm4::{adw, gtk}; +use wayscriber::domain::BoardGridKind; use crate::messages::Message; use crate::models::{ @@ -238,7 +239,7 @@ struct SectionLayout { visible: bool, expanded: bool, background_kind: BoardBackgroundOption, - grid_kind: wayscriber::domain::BoardGridKind, + grid_kind: BoardGridKind, pen_enabled: bool, auto_adjust: bool, persist: bool, @@ -271,18 +272,18 @@ fn section_layouts(app: &ConfiguratorApp, summary: &AppSearchSummary) -> Vec { - id: &'a str, - name: &'a str, - grid_spacing: &'a str, - background: ColorValues<'a>, - pen: ColorValues<'a>, +struct BoardValues<'draft> { + id: &'draft str, + name: &'draft str, + grid_spacing: &'draft str, + background: ColorValues<'draft>, + pen: ColorValues<'draft>, } /// One color field's values: the picker's editing hex, and the draft's own /// triplet, so the swatch parses the components by the same rules the draft /// does. -struct ColorValues<'a> { - hex: &'a str, - color: &'a ColorTripletInput, +struct ColorValues<'draft> { + hex: &'draft str, + color: &'draft ColorTripletInput, } diff --git a/configurator/src/app/pages/boards/color.rs b/configurator/src/app/pages/boards/color.rs index 4d684b11f..5eed36435 100644 --- a/configurator/src/app/pages/boards/color.rs +++ b/configurator/src/app/pages/boards/color.rs @@ -1,5 +1,8 @@ +use std::array; + use relm4::{ComponentSender, gtk}; +use gtk::gdk::RGBA; use gtk::glib::SignalHandlerId; use gtk::prelude::*; @@ -44,7 +47,7 @@ impl ColorRow { } let rgb = parse_triplet_values(&values.color.components); - let rgba = gtk::gdk::RGBA::new(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32, 1.0); + let rgba = RGBA::new(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32, 1.0); set_swatch_blocked(&self.swatch, &self.swatch_handler, &rgba); } } @@ -88,7 +91,7 @@ pub(super) fn build_color_row( }; controls.append(&swatch); - let components = std::array::from_fn(|component| { + let components = array::from_fn(|component| { let entry = gtk::Entry::builder() .placeholder_text(COMPONENT_PLACEHOLDERS.get(component).copied().unwrap_or("")) .width_chars(6) diff --git a/configurator/src/app/pages/boards/grid.rs b/configurator/src/app/pages/boards/grid.rs index a9f222e87..282e9b861 100644 --- a/configurator/src/app/pages/boards/grid.rs +++ b/configurator/src/app/pages/boards/grid.rs @@ -1,9 +1,7 @@ use wayscriber::i18n::tr; -use super::{ - SectionLayout, - rows::{TextRow, build_text_row}, -}; +use super::SectionLayout; +use super::rows::{TextRow, build_text_row}; use crate::app::state::ConfiguratorApp; use crate::messages::Message; use crate::models::{BoardBackgroundOption, BoardItemTextField}; diff --git a/configurator/src/app/pages/capture.rs b/configurator/src/app/pages/capture.rs index f7a32f1dc..081c8a4e5 100644 --- a/configurator/src/app/pages/capture.rs +++ b/configurator/src/app/pages/capture.rs @@ -88,7 +88,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Selection frontend"), tr("Native draws the picker over a frozen desktop image. Slurp uses the external selector."), RegionPickerOption::list(), - labels(RegionPickerOption::list(), RegionPickerOption::label), + &labels(RegionPickerOption::list(), RegionPickerOption::label), |app| app.draft.capture.region_picker, Message::CaptureRegionPickerChanged, ) @@ -126,7 +126,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Page size"), "", PdfPageSizeOption::list(), - labels(PdfPageSizeOption::list(), PdfPageSizeOption::label), + &labels(PdfPageSizeOption::list(), PdfPageSizeOption::label), |app| app.draft.export_pdf_page_size, Message::ExportPdfPageSizeChanged, ) @@ -134,7 +134,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Orientation"), "", PdfOrientationOption::list(), - labels(PdfOrientationOption::list(), PdfOrientationOption::label), + &labels(PdfOrientationOption::list(), PdfOrientationOption::label), |app| app.draft.export_pdf_orientation, Message::ExportPdfOrientationChanged, ) @@ -142,7 +142,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Fit"), "", PdfFitModeOption::list(), - labels(PdfFitModeOption::list(), PdfFitModeOption::label), + &labels(PdfFitModeOption::list(), PdfFitModeOption::label), |app| app.draft.export_pdf_fit, Message::ExportPdfFitChanged, ) @@ -150,7 +150,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Transparent page background"), "", PdfTransparentBackgroundOption::list(), - labels( + &labels( PdfTransparentBackgroundOption::list(), PdfTransparentBackgroundOption::label, ), @@ -185,7 +185,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Label position"), "", PdfLabelPositionOption::list(), - labels( + &labels( PdfLabelPositionOption::list(), PdfLabelPositionOption::label, ), @@ -196,7 +196,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Label content"), "", PdfLabelContentModeOption::list(), - labels( + &labels( PdfLabelContentModeOption::list(), PdfLabelContentModeOption::label, ), diff --git a/configurator/src/app/pages/color_rows.rs b/configurator/src/app/pages/color_rows.rs index 31e64875a..cd6126a66 100644 --- a/configurator/src/app/pages/color_rows.rs +++ b/configurator/src/app/pages/color_rows.rs @@ -16,6 +16,7 @@ use relm4::{adw, gtk}; use adw::prelude::*; +use gtk::gdk::RGBA; use gtk::glib::SignalHandlerId; use crate::messages::Message; @@ -30,7 +31,7 @@ pub(crate) type ResolvedColor = Option<(f64, f64, f64, f64)>; /// Formats a dialog selection the way the hex handler expects: `#rrggbb`, /// with an alpha byte only when the color is actually translucent. -pub(crate) fn dialog_hex(rgba: >k::gdk::RGBA) -> String { +pub(crate) fn dialog_hex(rgba: &RGBA) -> String { let channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8; let (r, g, b, a) = ( channel(rgba.red()), @@ -47,7 +48,7 @@ pub(crate) fn dialog_hex(rgba: >k::gdk::RGBA) -> String { /// Formats a selection for a four-component field, retaining `FF` so choosing /// fully opaque replaces any translucent alpha already in the draft. -fn dialog_hex_with_alpha(rgba: >k::gdk::RGBA) -> String { +fn dialog_hex_with_alpha(rgba: &RGBA) -> String { let channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8; let (r, g, b, a) = ( channel(rgba.red()), @@ -86,7 +87,7 @@ pub(crate) fn mark_hex_error(widget: &impl IsA, text: &str) { pub(crate) fn set_swatch_blocked( button: >k::ColorDialogButton, handler: &SignalHandlerId, - rgba: >k::gdk::RGBA, + rgba: &RGBA, ) { if dialog_hex(&button.rgba()) == dialog_hex(rgba) { return; @@ -140,7 +141,7 @@ pub(crate) fn color_row( set_text_blocked(&row, &row_handler, &text); mark_hex_error(&row, &text); if let Some((r, g, b, a)) = resolved(app) { - let rgba = gtk::gdk::RGBA::new(r as f32, g as f32, b as f32, a as f32); + let rgba = RGBA::new(r as f32, g as f32, b as f32, a as f32); set_swatch_blocked(&dialog_button, &button_handler, &rgba); } }); @@ -153,7 +154,7 @@ mod tests { #[test] fn alpha_dialog_hex_keeps_an_explicit_opaque_byte() { - let rgba = gtk::gdk::RGBA::new(18.0 / 255.0, 52.0 / 255.0, 86.0 / 255.0, 1.0); + let rgba = RGBA::new(18.0 / 255.0, 52.0 / 255.0, 86.0 / 255.0, 1.0); assert_eq!(dialog_hex_with_alpha(&rgba), "#123456FF"); assert_eq!(dialog_hex(&rgba), "#123456"); diff --git a/configurator/src/app/pages/daemon/tests.rs b/configurator/src/app/pages/daemon/tests.rs index afbc317e0..7c8ee786a 100644 --- a/configurator/src/app/pages/daemon/tests.rs +++ b/configurator/src/app/pages/daemon/tests.rs @@ -1,3 +1,8 @@ +use wayscriber::domain::Language; +use wayscriber::i18n; + +use crate::test_env; + use super::*; use crate::models::{ DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability, @@ -122,14 +127,14 @@ fn overall_status_follows_service_state() { #[test] fn feedback_tone_follows_the_outcome_in_every_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::feedback_tone_follows_the_outcome_in_every_language" ), || { use crate::app::daemon_workflow::{ActionFeedback, DaemonFeedback, StatusFeedback}; - wayscriber::i18n::set_language(wayscriber::domain::Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); let failed = DaemonFeedback::Action(ActionFeedback::Failed("权限不足".into())); let loaded = DaemonFeedback::Status(StatusFeedback::Loaded); diff --git a/configurator/src/app/pages/drawing.rs b/configurator/src/app/pages/drawing.rs index 11b16bd2e..6ce8fe60e 100644 --- a/configurator/src/app/pages/drawing.rs +++ b/configurator/src/app/pages/drawing.rs @@ -92,13 +92,13 @@ fn section_combo_row( list: >k::ListBox, title: &str, values: Vec, - labels: Vec, + labels: &[String], get: impl Fn(&ConfiguratorApp) -> O + 'static, to_message: impl Fn(O) -> Message + 'static, ) where O: Copy + PartialEq + 'static, { - let row = combo_row_widget(title, &labels); + let row = combo_row_widget(title, labels); let handler = connect_combo(&row, page.sender(), values.clone(), to_message); list.append(&row); page.bind(move |app, _summary| { @@ -191,8 +191,8 @@ fn set_visible_if_changed(widget: &impl IsA, visible: bool) { } } -fn select_if_changed(row: &adw::ComboRow, values: &[O], current: O) { - if let Some(index) = values.iter().position(|value| *value == current) { +fn select_if_changed(row: &adw::ComboRow, values: &[O], current: &O) { + if let Some(index) = values.iter().position(|value| value == current) { let index = index as u32; if row.selected() != index { row.set_selected(index); diff --git a/configurator/src/app/pages/drawing/default_color.rs b/configurator/src/app/pages/drawing/default_color.rs index 6c15e4a7c..4a68a18c0 100644 --- a/configurator/src/app/pages/drawing/default_color.rs +++ b/configurator/src/app/pages/drawing/default_color.rs @@ -18,7 +18,7 @@ pub(super) fn build(page: &mut PageBuilder) { tr("Color mode"), tr("A palette name or hex string, or explicit RGB components."), COLOR_MODES.to_vec(), - vec![tr("Named color").to_string(), tr("RGB color").to_string()], + &[tr("Named color").to_string(), tr("RGB color").to_string()], |app| app.draft.drawing_color.mode, Message::ColorModeChanged, ); @@ -29,7 +29,7 @@ pub(super) fn build(page: &mut PageBuilder) { &named, tr("Named color"), NamedColorOption::list(), - named_color_labels(), + &named_color_labels(), |app| app.draft.drawing_color.selected_named, Message::NamedColorSelected, ); diff --git a/configurator/src/app/pages/drawing/defaults.rs b/configurator/src/app/pages/drawing/defaults.rs index 733e892c8..3fc263b54 100644 --- a/configurator/src/app/pages/drawing/defaults.rs +++ b/configurator/src/app/pages/drawing/defaults.rs @@ -1,3 +1,4 @@ +use wayscriber::config; use wayscriber::i18n::tr; use crate::messages::Message; @@ -37,10 +38,10 @@ pub(super) fn build(page: &mut PageBuilder) { tr("Eraser mode"), "", EraserModeOption::list(), - EraserModeOption::list() + &EraserModeOption::list() .iter() .map(|option| option.label().to_string()) - .collect(), + .collect::>(), |app| app.draft.drawing_default_eraser_mode, Message::EraserModeChanged, ) @@ -64,7 +65,7 @@ pub(super) fn build(page: &mut PageBuilder) { validate_usize_range( &app.draft.drawing_shape_recognition_sensitivity, 0, - usize::from(wayscriber::config::MAX_SHAPE_RECOGNITION_SENSITIVITY), + usize::from(config::MAX_SHAPE_RECOGNITION_SENSITIVITY), ) }, ) diff --git a/configurator/src/app/pages/drawing/drag_mapping.rs b/configurator/src/app/pages/drawing/drag_mapping.rs index c09f3ce0b..dd0f99ccc 100644 --- a/configurator/src/app/pages/drawing/drag_mapping.rs +++ b/configurator/src/app/pages/drawing/drag_mapping.rs @@ -86,7 +86,7 @@ fn build_drag_button_section(page: &mut PageBuilder, button: DragMouseButton) { &list, field.label(), tools.clone(), - tool_labels.clone(), + &tool_labels, move |app| drag_tool_for_field(drag_button_config(app, button), field), move |option| Message::DrawingMouseDragToolChanged(button, field, option), ); @@ -95,7 +95,7 @@ fn build_drag_button_section(page: &mut PageBuilder, button: DragMouseButton) { &list, &trf("{} color", &[&field.label()]), colors.clone(), - color_labels.clone(), + &color_labels, move |app| drag_color_for_field(drag_button_config(app, button), field), move |option| Message::DrawingMouseDragColorChanged(button, field, option), ); diff --git a/configurator/src/app/pages/drawing/font.rs b/configurator/src/app/pages/drawing/font.rs index f369d80f9..be7ac894e 100644 --- a/configurator/src/app/pages/drawing/font.rs +++ b/configurator/src/app/pages/drawing/font.rs @@ -5,6 +5,7 @@ use std::rc::Rc; use relm4::{ComponentSender, adw, gtk}; use adw::prelude::*; +use gtk::pango::{AttrFontDesc, AttrList, FontDescription}; use crate::messages::Message; use crate::models::{FontStyleOption, FontWeightOption, TextField}; @@ -27,10 +28,10 @@ pub(super) fn build(page: &mut PageBuilder) { tr("Font weight"), "", FontWeightOption::list(), - FontWeightOption::list() + &FontWeightOption::list() .iter() .map(|option| option.label().to_string()) - .collect(), + .collect::>(), |app| app.draft.drawing_font_weight_option, Message::FontWeightOptionSelected, ) @@ -43,10 +44,10 @@ pub(super) fn build(page: &mut PageBuilder) { tr("Font style"), "", FontStyleOption::list(), - FontStyleOption::list() + &FontStyleOption::list() .iter() .map(|option| option.label().to_string()) - .collect(), + .collect::>(), |app| app.draft.drawing_font_style_option, Message::FontStyleOptionSelected, ); @@ -130,7 +131,7 @@ fn build_font_cycle(page: &mut PageBuilder) { .map(|row| row.layout) .eq(layouts.iter().copied()) { - rows = rebuild_font_cycle(&list, &families, Rc::clone(&installed), &layouts, &sender); + rows = rebuild_font_cycle(&list, &families, &installed, &layouts, &sender); } for (family, row) in app .draft @@ -177,7 +178,7 @@ struct BoundFontRow { fn rebuild_font_cycle( list: >k::ListBox, families: >k::StringList, - family_names: Rc<[String]>, + family_names: &Rc<[String]>, layouts: &[FontCycleLayout], sender: &ComponentSender, ) -> Vec { @@ -189,7 +190,7 @@ fn rebuild_font_cycle( .iter() .map(|layout| { let (row, refresh) = - build_font_cycle_row(*layout, families, Rc::clone(&family_names), sender); + build_font_cycle_row(*layout, families, Rc::clone(family_names), sender); list.append(&row); BoundFontRow { layout: *layout, @@ -292,8 +293,8 @@ fn family_preview_factory() -> gtk::SignalListItemFactory { return; }; label.set_label(&family); - let attributes = gtk::pango::AttrList::new(); - attributes.insert(gtk::pango::AttrFontDesc::new(&family_preview_font(&family))); + let attributes = AttrList::new(); + attributes.insert(AttrFontDesc::new(&family_preview_font(&family))); label.set_attributes(Some(&attributes)); }); factory @@ -304,8 +305,8 @@ fn family_preview_factory() -> gtk::SignalListItemFactory { /// Built field by field: `FontDescription::from_string` reads trailing style, /// weight, and stretch words off the family, so "Roboto Condensed" would preview /// in Roboto and "Archivo Black" in Archivo. -fn family_preview_font(family: &str) -> gtk::pango::FontDescription { - let mut description = gtk::pango::FontDescription::new(); +fn family_preview_font(family: &str) -> FontDescription { + let mut description = FontDescription::new(); description.set_family(family); description } @@ -361,10 +362,13 @@ fn missing_family_note(family: &str) -> Option { #[cfg(test)] mod tests { + use gtk::pango::{Stretch, Weight}; + use wayscriber::draw; + use super::*; fn installed() -> String { - wayscriber::draw::system_font_families() + draw::system_font_families() .first() .expect("at least one family") .clone() @@ -403,8 +407,8 @@ mod tests { let description = family_preview_font(family); assert_eq!(description.family().as_deref(), Some(family)); - assert_eq!(description.weight(), gtk::pango::Weight::Normal); - assert_eq!(description.stretch(), gtk::pango::Stretch::Normal); + assert_eq!(description.weight(), Weight::Normal); + assert_eq!(description.stretch(), Stretch::Normal); } } diff --git a/configurator/src/app/pages/drawing/quick_colors.rs b/configurator/src/app/pages/drawing/quick_colors.rs index 6b65b4639..a747d9cb9 100644 --- a/configurator/src/app/pages/drawing/quick_colors.rs +++ b/configurator/src/app/pages/drawing/quick_colors.rs @@ -3,6 +3,7 @@ use wayscriber::i18n::{tr, trf}; use relm4::{ComponentSender, adw, gtk}; use adw::prelude::*; +use gtk::gdk::RGBA; use wayscriber::config::{QUICK_COLOR_RENDER_LIMIT, QuickColorSlot}; @@ -120,10 +121,10 @@ fn quick_color_layouts(app: &ConfiguratorApp) -> Vec { } /// One quick color row's values: everything the layout deliberately left out. -struct QuickColorValues<'a> { - label: &'a str, - name: &'a str, - hex: &'a str, +struct QuickColorValues<'draft> { + label: &'draft str, + name: &'draft str, + hex: &'draft str, named: NamedColorOption, /// The color the entry resolves to, `None` when it resolves to nothing — /// which is also what makes the name field an error. @@ -224,7 +225,7 @@ fn build_quick_color_row( tr("Color mode"), &[tr("Named or hex").to_string(), "RGB".to_string()], ); - select_if_changed(&mode, &COLOR_MODES, layout.mode); + select_if_changed(&mode, &COLOR_MODES, &layout.mode); connect_combo(&mode, sender.clone(), COLOR_MODES.to_vec(), move |value| { Message::QuickColorModeChanged(index, value) }); @@ -307,7 +308,7 @@ fn build_quick_color_row( } if let Some((r, g, b, a)) = values.preview { - let rgba = gtk::gdk::RGBA::new(r as f32, g as f32, b as f32, a as f32); + let rgba = RGBA::new(r as f32, g as f32, b as f32, a as f32); set_swatch_blocked(&swatch, &swatch_handler, &rgba); } }); diff --git a/configurator/src/app/pages/keybindings/mod.rs b/configurator/src/app/pages/keybindings/mod.rs index b62a0d4b0..75982658a 100644 --- a/configurator/src/app/pages/keybindings/mod.rs +++ b/configurator/src/app/pages/keybindings/mod.rs @@ -8,7 +8,7 @@ mod widgets; use wayscriber::i18n::tr; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::rc::Rc; use relm4::prelude::*; @@ -47,11 +47,11 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { "Record a shortcut or sequence, reset one action, or edit the comma-separated list as text. Filter and sort to review many actions at once.", )) .build(); - for field in fields + let tab_fields = fields .iter() .copied() - .filter(|field| keybinding_tab(*field) == tab) - { + .filter(|field| keybinding_tab(*field) == tab); + for field in tab_fields { let row = binding_row(&group, field, sender, &mut bindings, refresh.clone()); slots.push((field, row, group.clone())); } @@ -173,7 +173,7 @@ fn conflict_banner( .child(&row) .build(); - let jump_field = std::rc::Rc::new(std::cell::Cell::new(None::)); + let jump_field = Rc::new(Cell::new(None::)); { let sender = sender.clone(); let jump_field = jump_field.clone(); diff --git a/configurator/src/app/pages/keybindings/recorder.rs b/configurator/src/app/pages/keybindings/recorder.rs index 3af5455af..3549e0c43 100644 --- a/configurator/src/app/pages/keybindings/recorder.rs +++ b/configurator/src/app/pages/keybindings/recorder.rs @@ -6,6 +6,8 @@ use std::rc::Rc; use relm4::{ComponentSender, gtk}; +use gtk::gdk::{ButtonEvent, EventType, InputSource, Key, ModifierType}; +use gtk::glib::Propagation; use gtk::glib::object::IsA; use gtk::prelude::*; @@ -151,25 +153,24 @@ fn attach_key_controller(popover: >k::Popover, sender: &ComponentSender Ctrl+K > Ctrl+K from a hold. if !pressed.borrow_mut().insert(keycode) { - return gtk::glib::Propagation::Stop; + return Propagation::Stop; } let keyval = gdk_keyval(key); let recorded = KeyboardModifiers { - ctrl: modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK), - shift: modifiers.contains(gtk::gdk::ModifierType::SHIFT_MASK), - alt: modifiers.contains(gtk::gdk::ModifierType::ALT_MASK), - super_held: modifiers.contains(gtk::gdk::ModifierType::SUPER_MASK) - || modifiers.contains(gtk::gdk::ModifierType::META_MASK) - || modifiers.contains(gtk::gdk::ModifierType::HYPER_MASK), + ctrl: modifiers.contains(ModifierType::CONTROL_MASK), + shift: modifiers.contains(ModifierType::SHIFT_MASK), + alt: modifiers.contains(ModifierType::ALT_MASK), + super_held: modifiers.contains(ModifierType::SUPER_MASK) + || modifiers.contains(ModifierType::META_MASK) + || modifiers.contains(ModifierType::HYPER_MASK), }; sender.input(Message::ShortcutRecorderKey(keyval, recorded)); - gtk::glib::Propagation::Stop + Propagation::Stop }); } { @@ -215,30 +216,30 @@ fn attach_button_controller(popover: >k::Popover, sender: &ComponentSender() else { - return gtk::glib::Propagation::Proceed; + let Some(button_event) = event.downcast_ref::() else { + return Propagation::Proceed; }; let kind = event .device() .map(|device| match device.source() { - gtk::gdk::InputSource::Mouse - | gtk::gdk::InputSource::Touchpad - | gtk::gdk::InputSource::Trackpoint => RecorderDeviceKind::Mouse, - gtk::gdk::InputSource::Pen => RecorderDeviceKind::Pen, + InputSource::Mouse | InputSource::Touchpad | InputSource::Trackpoint => { + RecorderDeviceKind::Mouse + } + InputSource::Pen => RecorderDeviceKind::Pen, _ => RecorderDeviceKind::Other, }) .unwrap_or(RecorderDeviceKind::Other); let modifiers = event.modifier_state(); let recorded = KeyboardModifiers { - ctrl: modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK), - shift: modifiers.contains(gtk::gdk::ModifierType::SHIFT_MASK), - alt: modifiers.contains(gtk::gdk::ModifierType::ALT_MASK), - super_held: modifiers.contains(gtk::gdk::ModifierType::SUPER_MASK) - || modifiers.contains(gtk::gdk::ModifierType::META_MASK) - || modifiers.contains(gtk::gdk::ModifierType::HYPER_MASK), + ctrl: modifiers.contains(ModifierType::CONTROL_MASK), + shift: modifiers.contains(ModifierType::SHIFT_MASK), + alt: modifiers.contains(ModifierType::ALT_MASK), + super_held: modifiers.contains(ModifierType::SUPER_MASK) + || modifiers.contains(ModifierType::META_MASK) + || modifiers.contains(ModifierType::HYPER_MASK), }; // Primary clicks must reach Finish / Remove Last Step / Cancel. Stop // only bindable auxiliary mouse or stylus barrel events. @@ -249,15 +250,15 @@ fn attach_button_controller(popover: >k::Popover, sender: &ComponentSender gtk::glib::Propagation::Proceed, + RecordedDevice::Unsupported { .. } => Propagation::Proceed, } }); popover.add_controller(controller); } -fn gdk_keyval(key: gtk::gdk::Key) -> u32 { +fn gdk_keyval(key: Key) -> u32 { use gtk::glib::translate::IntoGlib; key.into_glib() } diff --git a/configurator/src/app/pages/keybindings/row.rs b/configurator/src/app/pages/keybindings/row.rs index f3ab19e97..80d39b651 100644 --- a/configurator/src/app/pages/keybindings/row.rs +++ b/configurator/src/app/pages/keybindings/row.rs @@ -1,12 +1,13 @@ use wayscriber::i18n::{tr, trf}; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::rc::Rc; use relm4::prelude::*; use relm4::{adw, gtk}; use adw::prelude::*; +use gtk::pango::EllipsizeMode; use wayscriber::config::Shortcut; use crate::messages::Message; @@ -82,7 +83,7 @@ pub(super) fn binding_row( let compact_summary = gtk::Label::builder() .css_classes(["dim-label"]) - .ellipsize(gtk::pango::EllipsizeMode::End) + .ellipsize(EllipsizeMode::End) .max_width_chars(24) .xalign(1.0) .build(); @@ -162,16 +163,11 @@ pub(super) fn binding_row( let recorder = RecorderPopover::build(&row, field, sender); let text_editor = TextEditorPopover::build(&row, field, sender); - let seen_chips = std::rc::Rc::new(std::cell::RefCell::new(Vec::::new())); - let seen_badges = std::rc::Rc::new(std::cell::RefCell::new(Vec::::new())); - let compact_mode = std::rc::Rc::new(std::cell::Cell::new(false)); - let last_focus_serial = std::rc::Rc::new(std::cell::Cell::new(0)); + let seen_chips = Rc::new(RefCell::new(Vec::::new())); + let seen_badges = Rc::new(RefCell::new(Vec::::new())); + let compact_mode = Rc::new(Cell::new(false)); + let last_focus_serial = Rc::new(Cell::new(0)); { - let compact = compact.clone(); - let inline_host = inline_host.clone(); - let compact_mode = compact_mode.clone(); - let editor_popover = editor_popover.clone(); - let editor = editor.clone(); watch_compact(&row, move |is_compact| { compact_mode.set(is_compact); set_visible(&compact, is_compact); diff --git a/configurator/src/app/pages/keybindings/text_editor.rs b/configurator/src/app/pages/keybindings/text_editor.rs index 16cecbf56..d5135942f 100644 --- a/configurator/src/app/pages/keybindings/text_editor.rs +++ b/configurator/src/app/pages/keybindings/text_editor.rs @@ -2,6 +2,7 @@ use wayscriber::i18n::tr; use relm4::{ComponentSender, gtk}; +use gtk::glib::SignalHandlerId; use gtk::glib::object::IsA; use gtk::prelude::*; @@ -15,7 +16,7 @@ use super::widgets::{field_canceled, set_accessible_label, set_label, unparent_o pub(super) struct TextEditorPopover { pub popover: gtk::Popover, entry: gtk::Entry, - entry_handler: gtk::glib::SignalHandlerId, + entry_handler: SignalHandlerId, error: gtk::Label, } diff --git a/configurator/src/app/pages/keybindings/toolbar.rs b/configurator/src/app/pages/keybindings/toolbar.rs index 236c0c481..8c5a21b19 100644 --- a/configurator/src/app/pages/keybindings/toolbar.rs +++ b/configurator/src/app/pages/keybindings/toolbar.rs @@ -2,7 +2,8 @@ use wayscriber::i18n::{tr, trf}; -use relm4::{gtk, prelude::*}; +use relm4::gtk; +use relm4::prelude::*; use gtk::prelude::*; @@ -138,7 +139,7 @@ fn actions_bar( row.append(&reset_all); row.append(&review); - let sort_for_bind = sort.clone(); + let sort_for_bind = sort; bindings.push(Box::new(move |app, _summary| { let selected = ShortcutManagerSort::ALL .iter() diff --git a/configurator/src/app/pages/keybindings/widgets.rs b/configurator/src/app/pages/keybindings/widgets.rs index e560e9877..d15af9c07 100644 --- a/configurator/src/app/pages/keybindings/widgets.rs +++ b/configurator/src/app/pages/keybindings/widgets.rs @@ -1,7 +1,8 @@ use relm4::{ComponentSender, gtk}; +use gtk::accessible::Property; use gtk::glib::object::IsA; -use gtk::prelude::*; +use gtk::prelude::{AccessibleExt, *}; use crate::messages::Message; use crate::models::KeybindingField; @@ -20,8 +21,8 @@ pub(super) fn icon_button(icon: &str, tooltip: &str) -> gtk::Button { button } -pub(super) fn set_accessible_label(widget: &impl gtk::prelude::AccessibleExt, label: &str) { - widget.update_property(&[gtk::accessible::Property::Label(label)]); +pub(super) fn set_accessible_label(widget: &impl AccessibleExt, label: &str) { + widget.update_property(&[Property::Label(label)]); } /// Writes the widget's own visibility flag, never `is_visible`: a widget diff --git a/configurator/src/app/pages/mod.rs b/configurator/src/app/pages/mod.rs index e90806696..59e16ff74 100644 --- a/configurator/src/app/pages/mod.rs +++ b/configurator/src/app/pages/mod.rs @@ -24,6 +24,8 @@ mod session; mod tablet; mod ui; +use std::cell::Cell; + use relm4::prelude::*; use relm4::{adw, gtk}; @@ -49,7 +51,7 @@ use super::state::ConfiguratorApp; pub(crate) type Binding = Box; thread_local! { - static REPLACING_PAGES: std::cell::Cell = const { std::cell::Cell::new(false) }; + static REPLACING_PAGES: Cell = const { Cell::new(false) }; } /// Marks the pages as being replaced for as long as it lives. @@ -76,7 +78,7 @@ impl Drop for ReplacingPages { /// Whether the pages are being replaced right now; see [`ReplacingPages`]. pub(crate) fn replacing_pages() -> bool { - REPLACING_PAGES.with(std::cell::Cell::get) + REPLACING_PAGES.with(Cell::get) } /// Writes editable text the model owns without reporting it as a user edit. @@ -349,7 +351,7 @@ impl PageBuilder { title: &str, subtitle: &str, values: Vec, - labels: Vec, + labels: &[String], get: impl Fn(&ConfiguratorApp) -> O + 'static, to_message: impl Fn(O) -> Message + 'static, ) -> &mut Self @@ -376,7 +378,7 @@ impl PageBuilder { where O: Copy + PartialEq + 'static, { - let row = self.add_combo_row(title, subtitle, values, labels, get, to_message); + let row = self.add_combo_row(title, subtitle, values, &labels, get, to_message); row.add_prefix(>k::Image::from_icon_name(icon_name)); self } @@ -386,7 +388,7 @@ impl PageBuilder { title: &str, subtitle: &str, values: Vec, - labels: Vec, + labels: &[String], get: impl Fn(&ConfiguratorApp) -> O + 'static, to_message: impl Fn(O) -> Message + 'static, ) -> adw::ComboRow diff --git a/configurator/src/app/pages/performance.rs b/configurator/src/app/pages/performance.rs index eeb0478f4..0b8556055 100644 --- a/configurator/src/app/pages/performance.rs +++ b/configurator/src/app/pages/performance.rs @@ -41,7 +41,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { buffer_metadata.label, buffer_metadata.help, buffer_counts, - buffer_labels, + &buffer_labels, |app| app.draft.performance_buffer_count, Message::BufferCountChanged, ) diff --git a/configurator/src/app/pages/presets.rs b/configurator/src/app/pages/presets.rs index 040442b19..c6eea3823 100644 --- a/configurator/src/app/pages/presets.rs +++ b/configurator/src/app/pages/presets.rs @@ -42,7 +42,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Visible slots"), tr("How many preset slots the toolbar offers."), counts, - count_labels, + &count_labels, |app| app.draft.presets.slot_count, Message::PresetSlotCountChanged, ); @@ -187,7 +187,7 @@ fn build_slot(page: &mut PageBuilder, slot: usize) { rows.combo_row( tr("Tool"), tools, - tool_labels, + &tool_labels, move |app| app.draft.presets.slot(slot).map(|draft| draft.tool), Message::PresetToolChanged, ); @@ -215,7 +215,7 @@ fn build_slot(page: &mut PageBuilder, slot: usize) { rows.combo_row( tr("Eraser kind"), kinds, - kind_labels, + &kind_labels, move |app| app.draft.presets.slot(slot).map(|draft| draft.eraser_kind), Message::PresetEraserKindChanged, ); @@ -224,7 +224,7 @@ fn build_slot(page: &mut PageBuilder, slot: usize) { rows.combo_row( tr("Eraser mode"), modes, - mode_labels, + &mode_labels, move |app| app.draft.presets.slot(slot).map(|draft| draft.eraser_mode), Message::PresetEraserModeChanged, ); diff --git a/configurator/src/app/pages/presets/color.rs b/configurator/src/app/pages/presets/color.rs index e68e6a9f8..659c87832 100644 --- a/configurator/src/app/pages/presets/color.rs +++ b/configurator/src/app/pages/presets/color.rs @@ -14,7 +14,7 @@ pub(super) fn build_color_rows(rows: &mut SlotBuilder<'_>, slot: usize) { let mode_row = rows.combo_row( tr("Color mode"), vec![ColorMode::Named, ColorMode::Rgb], - vec![tr("Named").to_string(), "RGB".to_string()], + &[tr("Named").to_string(), "RGB".to_string()], move |app| app.draft.presets.slot(slot).map(|draft| draft.color.mode), Message::PresetColorModeChanged, ); @@ -52,7 +52,7 @@ pub(super) fn build_color_rows(rows: &mut SlotBuilder<'_>, slot: usize) { let named_row = rows.combo_row( tr("Named color"), named, - named_labels, + &named_labels, move |app| { app.draft .presets diff --git a/configurator/src/app/pages/presets/rows.rs b/configurator/src/app/pages/presets/rows.rs index 0f6a66009..277cb36cf 100644 --- a/configurator/src/app/pages/presets/rows.rs +++ b/configurator/src/app/pages/presets/rows.rs @@ -14,8 +14,8 @@ use super::super::{PageBuilder, set_selected_blocked, set_text_blocked}; /// Mirrors the [`PageBuilder`] row helpers, but adds rows to the slot's /// expander and reads the slot out of the model, so a slot the draft does /// not hold simply leaves its rows alone. -pub(super) struct SlotBuilder<'a> { - pub(super) page: &'a mut PageBuilder, +pub(super) struct SlotBuilder<'page> { + pub(super) page: &'page mut PageBuilder, pub(super) expander: adw::ExpanderRow, pub(super) slot: usize, } @@ -69,7 +69,7 @@ impl SlotBuilder<'_> { &mut self, title: &str, values: Vec, - labels: Vec, + labels: &[String], get: impl Fn(&ConfiguratorApp) -> Option + 'static, to_message: impl Fn(usize, O) -> Message + 'static, ) -> adw::ComboRow @@ -117,7 +117,7 @@ impl SlotBuilder<'_> { ) -> adw::ComboRow { let values = OverrideOption::list(); let labels = labels_of(&values, OverrideOption::label); - self.combo_row(title, values, labels, get, move |slot, value| { + self.combo_row(title, values, &labels, get, move |slot, value| { Message::PresetToggleOptionChanged(slot, field, value) }) } diff --git a/configurator/src/app/pages/render_profiles.rs b/configurator/src/app/pages/render_profiles.rs index 7d3c5d2b6..82a0b161e 100644 --- a/configurator/src/app/pages/render_profiles.rs +++ b/configurator/src/app/pages/render_profiles.rs @@ -79,7 +79,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Canvas export profile"), "", export_options, - export_labels, + &export_labels, |app| app.draft.render_profiles.export, Message::RenderProfileExportChanged, ); @@ -279,15 +279,15 @@ fn section_layouts(app: &ConfiguratorApp, summary: &AppSearchSummary) -> Vec { - id: &'a str, - name: &'a str, +struct ProfileValues<'draft> { + id: &'draft str, + name: &'draft str, /// One entry per mapping row, in mapping order. - mappings: Vec>, + mappings: Vec>, } /// One mapping row's two editing hex fields. -struct MappingValues<'a> { - from: &'a str, - to: &'a str, +struct MappingValues<'draft> { + from: &'draft str, + to: &'draft str, } diff --git a/configurator/src/app/pages/render_profiles/mapping.rs b/configurator/src/app/pages/render_profiles/mapping.rs index 391f3edfe..3b8ba92c6 100644 --- a/configurator/src/app/pages/render_profiles/mapping.rs +++ b/configurator/src/app/pages/render_profiles/mapping.rs @@ -3,6 +3,7 @@ use wayscriber::i18n::tr; use super::rows::{connect_button, plain_row, row_content_box, side_label}; use relm4::{ComponentSender, gtk}; +use gtk::gdk::RGBA; use gtk::glib::SignalHandlerId; use gtk::prelude::*; @@ -35,7 +36,7 @@ impl ColorField { // rather than flashing it to black on every keystroke. return; }; - let rgba = gtk::gdk::RGBA::new(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32, 1.0); + let rgba = RGBA::new(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32, 1.0); set_swatch_blocked(&self.swatch, &self.swatch_handler, &rgba); } } diff --git a/configurator/src/app/pages/session/catalog/card.rs b/configurator/src/app/pages/session/catalog/card.rs index 2bed5c850..7b5459d87 100644 --- a/configurator/src/app/pages/session/catalog/card.rs +++ b/configurator/src/app/pages/session/catalog/card.rs @@ -208,12 +208,12 @@ fn item_card( let clear_was_armed = confirm.get_visible(); let answer_has_focus = confirm_button.has_focus() || cancel_button.has_focus(); - let focus_after_refresh = clear_focus_after_refresh( - clear_was_armed, - values.clear_armed, - values.clear_enabled, + let focus_after_refresh = clear_focus_after_refresh(ClearConfirmTransition { + was_armed: clear_was_armed, + is_armed: values.clear_armed, + clear_enabled: values.clear_enabled, answer_has_focus, - ); + }); if focus_after_refresh == ClearFocusTarget::Catalog { // Confirm enters the busy state and disables the row's actions. // Temporarily make the stable page target focusable and move there @@ -242,10 +242,10 @@ fn item_card( CatalogRow { card, refresh } } -struct InputRow<'a> { - id: &'a str, - placeholder: &'a str, - button_label: &'a str, +struct InputRow<'text> { + id: &'text str, + placeholder: &'text str, + button_label: &'text str, on_input: fn(String, String) -> Message, request: Message, } @@ -293,12 +293,27 @@ enum ClearFocusTarget { Catalog, } -fn clear_focus_after_refresh( +/// The clear confirmation's state across one refresh. +#[derive(Debug, Clone, Copy, Default)] +struct ClearConfirmTransition { + /// The confirmation was showing before this refresh. was_armed: bool, + /// The confirmation shows after this refresh. is_armed: bool, + /// The clear action stays enabled after this refresh. clear_enabled: bool, + /// Keyboard focus is on the confirm or cancel answer. answer_has_focus: bool, -) -> ClearFocusTarget { +} + +fn clear_focus_after_refresh(transition: ClearConfirmTransition) -> ClearFocusTarget { + let ClearConfirmTransition { + was_armed, + is_armed, + clear_enabled, + answer_has_focus, + } = transition; + if is_armed && !was_armed { return ClearFocusTarget::Confirm; } @@ -319,22 +334,38 @@ mod tests { #[test] fn clear_confirmation_focus_has_enabled_targets_for_every_transition() { assert_eq!( - clear_focus_after_refresh(false, true, true, false), + clear_focus_after_refresh(ClearConfirmTransition { + is_armed: true, + clear_enabled: true, + ..ClearConfirmTransition::default() + }), ClearFocusTarget::Confirm, "arming focuses the newly revealed confirmation" ); assert_eq!( - clear_focus_after_refresh(true, false, true, true), + clear_focus_after_refresh(ClearConfirmTransition { + was_armed: true, + clear_enabled: true, + answer_has_focus: true, + ..ClearConfirmTransition::default() + }), ClearFocusTarget::ClearAction, "cancel returns to the restored clear action" ); assert_eq!( - clear_focus_after_refresh(true, false, false, true), + clear_focus_after_refresh(ClearConfirmTransition { + was_armed: true, + answer_has_focus: true, + ..ClearConfirmTransition::default() + }), ClearFocusTarget::Catalog, "confirm moves away from the row actions disabled by busy state" ); assert_eq!( - clear_focus_after_refresh(true, false, false, false), + clear_focus_after_refresh(ClearConfirmTransition { + was_armed: true, + ..ClearConfirmTransition::default() + }), ClearFocusTarget::Unchanged, "a pointer-triggered transition does not steal keyboard focus" ); diff --git a/configurator/src/app/pages/session/catalog/tests.rs b/configurator/src/app/pages/session/catalog/tests.rs index f44f26cac..4c0fc27e7 100644 --- a/configurator/src/app/pages/session/catalog/tests.rs +++ b/configurator/src/app/pages/session/catalog/tests.rs @@ -1,5 +1,9 @@ use std::path::PathBuf; +use wayscriber::i18n; + +use crate::test_env; + use super::*; use crate::app::state::PendingConfirmation; use crate::models::session::SessionArtifactSummary; @@ -182,7 +186,7 @@ fn catalog_focus_fallback_leaves_the_tab_chain_when_busy_work_finishes() { /// chosen when the layout is drawn, and a new language rebuilds the card. #[test] fn card_labels_follow_the_language_the_list_is_drawn_in() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::card_labels_follow_the_language_the_list_is_drawn_in" @@ -193,7 +197,7 @@ fn card_labels_follow_the_language_the_list_is_drawn_in() { let english = catalog_layout(&app); assert_eq!(english.items[0].last_opened_label, "Never"); - wayscriber::i18n::set_language(Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); let chinese = catalog_layout(&app); assert_eq!(chinese.items[0].last_opened_label, "从不"); diff --git a/configurator/src/app/pages/session/settings.rs b/configurator/src/app/pages/session/settings.rs index ba47de1ba..89a6bb4e1 100644 --- a/configurator/src/app/pages/session/settings.rs +++ b/configurator/src/app/pages/session/settings.rs @@ -56,7 +56,7 @@ pub(super) fn add(page: &mut PageBuilder) { tr("Storage mode"), "", SessionStorageModeOption::list(), - option_labels(SessionStorageModeOption::list(), |option| option.label()), + &option_labels(&SessionStorageModeOption::list(), |option| option.label()), |app| app.draft.session_storage_mode, Message::SessionStorageModeChanged, ); @@ -65,7 +65,7 @@ pub(super) fn add(page: &mut PageBuilder) { tr("Compression"), "", SessionCompressionOption::list(), - option_labels(SessionCompressionOption::list(), |option| option.label()), + &option_labels(&SessionCompressionOption::list(), |option| option.label()), |app| app.draft.session_compression, Message::SessionCompressionChanged, ) @@ -131,7 +131,7 @@ pub(super) fn add(page: &mut PageBuilder) { ); } -fn option_labels(options: Vec, label: impl Fn(&O) -> &'static str) -> Vec { +fn option_labels(options: &[O], label: impl Fn(&O) -> &'static str) -> Vec { options .iter() .map(|option| label(option).to_string()) diff --git a/configurator/src/app/pages/tablet.rs b/configurator/src/app/pages/tablet.rs index bcb8693cc..daa1b8772 100644 --- a/configurator/src/app/pages/tablet.rs +++ b/configurator/src/app/pages/tablet.rs @@ -20,12 +20,12 @@ use super::{BuiltPage, PageBuilder}; pub(super) fn build(sender: &ComponentSender) -> BuiltPage { let edit_modes = PressureThicknessEditModeOption::list(); - let edit_mode_labels = edit_modes + let edit_mode_labels: Vec = edit_modes .iter() .map(|mode| mode.label().to_string()) .collect(); let entry_modes = PressureThicknessEntryModeOption::list(); - let entry_mode_labels = entry_modes + let entry_mode_labels: Vec = entry_modes .iter() .map(|mode| mode.label().to_string()) .collect(); @@ -82,7 +82,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Pressure thickness edit mode"), "", edit_modes, - edit_mode_labels, + &edit_mode_labels, |app| app.draft.tablet_pressure_thickness_edit_mode, Message::TabletPressureEditModeChanged, ) @@ -90,7 +90,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Pressure thickness entry mode"), "", entry_modes, - entry_mode_labels, + &entry_mode_labels, |app| app.draft.tablet_pressure_thickness_entry_mode, Message::TabletPressureEntryModeChanged, ); diff --git a/configurator/src/app/pages/tests.rs b/configurator/src/app/pages/tests.rs index 1671d5ae2..90c32af68 100644 --- a/configurator/src/app/pages/tests.rs +++ b/configurator/src/app/pages/tests.rs @@ -1,4 +1,7 @@ use wayscriber::domain::Language; +use wayscriber::i18n; + +use crate::test_env; use super::validate_u32_range; @@ -22,10 +25,10 @@ fn an_invalid_whole_number_names_the_actual_bounds() { #[test] fn the_bounds_survive_translation() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!(module_path!(), "::the_bounds_survive_translation"), || { - wayscriber::i18n::set_language(Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); assert_eq!( validate_u32_range("256", 0, 255).as_deref(), Some("请输入 0 到 255 之间的整数。") diff --git a/configurator/src/app/pages/ui/general.rs b/configurator/src/app/pages/ui/general.rs index 8e5f72031..17de7e1b7 100644 --- a/configurator/src/app/pages/ui/general.rs +++ b/configurator/src/app/pages/ui/general.rs @@ -32,7 +32,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Theme"), tr("\"Auto\" uses dark. \"Light\" supports the command palette, including shortcut editing and tooltips; other popups still use dark."), themes, - theme_labels, + &theme_labels, |app| app.draft.ui_theme, Message::UiThemeChanged, ) @@ -40,7 +40,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Reduced motion"), tr("\"On\" disables UI animations. \"Auto\" follows the system preference in a future release and keeps full motion for now."), motions, - motion_labels, + &motion_labels, |app| app.draft.ui_reduced_motion, Message::UiReducedMotionChanged, ) diff --git a/configurator/src/app/pages/ui/input_hud.rs b/configurator/src/app/pages/ui/input_hud.rs index 82ae85a49..74a9e7e9c 100644 --- a/configurator/src/app/pages/ui/input_hud.rs +++ b/configurator/src/app/pages/ui/input_hud.rs @@ -30,7 +30,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Input source"), tr("\"Overlay only\" shows what Wayscriber itself receives. \"System-wide\" also shows input that goes to other apps; it needs a build with the input-monitor feature and read access to /dev/input (usually `input` group membership), and it sees every keystroke on the seat - including passwords typed elsewhere."), modes, - mode_labels, + &mode_labels, |app| app.draft.input_hud_mode, Message::InputHudModeChanged, ) @@ -38,7 +38,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Screen position"), "", positions, - position_labels, + &position_labels, |app| app.draft.input_hud_position, Message::InputHudPositionChanged, ) diff --git a/configurator/src/app/pages/ui/presenter_mode.rs b/configurator/src/app/pages/ui/presenter_mode.rs index 5d8d1220e..5ba9a9187 100644 --- a/configurator/src/app/pages/ui/presenter_mode.rs +++ b/configurator/src/app/pages/ui/presenter_mode.rs @@ -37,7 +37,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Top toolbar while presenting"), "", toolbar_modes, - toolbar_mode_labels, + &toolbar_mode_labels, |app| app.draft.presenter_toolbar_mode, Message::PresenterToolbarModeChanged, ) @@ -69,7 +69,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Tool behavior"), "", behaviors, - behavior_labels, + &behavior_labels, |app| app.draft.presenter_tool_behavior, Message::PresenterToolBehaviorChanged, ) diff --git a/configurator/src/app/pages/ui/status_bar.rs b/configurator/src/app/pages/ui/status_bar.rs index 918e5ac07..2a6e424ea 100644 --- a/configurator/src/app/pages/ui/status_bar.rs +++ b/configurator/src/app/pages/ui/status_bar.rs @@ -126,7 +126,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Status bar position"), "", positions, - position_labels, + &position_labels, |app| app.draft.ui_status_position, Message::StatusPositionChanged, ); diff --git a/configurator/src/app/pages/ui/toolbar.rs b/configurator/src/app/pages/ui/toolbar.rs index e3525fe60..df31692d4 100644 --- a/configurator/src/app/pages/ui/toolbar.rs +++ b/configurator/src/app/pages/ui/toolbar.rs @@ -36,7 +36,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Layout mode"), tr("Simple: core pens. Regular: adds Shape Pen, Line, Arrow, presets. Advanced: shapes inline, multi-step undo."), layout_modes, - layout_labels, + &layout_labels, |app| app.draft.ui_toolbar_layout_mode, Message::ToolbarLayoutModeChanged, ) @@ -44,7 +44,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Zoom chip"), "", zoom_chips, - zoom_chip_labels, + &zoom_chip_labels, |app| app.draft.ui_toolbar_zoom_chip_display, Message::ToolbarZoomChipDisplayChanged, ) @@ -58,7 +58,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Shortcut edit click"), "", rebinds, - rebind_labels, + &rebind_labels, |app| app.draft.ui_toolbar_rebind_modifier, Message::ToolbarRebindModifierChanged, ) @@ -66,7 +66,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Stroke controls"), tr("How the style pill shows pen smoothing and Shape Pen sensitivity. Panel: one Pen feel button opening meters with a live smoothing preview. Meters or Steppers: inline controls."), stroke_styles, - stroke_style_labels, + &stroke_style_labels, |app| app.draft.ui_toolbar_stroke_controls, Message::ToolbarStrokeControlsChanged, ) @@ -179,7 +179,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { tr("Edit mode"), tr("Overrides below apply to the mode selected here; \"Default\" keeps the mode preset."), override_modes, - override_mode_labels, + &override_mode_labels, |app| app.override_mode, Message::ToolbarOverrideModeChanged, ); @@ -198,7 +198,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { field.label(), "", values, - labels, + &labels, move |app| toolbar_override(app, field), move |value| Message::ToolbarOverrideChanged(field, value), ); diff --git a/configurator/src/app/search/summary.rs b/configurator/src/app/search/summary.rs index 197c66d52..5708772e0 100644 --- a/configurator/src/app/search/summary.rs +++ b/configurator/src/app/search/summary.rs @@ -1,10 +1,24 @@ +use std::iter; + +use wayscriber::domain::Language; + use crate::app::state::ConfiguratorApp; use crate::models::{KeybindingsTabId, SearchQuery, TabId, UiTabId, keybinding_tab}; use wayscriber::config::{ PERFORMANCE_FIELD_METADATA, PerformanceFieldGroup, action_meta, toolbar_item_definitions, }; -use super::terms::*; +#[cfg(feature = "tablet-input")] +use super::terms::TABLET_TERMS; +use super::terms::{ + ARROW_TERMS, BOARD_GENERAL_TERMS, CAPTURE_FILE_TERMS, CAPTURE_PDF_IDENTITY_TERMS, + CAPTURE_PDF_TERMS, CAPTURE_REGION_TERMS, DAEMON_LIGHT_TERMS, DAEMON_SERVICE_TERMS, + DAEMON_SHORTCUT_TERMS, DAEMON_STATUS_TERMS, DRAWING_COLOR_TERMS, DRAWING_DEFAULT_TERMS, + DRAWING_DRAG_TERMS, DRAWING_FONT_TERMS, DRAWING_LASER_TERMS, HISTORY_CUSTOM_TERMS, + HISTORY_MAIN_TERMS, PRESET_CONTROL_TERMS, RENDER_PROFILE_GENERAL_TERMS, SESSION_CATALOG_TERMS, + SESSION_PERSISTENCE_TERMS, UI_GENERAL_TERMS, tab_aliases, tab_scope_aliases, ui_tab_aliases, + ui_tab_terms, +}; use super::types::{AppSearchSummary, SearchArea, TabSearchSummary}; pub(super) fn build_search_summary(app: &ConfiguratorApp) -> AppSearchSummary { @@ -167,10 +181,10 @@ fn performance_matches(query: &SearchQuery, summary: &mut TabSearchSummary) { ] { let mut parts = vec![identity]; parts.extend(area.section_titles()); - for metadata in PERFORMANCE_FIELD_METADATA + let group_fields = PERFORMANCE_FIELD_METADATA .iter() - .filter(|metadata| metadata.group == group) - { + .filter(|metadata| metadata.group == group); + for metadata in group_fields { parts.extend([metadata.path, metadata.label, metadata.help]); parts.extend_from_slice(metadata.search_terms); } @@ -187,7 +201,7 @@ fn ui_matches(query: &SearchQuery, summary: &mut TabSearchSummary) { .iter() .copied() .chain( - wayscriber::domain::Language::ALL + Language::ALL .into_iter() .flat_map(|language| [language.native_name(), language.word_for_language()]) .flatten(), @@ -468,7 +482,7 @@ impl ScopedSearchQuery for SearchQuery { .chain(parts.iter().copied()), ) || tab_scope_aliases(tab) .iter() - .any(|alias| self.matches_parts(std::iter::once(*alias).chain(parts.iter().copied()))) + .any(|alias| self.matches_parts(iter::once(*alias).chain(parts.iter().copied()))) } fn matches_any_raw_text(&self, values: &[&str]) -> bool { diff --git a/configurator/src/app/search/tests.rs b/configurator/src/app/search/tests.rs index f9bbd556c..f7ed8f744 100644 --- a/configurator/src/app/search/tests.rs +++ b/configurator/src/app/search/tests.rs @@ -5,6 +5,9 @@ //! that every control label a page shows is indexed by the search terms — //! return with the GTK page registry, once every page is ported. +use wayscriber::domain::Language; +use wayscriber::i18n; + use super::*; use std::path::PathBuf; @@ -12,6 +15,7 @@ use crate::models::session::SessionArtifactSummary; use crate::models::{ KeybindingField, KeybindingsTabId, SearchQuery, SessionCatalogItem, TabId, UiTabId, }; +use crate::test_env; use wayscriber::config::toolbar_item_ids as ids; #[test] @@ -856,13 +860,13 @@ fn catalog_item(id: &str) -> SessionCatalogItem { /// found, and titles as displayed find their tab, sub-tab and section. #[test] fn chinese_ui_search_matches_english_terms_and_displayed_titles() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::chinese_ui_search_matches_english_terms_and_displayed_titles" ), || { - wayscriber::i18n::set_language(wayscriber::domain::Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); let (mut app, _effects) = ConfiguratorApp::new_app(); let tabs_for = |app: &mut ConfiguratorApp, query: &str| { app.search_query = SearchQuery::new(query); @@ -889,17 +893,14 @@ fn chinese_ui_search_matches_english_terms_and_displayed_titles() { #[test] fn language_search_reveals_general_ui_in_either_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::language_search_reveals_general_ui_in_either_language" ), || { - for language in [ - wayscriber::domain::Language::English, - wayscriber::domain::Language::SimplifiedChinese, - ] { - wayscriber::i18n::set_language(language); + for language in [Language::English, Language::SimplifiedChinese] { + i18n::set_language(language); let (mut app, _effects) = ConfiguratorApp::new_app(); for query in ["language", "locale", "English", "简体中文", "中文", "语言"] { app.search_query = SearchQuery::new(query); diff --git a/configurator/src/app/session_catalog.rs b/configurator/src/app/session_catalog.rs index d7dd23947..384737316 100644 --- a/configurator/src/app/session_catalog.rs +++ b/configurator/src/app/session_catalog.rs @@ -1,3 +1,4 @@ +use std::fs; use std::fs::{File, OpenOptions}; use std::io::ErrorKind; use std::path::{Path, PathBuf}; @@ -17,6 +18,9 @@ mod move_file; pub(super) use duplicate::duplicate_session_catalog_entry; pub(super) use move_file::move_session_catalog_entry; +use wayscriber::config::{Config, LoadedConfig}; +use wayscriber::session::{ClearToolStateOutcome, SessionOptions, catalog}; +use wayscriber::{paths, session}; pub(super) async fn load_session_catalog() -> Result, String> { run_blocking( @@ -30,8 +34,7 @@ pub(super) async fn forget_session_catalog_entry( id: String, ) -> Result { run_blocking(BlockingJobKind::SessionCatalogMutation, move || { - let removed = wayscriber::session::catalog::forget_session_by_id(&id) - .map_err(|err| err.to_string())?; + let removed = catalog::forget_session_by_id(&id).map_err(|err| err.to_string())?; let items = load_session_catalog_sync()?; Ok(SessionCatalogActionResult::success( if removed { @@ -50,11 +53,8 @@ pub(super) async fn rename_session_catalog_entry( display_name: String, ) -> Result { run_blocking(BlockingJobKind::SessionCatalogMutation, move || { - let renamed = wayscriber::session::catalog::rename_session_display_name_by_id( - &id, - display_name.trim(), - ) - .map_err(|err| err.to_string())?; + let renamed = catalog::rename_session_display_name_by_id(&id, display_name.trim()) + .map_err(|err| err.to_string())?; let items = load_session_catalog_sync()?; Ok(SessionCatalogActionResult::success( if let Some(entry) = renamed { @@ -95,7 +95,7 @@ pub(super) async fn clear_session_catalog_entry( return Err(blocker); } - let outcome = wayscriber::session::clear_named_session_non_lock_artifacts(&item.path) + let outcome = session::clear_named_session_non_lock_artifacts(&item.path) .map_err(|err| err.to_string())?; let items = load_session_catalog_sync()?; Ok(SessionCatalogActionResult::success( @@ -125,8 +125,7 @@ pub(super) async fn clear_session_catalog_tool_state_entry( } let options = named_session_options_for_catalog_item(&item)?; - let outcome = - wayscriber::session::clear_tool_state(&options).map_err(|err| err.to_string())?; + let outcome = session::clear_tool_state(&options).map_err(|err| err.to_string())?; let items = load_session_catalog_sync()?; Ok(SessionCatalogActionResult::success( clear_tool_state_catalog_message(&item.display_name, outcome), @@ -144,7 +143,7 @@ fn service_status_blocker( } fn load_session_catalog_sync() -> Result, String> { - wayscriber::session::catalog::recent_sessions() + catalog::recent_sessions() .map_err(|err| err.to_string())? .into_iter() .map(SessionCatalogItem::from_entry) @@ -160,10 +159,10 @@ fn find_session_catalog_item(id: &str) -> Result { fn named_session_options_for_catalog_item( item: &SessionCatalogItem, -) -> Result { - let loaded = wayscriber::config::Config::load().map_err(|err| err.to_string())?; +) -> Result { + let loaded = Config::load().map_err(|err| err.to_string())?; ensure_session_config_available_for_destructive_action(&loaded)?; - let mut options = wayscriber::session::options_from_config_for_named_file( + let mut options = session::options_from_config_for_named_file( &loaded.config.session, item.path.clone(), None, @@ -173,7 +172,7 @@ fn named_session_options_for_catalog_item( } fn ensure_session_config_available_for_destructive_action( - loaded: &wayscriber::config::LoadedConfig, + loaded: &LoadedConfig, ) -> Result<(), String> { if loaded.section_failed("session") { return Err( @@ -184,24 +183,21 @@ fn ensure_session_config_available_for_destructive_action( Ok(()) } -fn clear_tool_state_catalog_message( - display_name: &str, - outcome: wayscriber::session::ClearToolStateOutcome, -) -> String { +fn clear_tool_state_catalog_message(display_name: &str, outcome: ClearToolStateOutcome) -> String { match outcome { - wayscriber::session::ClearToolStateOutcome::NoSession => { + ClearToolStateOutcome::NoSession => { trf("No saved session file found for {}.", &[&display_name]) } - wayscriber::session::ClearToolStateOutcome::NoToolState => { + ClearToolStateOutcome::NoToolState => { trf("No saved tool state found for {}.", &[&display_name]) } - wayscriber::session::ClearToolStateOutcome::Cleared { + ClearToolStateOutcome::Cleared { preserved_board_data: true, } => trf( tr("Cleared saved tool state for {}. Boards and history were preserved."), &[&display_name], ), - wayscriber::session::ClearToolStateOutcome::Cleared { + ClearToolStateOutcome::Cleared { preserved_board_data: false, } => trf( "Cleared saved tool state for {}. No board data was present.", @@ -238,8 +234,8 @@ enum RuntimeLockKind { impl RuntimeLockKind { fn path(self) -> PathBuf { match self { - Self::Daemon => wayscriber::paths::daemon_lock_file(), - Self::Overlay => wayscriber::paths::overlay_lock_file(), + Self::Daemon => paths::daemon_lock_file(), + Self::Overlay => paths::overlay_lock_file(), } } @@ -320,7 +316,7 @@ fn acquire_runtime_lock_for_inactive_operation( ) -> Result { let path = kind.path(); if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(|err| { + fs::create_dir_all(parent).map_err(|err| { format!( "{} is disabled because the {} lock directory could not be created: {} ({err})", operation.label(), diff --git a/configurator/src/app/session_catalog/duplicate.rs b/configurator/src/app/session_catalog/duplicate.rs index 69dcbf37f..0023695bf 100644 --- a/configurator/src/app/session_catalog/duplicate.rs +++ b/configurator/src/app/session_catalog/duplicate.rs @@ -1,4 +1,7 @@ use wayscriber::i18n::{tr, trf}; +use wayscriber::session; +use wayscriber::session::catalog; +use wayscriber::session::catalog::CatalogEvent; use std::path::{Path, PathBuf}; @@ -39,11 +42,11 @@ fn duplicate_session_catalog_entry_sync( return Err(blocker); } - let outcome = wayscriber::session::duplicate_named_session_primary(&item.path, target) + let outcome = session::duplicate_named_session_primary(&item.path, target) .map_err(|err| err.to_string())?; - let entry = match wayscriber::session::catalog::upsert_session_event_with_display_name( + let entry = match catalog::upsert_session_event_with_display_name( &outcome.target, - wayscriber::session::catalog::CatalogEvent::Saved, + CatalogEvent::Saved, &item.display_name, ) { Ok(entry) => entry, @@ -81,11 +84,14 @@ fn duplicate_session_catalog_entry_sync( #[cfg(test)] mod tests { use std::ffi::OsString; + use std::fs; use crate::models::{ DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability, ShortcutBackend, }; + use crate::test_env::EnvGuard; + use crate::test_temp; use wayscriber::env_vars::{CATALOG_HOOKS_TEST_ENV, XDG_DATA_HOME_ENV, XDG_RUNTIME_DIR_ENV}; use super::*; @@ -110,22 +116,22 @@ mod tests { #[test] fn duplicate_session_catalog_entry_copies_primary_and_catalogs_new_entry() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[ + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[ (CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()), (XDG_DATA_HOME_ENV, temp.path().as_os_str()), (XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()), ]); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("lecture-copy.wayscriber-session"); - let source_artifacts = wayscriber::session::named_session_artifact_paths(&source); - let target_artifacts = wayscriber::session::named_session_artifact_paths(&target); - std::fs::write(&source_artifacts.primary, b"primary").unwrap(); - std::fs::write(&source_artifacts.backup, b"backup").unwrap(); - std::fs::write(&source_artifacts.lock, b"lock").unwrap(); - let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name( + let source_artifacts = session::named_session_artifact_paths(&source); + let target_artifacts = session::named_session_artifact_paths(&target); + fs::write(&source_artifacts.primary, b"primary").unwrap(); + fs::write(&source_artifacts.backup, b"backup").unwrap(); + fs::write(&source_artifacts.lock, b"lock").unwrap(); + let source_entry = catalog::upsert_session_event_with_display_name( &source, - wayscriber::session::catalog::CatalogEvent::Saved, + CatalogEvent::Saved, "Lecture", ) .unwrap(); @@ -134,14 +140,11 @@ mod tests { duplicate_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()) .unwrap(); - assert_eq!( - std::fs::read(&target_artifacts.primary).unwrap(), - b"primary" - ); + assert_eq!(fs::read(&target_artifacts.primary).unwrap(), b"primary"); assert!(!target_artifacts.backup.exists()); assert!(!target_artifacts.lock.exists()); assert!(result.message.contains("Duplicated Lecture")); - let recents = wayscriber::session::catalog::recent_sessions().unwrap(); + let recents = catalog::recent_sessions().unwrap(); assert_eq!(recents.len(), 2); assert_eq!( recents @@ -155,52 +158,47 @@ mod tests { #[test] fn duplicate_session_catalog_entry_warns_when_catalog_update_fails_after_copy() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[ + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[ (CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()), (XDG_DATA_HOME_ENV, temp.path().as_os_str()), (XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()), ]); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("lecture-copy.wayscriber-session"); - std::fs::write(&source, b"primary").unwrap(); - let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name( + fs::write(&source, b"primary").unwrap(); + let source_entry = catalog::upsert_session_event_with_display_name( &source, - wayscriber::session::catalog::CatalogEvent::Saved, + CatalogEvent::Saved, "Lecture", ) .unwrap(); let lock_path = catalog_lock_path(); - std::fs::remove_file(&lock_path).unwrap(); - std::fs::create_dir(&lock_path).unwrap(); + fs::remove_file(&lock_path).unwrap(); + fs::create_dir(&lock_path).unwrap(); let result = duplicate_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()) .unwrap(); - std::fs::remove_dir(&lock_path).unwrap(); + fs::remove_dir(&lock_path).unwrap(); assert!(result.warning); assert!( result .message .contains("failed to update the session catalog") ); - assert_eq!(std::fs::read(&target).unwrap(), b"primary"); + assert_eq!(fs::read(&target).unwrap(), b"primary"); assert_eq!( result.items.len(), 1, "warning should keep the pre-copy catalog rows visible" ); - assert_eq!( - wayscriber::session::catalog::recent_sessions() - .unwrap() - .len(), - 1 - ); + assert_eq!(catalog::recent_sessions().unwrap().len(), 1); } - fn catalog_lock_path() -> std::path::PathBuf { - let catalog_path = wayscriber::session::catalog::catalog_path(); + fn catalog_lock_path() -> PathBuf { + let catalog_path = catalog::catalog_path(); let mut raw = OsString::from(catalog_path.as_os_str()); raw.push(".lock"); raw.into() diff --git a/configurator/src/app/session_catalog/move_file.rs b/configurator/src/app/session_catalog/move_file.rs index be1dc7d1f..99776a21d 100644 --- a/configurator/src/app/session_catalog/move_file.rs +++ b/configurator/src/app/session_catalog/move_file.rs @@ -1,4 +1,6 @@ use wayscriber::i18n::{tr, trf}; +use wayscriber::session; +use wayscriber::session::{NamedSessionMoveOutcome, catalog}; use std::path::{Path, PathBuf}; @@ -41,29 +43,27 @@ fn move_session_catalog_entry_sync( return Err(blocker); } - let outcome = wayscriber::session::move_named_session_non_lock_artifacts(&item.path, target) + let outcome = session::move_named_session_non_lock_artifacts(&item.path, target) .map_err(|err| err.to_string())?; - let entry = - match wayscriber::session::catalog::move_session_path_by_id(&item.id, &outcome.target) { - Ok(Some(entry)) => entry, - Ok(None) => { - return rollback_moved_artifacts_after_catalog_failure( - &item, - &outcome, - initial_items, - tr("the original catalog entry disappeared before metadata could be updated") - .to_string(), - ); - } - Err(err) => { - return rollback_moved_artifacts_after_catalog_failure( - &item, - &outcome, - initial_items, - format!("failed to update the session catalog: {err}"), - ); - } - }; + let entry = match catalog::move_session_path_by_id(&item.id, &outcome.target) { + Ok(Some(entry)) => entry, + Ok(None) => { + return rollback_moved_artifacts_after_catalog_failure( + &item, + &outcome, + initial_items, + tr("the original catalog entry disappeared before metadata could be updated"), + ); + } + Err(err) => { + return rollback_moved_artifacts_after_catalog_failure( + &item, + &outcome, + initial_items, + &format!("failed to update the session catalog: {err}"), + ); + } + }; let items = match load_session_catalog_sync() { Ok(items) => items, Err(err) => { @@ -88,11 +88,11 @@ fn move_session_catalog_entry_sync( fn rollback_moved_artifacts_after_catalog_failure( item: &SessionCatalogItem, - outcome: &wayscriber::session::NamedSessionMoveOutcome, + outcome: &NamedSessionMoveOutcome, initial_items: Vec, - reason: String, + reason: &str, ) -> Result { - match wayscriber::session::rollback_named_session_non_lock_artifacts_move(outcome) { + match session::rollback_named_session_non_lock_artifacts_move(outcome) { Ok(_) => Err(trf( "Move Session failed because {}; rolled back moved files for {}.", &[&reason, &item.display_name], @@ -118,10 +118,10 @@ fn reject_catalog_target_collision( source_id: &str, target: &Path, ) -> Result<(), String> { - if let Some(existing) = items.iter().find(|item| { - item.id != source_id - && wayscriber::session::catalog::session_paths_match(&item.path, target) - }) { + if let Some(existing) = items + .iter() + .find(|item| item.id != source_id && catalog::session_paths_match(&item.path, target)) + { return Err(trf( "Move Session target is already in the catalog as {}.", &[&existing.display_name], @@ -133,11 +133,16 @@ fn reject_catalog_target_collision( #[cfg(test)] mod tests { use std::ffi::OsString; + use std::fs; + + use wayscriber::session::catalog::CatalogEvent; use crate::models::{ DaemonRuntimeStatus, DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability, ShortcutBackend, }; + use crate::test_env::EnvGuard; + use crate::test_temp; use wayscriber::env_vars::{CATALOG_HOOKS_TEST_ENV, XDG_DATA_HOME_ENV, XDG_RUNTIME_DIR_ENV}; use super::*; @@ -162,22 +167,22 @@ mod tests { #[test] fn move_session_catalog_entry_moves_artifacts_and_preserves_catalog_id() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[ + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[ (CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()), (XDG_DATA_HOME_ENV, temp.path().as_os_str()), (XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()), ]); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session"); - let source_artifacts = wayscriber::session::named_session_artifact_paths(&source); - let target_artifacts = wayscriber::session::named_session_artifact_paths(&target); - std::fs::write(&source_artifacts.primary, b"primary").unwrap(); - std::fs::write(&source_artifacts.backup, b"backup").unwrap(); - std::fs::write(&source_artifacts.lock, b"source lock").unwrap(); - let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name( + let source_artifacts = session::named_session_artifact_paths(&source); + let target_artifacts = session::named_session_artifact_paths(&target); + fs::write(&source_artifacts.primary, b"primary").unwrap(); + fs::write(&source_artifacts.backup, b"backup").unwrap(); + fs::write(&source_artifacts.lock, b"source lock").unwrap(); + let source_entry = catalog::upsert_session_event_with_display_name( &source, - wayscriber::session::catalog::CatalogEvent::Saved, + CatalogEvent::Saved, "Lecture", ) .unwrap(); @@ -186,18 +191,12 @@ mod tests { move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()).unwrap(); assert!(!result.warning); - assert_eq!( - std::fs::read(&target_artifacts.primary).unwrap(), - b"primary" - ); - assert_eq!(std::fs::read(&target_artifacts.backup).unwrap(), b"backup"); - assert_eq!( - std::fs::read(&source_artifacts.lock).unwrap(), - b"source lock" - ); + assert_eq!(fs::read(&target_artifacts.primary).unwrap(), b"primary"); + assert_eq!(fs::read(&target_artifacts.backup).unwrap(), b"backup"); + assert_eq!(fs::read(&source_artifacts.lock).unwrap(), b"source lock"); assert!(!source_artifacts.primary.exists()); assert!(!source_artifacts.backup.exists()); - let recents = wayscriber::session::catalog::recent_sessions().unwrap(); + let recents = catalog::recent_sessions().unwrap(); assert_eq!(recents.len(), 1); assert_eq!(recents[0].id, source_entry.id); assert_eq!(Path::new(&recents[0].path), target.as_path()); @@ -205,22 +204,22 @@ mod tests { #[test] fn move_session_catalog_entry_failure_keeps_catalog_and_source_artifacts() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[ + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[ (CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()), (XDG_DATA_HOME_ENV, temp.path().as_os_str()), (XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()), ]); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session"); - let source_artifacts = wayscriber::session::named_session_artifact_paths(&source); - let target_artifacts = wayscriber::session::named_session_artifact_paths(&target); - std::fs::write(&source_artifacts.primary, b"primary").unwrap(); - std::fs::write(&source_artifacts.backup, b"backup").unwrap(); - std::fs::write(&target_artifacts.clear_marker, b"cleared").unwrap(); - let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name( + let source_artifacts = session::named_session_artifact_paths(&source); + let target_artifacts = session::named_session_artifact_paths(&target); + fs::write(&source_artifacts.primary, b"primary").unwrap(); + fs::write(&source_artifacts.backup, b"backup").unwrap(); + fs::write(&target_artifacts.clear_marker, b"cleared").unwrap(); + let source_entry = catalog::upsert_session_event_with_display_name( &source, - wayscriber::session::catalog::CatalogEvent::Saved, + CatalogEvent::Saved, "Lecture", ) .unwrap(); @@ -229,16 +228,13 @@ mod tests { .expect_err("target artifact should block move"); assert!(err.contains("already has session artifacts")); + assert_eq!(fs::read(&source_artifacts.primary).unwrap(), b"primary"); + assert_eq!(fs::read(&source_artifacts.backup).unwrap(), b"backup"); assert_eq!( - std::fs::read(&source_artifacts.primary).unwrap(), - b"primary" - ); - assert_eq!(std::fs::read(&source_artifacts.backup).unwrap(), b"backup"); - assert_eq!( - std::fs::read(&target_artifacts.clear_marker).unwrap(), + fs::read(&target_artifacts.clear_marker).unwrap(), b"cleared" ); - let recents = wayscriber::session::catalog::recent_sessions().unwrap(); + let recents = catalog::recent_sessions().unwrap(); assert_eq!(recents.len(), 1); assert_eq!(recents[0].id, source_entry.id); assert_eq!(Path::new(&recents[0].path), source.as_path()); @@ -246,75 +242,71 @@ mod tests { #[test] fn move_session_catalog_entry_rejects_catalog_target_collision_before_disk_move() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[ + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[ (CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()), (XDG_DATA_HOME_ENV, temp.path().as_os_str()), (XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()), ]); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session"); - std::fs::write(&source, b"primary").unwrap(); - let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name( + fs::write(&source, b"primary").unwrap(); + let source_entry = catalog::upsert_session_event_with_display_name( &source, - wayscriber::session::catalog::CatalogEvent::Saved, + CatalogEvent::Saved, "Lecture", ) .unwrap(); - wayscriber::session::catalog::upsert_session_event_with_display_name( - &target, - wayscriber::session::catalog::CatalogEvent::Saved, - "Archive", - ) - .unwrap(); + catalog::upsert_session_event_with_display_name(&target, CatalogEvent::Saved, "Archive") + .unwrap(); let err = move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()) .expect_err("target catalog entry should block move"); assert!(err.contains("already in the catalog")); - assert_eq!(std::fs::read(&source).unwrap(), b"primary"); + assert_eq!(fs::read(&source).unwrap(), b"primary"); assert!(!target.exists()); } #[test] fn move_session_catalog_entry_rolls_back_when_catalog_update_fails_after_move() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[ + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[ (CATALOG_HOOKS_TEST_ENV, temp.path().as_os_str()), (XDG_DATA_HOME_ENV, temp.path().as_os_str()), (XDG_RUNTIME_DIR_ENV, temp.path().as_os_str()), ]); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session"); - let source_artifacts = wayscriber::session::named_session_artifact_paths(&source); - std::fs::write(&source_artifacts.primary, b"primary").unwrap(); - std::fs::create_dir(&source_artifacts.backup).unwrap(); - let source_entry = wayscriber::session::catalog::upsert_session_event_with_display_name( + let source_artifacts = session::named_session_artifact_paths(&source); + fs::write(&source_artifacts.primary, b"primary").unwrap(); + fs::create_dir(&source_artifacts.backup).unwrap(); + let source_entry = catalog::upsert_session_event_with_display_name( &source, - wayscriber::session::catalog::CatalogEvent::Saved, + CatalogEvent::Saved, "Lecture", ) .unwrap(); let lock_path = catalog_lock_path(); - std::fs::remove_file(&lock_path).unwrap(); - std::fs::create_dir(&lock_path).unwrap(); + fs::remove_file(&lock_path).unwrap(); + fs::create_dir(&lock_path).unwrap(); let err = move_session_catalog_entry_sync(&source_entry.id, &target, &inactive_status()) .expect_err("catalog update failure should roll back disk move"); - std::fs::remove_dir(&lock_path).unwrap(); + fs::remove_dir(&lock_path).unwrap(); assert!(err.contains("failed to update the session catalog")); assert!(err.contains("rolled back moved files")); - assert_eq!(std::fs::read(&source).unwrap(), b"primary"); + assert_eq!(fs::read(&source).unwrap(), b"primary"); assert!(source_artifacts.backup.is_dir()); assert!(!target.exists()); - let recents = wayscriber::session::catalog::recent_sessions().unwrap(); + let recents = catalog::recent_sessions().unwrap(); assert_eq!(recents.len(), 1); assert_eq!(Path::new(&recents[0].path), source.as_path()); } fn catalog_lock_path() -> PathBuf { - let catalog_path = wayscriber::session::catalog::catalog_path(); + let catalog_path = catalog::catalog_path(); let mut raw = OsString::from(catalog_path.as_os_str()); raw.push(".lock"); raw.into() diff --git a/configurator/src/app/session_catalog/tests.rs b/configurator/src/app/session_catalog/tests.rs index af8fe11b7..2846e0216 100644 --- a/configurator/src/app/session_catalog/tests.rs +++ b/configurator/src/app/session_catalog/tests.rs @@ -1,9 +1,14 @@ +use wayscriber::config::{ConfigSectionError, ConfigSource, ConfigValidationReport}; + use super::*; use std::path::PathBuf; +use crate::models::session::SessionArtifactSummary; use crate::models::{ DesktopEnvironment, LightShortcutApplyCapability, ShortcutApplyCapability, ShortcutBackend, }; +use crate::test_env::EnvGuard; +use crate::test_temp; use wayscriber::env_vars::XDG_RUNTIME_DIR_ENV; fn daemon_status(active: bool) -> DaemonRuntimeStatus { @@ -26,8 +31,8 @@ fn daemon_status(active: bool) -> DaemonRuntimeStatus { #[test] fn clear_policy_blocks_running_daemon() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let status = daemon_status(true); let blocker = SessionCatalogOperation::Clear @@ -39,8 +44,8 @@ fn clear_policy_blocks_running_daemon() { #[test] fn clear_policy_allows_inactive_daemon() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let status = daemon_status(false); let blocker = SessionCatalogOperation::Clear.cached_status_blocker(Some(&status)); @@ -50,8 +55,8 @@ fn clear_policy_allows_inactive_daemon() { #[test] fn duplicate_policy_uses_duplicate_status_message() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let blocker = SessionCatalogOperation::Duplicate .cached_status_blocker(None) .expect("unknown status should block duplicate"); @@ -61,8 +66,8 @@ fn duplicate_policy_uses_duplicate_status_message() { #[test] fn move_policy_uses_move_status_message() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let blocker = SessionCatalogOperation::Move .cached_status_blocker(None) .expect("unknown status should block move"); @@ -72,8 +77,8 @@ fn move_policy_uses_move_status_message() { #[test] fn clear_policy_blocks_unknown_daemon_status() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let blocker = SessionCatalogOperation::Clear .cached_status_blocker(None) .expect("unknown status should block clear"); @@ -83,8 +88,8 @@ fn clear_policy_blocks_unknown_daemon_status() { #[test] fn clear_tool_state_policy_uses_tool_state_status_message() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let blocker = SessionCatalogOperation::ClearToolState .cached_status_blocker(None) .expect("unknown status should block tool reset"); @@ -95,8 +100,8 @@ fn clear_tool_state_policy_uses_tool_state_status_message() { #[test] fn session_clear_transaction_guard_blocks_manual_daemon_lock() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap(); let error = acquire_runtime_lock_for_inactive_operation( RuntimeLockKind::Daemon, @@ -109,8 +114,8 @@ fn session_clear_transaction_guard_blocks_manual_daemon_lock() { #[test] fn inactive_session_reservation_holds_the_overlay_lock_but_not_the_daemon_lock() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let reservation = reserve_inactive_session_operation(SessionCatalogOperation::Clear).unwrap(); @@ -130,8 +135,8 @@ fn inactive_session_reservation_holds_the_overlay_lock_but_not_the_daemon_lock() #[test] fn inactive_session_reservation_refuses_a_running_daemon() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap(); let error = match reserve_inactive_session_operation(SessionCatalogOperation::Move) { @@ -148,8 +153,8 @@ fn inactive_session_reservation_refuses_a_running_daemon() { #[test] fn cached_status_blocker_does_not_probe_runtime_locks() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap(); let _overlay_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Overlay).unwrap(); let status = daemon_status(false); @@ -164,8 +169,8 @@ fn cached_status_blocker_does_not_probe_runtime_locks() { #[test] fn clear_runtime_guards_hold_daemon_and_overlay_locks() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let _daemon_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Daemon).unwrap(); let _overlay_lock = acquire_runtime_lock_for_clear(RuntimeLockKind::Overlay).unwrap(); @@ -190,7 +195,7 @@ fn session_artifact_status_reports_size_when_present() { created_at_millis: 1_767_261_600_000, last_opened_at_millis: None, last_saved_at_millis: None, - artifacts: crate::models::session::SessionArtifactSummary { + artifacts: SessionArtifactSummary { primary_exists: true, backup_exists: false, recovery_exists: false, @@ -206,11 +211,11 @@ fn session_artifact_status_reports_size_when_present() { #[test] fn clear_tool_state_rejects_defaulted_session_config() { - let loaded = wayscriber::config::LoadedConfig { - config: wayscriber::config::Config::default(), - source: wayscriber::config::ConfigSource::Primary, - validation: wayscriber::config::ConfigValidationReport::default(), - section_errors: vec![wayscriber::config::ConfigSectionError { + let loaded = LoadedConfig { + config: Config::default(), + source: ConfigSource::Primary, + validation: ConfigValidationReport::default(), + section_errors: vec![ConfigSectionError { section: "session".to_string(), error: "invalid value".to_string(), }], diff --git a/configurator/src/app/shortcut_workflow.rs b/configurator/src/app/shortcut_workflow.rs index ad627aaa8..95b601e55 100644 --- a/configurator/src/app/shortcut_workflow.rs +++ b/configurator/src/app/shortcut_workflow.rs @@ -1,4 +1,6 @@ //! Shortcut edit modes are mutually exclusive and clear as one workflow. +use std::mem; + use crate::models::{ KeybindingField, PendingShortcutConflict, ShortcutRecorderState, ShortcutTextEditor, }; @@ -59,7 +61,7 @@ impl ShortcutWorkflow { } pub(crate) fn take_recorder(&mut self) -> Option { self.recorder()?; - match std::mem::take(&mut self.phase) { + match mem::take(&mut self.phase) { ShortcutPhase::Recording(value) => Some(value), _ => None, } @@ -98,7 +100,7 @@ impl ShortcutWorkflow { } pub(crate) fn take_conflict(&mut self) -> Option { self.conflict()?; - match std::mem::take(&mut self.phase) { + match mem::take(&mut self.phase) { ShortcutPhase::Conflict(value) => Some(value), _ => None, } diff --git a/configurator/src/app/startup.rs b/configurator/src/app/startup.rs index d822dc5c4..e20630309 100644 --- a/configurator/src/app/startup.rs +++ b/configurator/src/app/startup.rs @@ -1,6 +1,8 @@ +use std::mem; + use wayscriber::configurator_destination::ConfiguratorScreen; -use crate::models::{SearchQuery, TabId, UiTabId}; +use crate::models::{SearchQuery, TabId, UiTabId, keybindings}; use super::effects::Effect; use super::state::ConfiguratorApp; @@ -12,7 +14,7 @@ impl ConfiguratorApp { /// Reload is the user asking for the file again, not a fresh launch, and /// snapping the tabs back would undo wherever they navigated in between. pub(crate) fn apply_startup_request(&mut self) -> Vec { - let request = std::mem::take(&mut self.startup_request); + let request = mem::take(&mut self.startup_request); if let Some(problem) = request.problem() { self.status = self.status.clone().with_note(&problem.message()); @@ -40,8 +42,7 @@ impl ConfiguratorApp { // nothing to show lands where the matches actually are. self.align_active_tabs_for_search(); if self.active_tab == TabId::Keybindings - && let Some(field) = - crate::models::keybindings::field_matching_search_term(&term) + && let Some(field) = keybindings::field_matching_search_term(&term) { self.select_keybinding_field(field); } @@ -102,11 +103,18 @@ impl ConfiguratorApp { #[cfg(test)] mod tests { - use std::path::Path; + use std::ffi::OsString; + use std::path::{Path, PathBuf}; + use std::{fs, iter}; use wayscriber::config::ConfigDocument; + use wayscriber::configurator_destination; use wayscriber::configurator_destination::{ConfiguratorDestination, KeybindingsSection}; + use crate::app::search::SearchArea; + use crate::models::KeybindingField; + use crate::test_env; + use super::*; use crate::app::state::StatusMessage; use crate::messages::CommandMessage; @@ -117,18 +125,18 @@ mod tests { status.text().unwrap_or_default().to_string() } - fn args(values: &[&str]) -> Vec { - std::iter::once("wayscriber-configurator") + fn args(values: &[&str]) -> Vec { + iter::once("wayscriber-configurator") .chain(values.iter().copied()) - .map(std::ffi::OsString::from) + .map(OsString::from) .collect() } /// A config file of its own, so the load path under test is the real one. - fn config_file() -> (TempDir, std::path::PathBuf) { + fn config_file() -> (TempDir, PathBuf) { let dir = tempdir().expect("temporary test directory"); let path = dir.path().join("config.toml"); - std::fs::write(&path, "").expect("write test config"); + fs::write(&path, "").expect("write test config"); (dir, path) } @@ -140,8 +148,8 @@ mod tests { } /// The app as the launcher would start it, after its first config load. - fn app_launched_with(values: &[&str]) -> (ConfiguratorApp, TempDir, std::path::PathBuf) { - crate::test_env::sanitize_language_environment(); + fn app_launched_with(values: &[&str]) -> (ConfiguratorApp, TempDir, PathBuf) { + test_env::sanitize_language_environment(); let (dir, path) = config_file(); let (mut app, _effects) = ConfiguratorApp::new_app_with_startup(StartupRequest::from_args(args(values))); @@ -237,8 +245,7 @@ mod tests { #[test] fn the_onboarding_hint_destination_lands_on_the_general_ui_setting() { - let launched = - wayscriber::configurator_destination::onboarding_hints_destination().as_arg(); + let launched = configurator_destination::onboarding_hints_destination().as_arg(); let (app, _dir, _path) = app_launched_with(&["--open", &launched]); assert_eq!(app.active_tab, TabId::Ui); @@ -247,7 +254,7 @@ mod tests { let ui = summary .tab(TabId::Ui) .expect("General UI destination match"); - assert!(ui.area_matches(crate::app::search::SearchArea::UiGeneral)); + assert!(ui.area_matches(SearchArea::UiGeneral)); } /// The shortcut-action row: subtab plus action search. @@ -259,10 +266,7 @@ mod tests { assert_eq!(app.active_tab, TabId::Keybindings); assert_eq!(app.active_keybindings_tab, KeybindingsTabId::Drawing); assert_eq!(app.search_query.raw(), "Clear Canvas"); - assert_eq!( - app.selected_keybinding, - Some(crate::models::KeybindingField::ClearCanvas) - ); + assert_eq!(app.selected_keybinding, Some(KeybindingField::ClearCanvas)); assert!(app.search_summary().tab(TabId::Keybindings).is_some()); } @@ -275,10 +279,7 @@ mod tests { assert_eq!(app.active_tab, TabId::Keybindings); assert_eq!(app.active_keybindings_tab, KeybindingsTabId::Drawing); - assert_eq!( - app.selected_keybinding, - Some(crate::models::KeybindingField::ClearCanvas) - ); + assert_eq!(app.selected_keybinding, Some(KeybindingField::ClearCanvas)); } /// A term with no match on the requested screen is the one case where diff --git a/configurator/src/app/state.rs b/configurator/src/app/state.rs index 034a8e551..8d7d93a40 100644 --- a/configurator/src/app/state.rs +++ b/configurator/src/app/state.rs @@ -3,12 +3,15 @@ use wayscriber::i18n::tr; use std::collections::HashMap; use wayscriber::config::{Config, MigrationPreview, PRESET_SLOTS_MAX}; +use wayscriber::i18n; use crate::models::{ ColorPickerId, ConfigDraft, DesktopEnvironment, DragMouseButton, KeybindingField, KeybindingsTabId, SearchQuery, SessionCatalogState, ShortcutManagerFilter, ShortcutManagerSort, StartupRequest, TabId, ToolbarLayoutModeOption, UiTabId, }; +#[cfg(test)] +use crate::test_env; use super::effects::Effect; @@ -222,7 +225,7 @@ impl ConfiguratorApp { /// about to draw. A transition that fails keeps the language it found. /// The shell notices the change and rebuilds its pages in that language. pub(crate) fn apply_draft_language(&self) { - wayscriber::i18n::set_language(self.draft.ui_language.to_language()); + i18n::set_language(self.draft.ui_language.to_language()); } /// The app as a launch with no destination. @@ -231,7 +234,7 @@ impl ConfiguratorApp { /// when it is the empty one. #[cfg(test)] pub(crate) fn new_app() -> (Self, Vec) { - crate::test_env::sanitize_language_environment(); + test_env::sanitize_language_environment(); Self::new_app_with_startup(StartupRequest::default()) } diff --git a/configurator/src/app/update/boards.rs b/configurator/src/app/update/boards.rs index 016cf8e94..30834a95b 100644 --- a/configurator/src/app/update/boards.rs +++ b/configurator/src/app/update/boards.rs @@ -1,3 +1,5 @@ +use wayscriber::domain::BoardGridKind; + use crate::models::{ BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorPickerId, }; @@ -9,7 +11,7 @@ impl ConfiguratorApp { pub(super) fn handle_boards_grid_kind_changed( &mut self, index: usize, - value: wayscriber::domain::BoardGridKind, + value: BoardGridKind, ) -> Vec { if let Some(item) = self.draft.boards.items.get_mut(index) { item.grid_kind = value; diff --git a/configurator/src/app/update/color_picker.rs b/configurator/src/app/update/color_picker.rs index 29ac2fa07..fafadcd47 100644 --- a/configurator/src/app/update/color_picker.rs +++ b/configurator/src/app/update/color_picker.rs @@ -15,9 +15,10 @@ impl ConfiguratorApp { value: String, ) -> Vec { self.status = StatusMessage::idle(); - self.color_picker_hex.insert(id, value.clone()); + let parsed = parse_hex(&value); + self.color_picker_hex.insert(id, value); - if let Some((rgb, alpha)) = parse_hex(&value) { + if let Some((rgb, alpha)) = parsed { let alpha = if id.uses_alpha() { alpha.or_else(|| self.current_alpha_for_id(id)) } else { @@ -169,41 +170,41 @@ impl ConfiguratorApp { } } ColorPickerId::StatusBarBg => { - self.apply_quad_rgb(QuadField::StatusBarBg, values, alpha); + self.apply_quad_rgb(QuadField::StatusBarBg, &values, alpha); } ColorPickerId::StatusBarText => { - self.apply_quad_rgb(QuadField::StatusBarText, values, alpha); + self.apply_quad_rgb(QuadField::StatusBarText, &values, alpha); } ColorPickerId::HighlightFill => { - self.apply_quad_rgb(QuadField::HighlightFill, values, alpha); + self.apply_quad_rgb(QuadField::HighlightFill, &values, alpha); } ColorPickerId::HighlightOutline => { - self.apply_quad_rgb(QuadField::HighlightOutline, values, alpha); + self.apply_quad_rgb(QuadField::HighlightOutline, &values, alpha); } ColorPickerId::LaserColor => { - self.apply_quad_rgb(QuadField::LaserColor, values, alpha); + self.apply_quad_rgb(QuadField::LaserColor, &values, alpha); } ColorPickerId::HelpBg => { - self.apply_quad_rgb(QuadField::HelpBg, values, alpha); + self.apply_quad_rgb(QuadField::HelpBg, &values, alpha); } ColorPickerId::HelpBorder => { - self.apply_quad_rgb(QuadField::HelpBorder, values, alpha); + self.apply_quad_rgb(QuadField::HelpBorder, &values, alpha); } ColorPickerId::HelpText => { - self.apply_quad_rgb(QuadField::HelpText, values, alpha); + self.apply_quad_rgb(QuadField::HelpText, &values, alpha); } ColorPickerId::ExportPdfLabelText => { - self.apply_quad_rgb(QuadField::ExportPdfLabelText, values, alpha); + self.apply_quad_rgb(QuadField::ExportPdfLabelText, &values, alpha); } ColorPickerId::ExportPdfLabelBackground => { - self.apply_quad_rgb(QuadField::ExportPdfLabelBackground, values, alpha); + self.apply_quad_rgb(QuadField::ExportPdfLabelBackground, &values, alpha); } } self.sync_color_picker_hex_for_id(id); } - fn apply_quad_rgb(&mut self, field: QuadField, values: [String; 3], alpha: Option) { + fn apply_quad_rgb(&mut self, field: QuadField, values: &[String; 3], alpha: Option) { for (component, value) in values.iter().enumerate() { self.draft.set_quad(field, component, value.to_string()); } diff --git a/configurator/src/app/update/config/load.rs b/configurator/src/app/update/config/load.rs index 5e195a5fa..32cfab2d0 100644 --- a/configurator/src/app/update/config/load.rs +++ b/configurator/src/app/update/config/load.rs @@ -2,6 +2,7 @@ use wayscriber::i18n::{tr, trf}; use wayscriber::config::ConfigDocument; +use crate::app::document_workflow::LeaveAction; use crate::models::ConfigDraft; use super::super::super::effects::Effect; @@ -60,7 +61,7 @@ impl ConfiguratorApp { } pub(in crate::app::update) fn handle_reload_requested(&mut self) -> Vec { - self.request_leave(crate::app::document_workflow::LeaveAction::Reload) + self.request_leave(LeaveAction::Reload) } pub(in crate::app::update) fn begin_config_reload(&mut self) -> Vec { diff --git a/configurator/src/app/update/config/tests.rs b/configurator/src/app/update/config/tests.rs index ff6a6d2e7..86d69d5af 100644 --- a/configurator/src/app/update/config/tests.rs +++ b/configurator/src/app/update/config/tests.rs @@ -1,7 +1,11 @@ +use std::fs; use std::path::{Path, PathBuf}; +use wayscriber::config; use wayscriber::config::{Action, CURRENT_CONFIG_REVISION, Config, ConfigDocument}; +use crate::test_temp; + use super::save::save_validation_errors; use super::*; use crate::app::effects::Effect; @@ -16,9 +20,9 @@ fn status_contains(status: &StatusMessage, needle: &str) -> bool { } fn temp_config_document(name: &str, contents: &str) -> (TempDir, Box) { - let temp = crate::test_temp::tempdir().expect("temporary config directory"); + let temp = test_temp::tempdir().expect("temporary config directory"); let path = temp.path().join(format!("{name}.toml")); - std::fs::write(&path, contents).expect("write test config"); + fs::write(&path, contents).expect("write test config"); let document = ConfigDocument::load_from_path(&path).expect("load test config document"); (temp, Box::new(document)) } @@ -128,7 +132,7 @@ fn handle_config_loaded_surfaces_resolved_shortcut_conflicts() { "shortcut-conflict", &format!( "config_revision = {}\n\n[keybindings]\ntoggle_toolbar = [\"F2\"]\ncycle_toolbar_display = [\"F2\"]\n", - wayscriber::config::CURRENT_CONFIG_REVISION + config::CURRENT_CONFIG_REVISION ), ); @@ -155,7 +159,7 @@ fn handle_config_loaded_surfaces_skipped_default_shortcuts() { "skipped-default", &format!( "config_revision = {}\n\n[keybindings]\ntoggle_toolbar = [\"F2\", \"F9\"]\n", - wayscriber::config::CURRENT_CONFIG_REVISION + config::CURRENT_CONFIG_REVISION ), ); @@ -185,7 +189,7 @@ fn handle_config_loaded_surfaces_shortcuts_that_could_not_be_parsed() { "invalid-shortcut", &format!( "config_revision = {}\n\n[keybindings]\nclear_canvas = [\"Ctrl+Shift\"]\n", - wayscriber::config::CURRENT_CONFIG_REVISION + config::CURRENT_CONFIG_REVISION ), ); @@ -211,7 +215,7 @@ fn handle_config_loaded_separates_every_keybinding_diagnostic_kind() { "invalid-and-conflicting", &format!( "config_revision = {}\n\n[keybindings]\nclear_canvas = [\"Ctrl+Shift\"]\ntoggle_toolbar = [\"F2\", \"F9\"]\nundo = [\"Ctrl+Alt+U\"]\nredo = [\"Ctrl+Alt+U\"]\n", - wayscriber::config::CURRENT_CONFIG_REVISION + config::CURRENT_CONFIG_REVISION ), ); @@ -900,9 +904,9 @@ const LEGACY_REVISION_ZERO_CONFIG: &str = "config_revision = 0\n\n[drawing]\ndef /// A config file of its own, in a directory the test owns: an applied /// migration saves, and a save drops its `.bak` next to the file. fn app_with_config_file(contents: &str) -> (ConfiguratorApp, TempDir, PathBuf) { - let dir = crate::test_temp::tempdir().expect("temporary test directory"); + let dir = test_temp::tempdir().expect("temporary test directory"); let path = dir.path().join("config.toml"); - std::fs::write(&path, contents).expect("write test config"); + fs::write(&path, contents).expect("write test config"); let (mut app, _effects) = ConfiguratorApp::new_app(); load_config_file(&mut app, &path); (app, dir, path) @@ -954,7 +958,7 @@ fn config_setting(contents: &str, key: &str) -> Option { } fn read_config(path: &Path) -> String { - std::fs::read_to_string(path).expect("read the saved config") + fs::read_to_string(path).expect("read the saved config") } /// A file that is valid TOML but holds one value serde cannot map used to @@ -963,9 +967,9 @@ fn read_config(path: &Path) -> String { /// defaults instead of calling the file unparseable, and Save keeps the rest. #[test] fn a_file_with_one_unreadable_value_loads_its_other_settings_and_saves_them_back() { - let dir = crate::test_temp::tempdir().expect("temporary test directory"); + let dir = test_temp::tempdir().expect("temporary test directory"); let path = dir.path().join("config.toml"); - std::fs::write( + fs::write( &path, "# mine\n[drawing]\ndefault_thickness = 7.0\n\n[keybindings]\nundo = \"Ctrl+Z\"\n", ) @@ -1061,7 +1065,7 @@ fn applying_and_saving_writes_the_reviewed_fields_the_revision_and_a_backup() { let backup = save_draft(&mut app).expect("the save creates a backup"); assert_eq!( - std::fs::read_to_string(&backup).expect("read the backup"), + fs::read_to_string(&backup).expect("read the backup"), LEGACY_REVISION_ZERO_CONFIG, "the backup holds the file as it was before the migration" ); @@ -1332,11 +1336,11 @@ fn a_fully_customized_apply_records_the_revision_without_reoffering() { fn dismissal_follows_the_file_not_the_path_across_a_retarget() { use std::os::unix::fs::symlink; - let dir = crate::test_temp::tempdir().expect("temporary test directory"); + let dir = test_temp::tempdir().expect("temporary test directory"); let first = dir.path().join("profile-a.toml"); let second = dir.path().join("profile-b.toml"); - std::fs::write(&first, LEGACY_REVISION_ZERO_CONFIG).expect("write the first profile"); - std::fs::write(&second, LEGACY_REVISION_ZERO_CONFIG).expect("write the second profile"); + fs::write(&first, LEGACY_REVISION_ZERO_CONFIG).expect("write the first profile"); + fs::write(&second, LEGACY_REVISION_ZERO_CONFIG).expect("write the second profile"); let link = dir.path().join("config.toml"); symlink(&first, &link).expect("link the config path at the first profile"); @@ -1353,7 +1357,7 @@ fn dismissal_follows_the_file_not_the_path_across_a_retarget() { "the same file reloaded is not the user asking again" ); - std::fs::remove_file(&link).expect("unlink the config path"); + fs::remove_file(&link).expect("unlink the config path"); symlink(&second, &link).expect("retarget the config path at the second profile"); load_config_file(&mut app, &link); @@ -1380,7 +1384,7 @@ fn an_empty_or_missing_file_never_offers_a_migration() { let (app, _dir, _path) = app_with_config_file(""); assert!(app.pending_migration().is_none()); - let dir = crate::test_temp::tempdir().expect("temporary test directory"); + let dir = test_temp::tempdir().expect("temporary test directory"); let (mut app, _effects) = ConfiguratorApp::new_app(); load_config_file(&mut app, &dir.path().join("missing.toml")); diff --git a/configurator/src/app/update/config/tests/language.rs b/configurator/src/app/update/config/tests/language.rs index 17e4de1c1..54cf057fb 100644 --- a/configurator/src/app/update/config/tests/language.rs +++ b/configurator/src/app/update/config/tests/language.rs @@ -5,9 +5,13 @@ //! it selects reaches no other test. The override and locale variables are //! cleared there, so `auto` resolves to English. +use std::fs; + use wayscriber::domain::Language; use wayscriber::i18n; +use crate::test_env; + use super::{app_with_config_file, load_config_file, save_draft, status_contains}; use crate::app::effects::Effect; use crate::models::LanguageOption; @@ -17,7 +21,7 @@ const CHINESE_FILE: &str = "[ui]\nlanguage = \"zh-CN\"\n"; #[test] fn the_first_load_applies_the_saved_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::the_first_load_applies_the_saved_language" @@ -35,7 +39,7 @@ fn the_first_load_applies_the_saved_language() { #[test] fn selecting_a_language_previews_it_without_writing() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::selecting_a_language_previews_it_without_writing" @@ -55,7 +59,7 @@ fn selecting_a_language_previews_it_without_writing() { #[test] fn a_successful_reload_applies_the_loaded_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::a_successful_reload_applies_the_loaded_language" @@ -78,7 +82,7 @@ fn a_successful_reload_applies_the_loaded_language() { #[test] fn a_failed_reload_keeps_the_preview() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!(module_path!(), "::a_failed_reload_keeps_the_preview"), || { let (mut app, _dir, _path) = app_with_config_file(ENGLISH_FILE); @@ -97,7 +101,7 @@ fn a_failed_reload_keeps_the_preview() { #[test] fn confirmed_defaults_apply_the_default_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::confirmed_defaults_apply_the_default_language" @@ -122,7 +126,7 @@ fn confirmed_defaults_apply_the_default_language() { #[test] fn a_canceled_defaults_request_keeps_the_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::a_canceled_defaults_request_keeps_the_language" @@ -140,7 +144,7 @@ fn a_canceled_defaults_request_keeps_the_language() { #[test] fn a_finished_save_applies_the_saved_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::a_finished_save_applies_the_saved_language" @@ -162,7 +166,7 @@ fn a_finished_save_applies_the_saved_language() { #[test] fn canceling_the_leave_prompt_keeps_the_preview() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::canceling_the_leave_prompt_keeps_the_preview" @@ -183,7 +187,7 @@ fn canceling_the_leave_prompt_keeps_the_preview() { #[test] fn discarding_to_reload_applies_the_document_that_loads() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::discarding_to_reload_applies_the_document_that_loads" @@ -196,7 +200,7 @@ fn discarding_to_reload_applies_the_document_that_loads() { let effects = app.discard_before_leave(); assert!(matches!(effects.as_slice(), [Effect::LoadConfig])); // Another editor saved in the meantime: what loads is what counts. - std::fs::write(&path, CHINESE_FILE).expect("rewrite config"); + fs::write(&path, CHINESE_FILE).expect("rewrite config"); load_config_file(&mut app, &path); assert_eq!(app.draft.ui_language, LanguageOption::SimplifiedChinese); diff --git a/configurator/src/app/update/daemon.rs b/configurator/src/app/update/daemon.rs index 3ba45e81c..e2757d24f 100644 --- a/configurator/src/app/update/daemon.rs +++ b/configurator/src/app/update/daemon.rs @@ -194,7 +194,7 @@ mod tests { }] )); let _ = app.handle_daemon_action_completed(Ok(DaemonActionResult { - status: new_status.clone(), + status: new_status, message: "refresh complete".to_string(), partial_failure: false, })); diff --git a/configurator/src/app/update/fields/drawing.rs b/configurator/src/app/update/fields/drawing.rs index 8497834dd..c5c819c7a 100644 --- a/configurator/src/app/update/fields/drawing.rs +++ b/configurator/src/app/update/fields/drawing.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use crate::models::{ ArrowStyleOption, ColorMode, ColorPickerId, DragColorOption, DragMouseButton, DragToolField, DragToolOption, EraserModeOption, NamedColorOption, @@ -108,9 +110,9 @@ impl ConfiguratorApp { self.status = StatusMessage::idle(); if self.draft.drawing_quick_colors.remove_entry(index) { self.remap_quick_color_pickers(|picker_index| match picker_index.cmp(&index) { - std::cmp::Ordering::Less => Some(picker_index), - std::cmp::Ordering::Equal => None, - std::cmp::Ordering::Greater => picker_index.checked_sub(1), + Ordering::Less => Some(picker_index), + Ordering::Equal => None, + Ordering::Greater => picker_index.checked_sub(1), }); self.refresh_dirty_flag(); } @@ -178,7 +180,7 @@ impl ConfiguratorApp { pub(in crate::app::update) fn handle_font_cycle_changed( &mut self, index: usize, - family: String, + family: &str, ) -> Vec { match self.draft.drawing_font_cycle.set(index, family) { Ok(changed) => { diff --git a/configurator/src/app/update/fields/tests.rs b/configurator/src/app/update/fields/tests.rs index 3ad1cb6d9..98e25eadf 100644 --- a/configurator/src/app/update/fields/tests.rs +++ b/configurator/src/app/update/fields/tests.rs @@ -1,3 +1,5 @@ +use wayscriber::draw; + use super::*; use wayscriber::config::{ColorSpec, Config}; @@ -164,7 +166,7 @@ fn choosing_a_duplicate_font_keeps_the_list_and_explains_the_rejection() { config.drawing.font_cycle = vec!["Sans".to_string(), "Serif".to_string()]; app.draft = ConfigDraft::from_config(&config); - let _ = app.handle_font_cycle_changed(1, "sans".to_string()); + let _ = app.handle_font_cycle_changed(1, "sans"); assert_eq!(app.draft.drawing_font_cycle.entries(), ["Sans", "Serif"]); let feedback = app @@ -179,7 +181,7 @@ fn choosing_a_duplicate_font_keeps_the_list_and_explains_the_rejection() { fn add_font_never_creates_a_blank_or_duplicate_when_the_catalog_is_exhausted() { let (mut app, _effects) = ConfiguratorApp::new_app(); let mut config = Config::default(); - config.drawing.font_cycle = wayscriber::draw::system_font_families().to_vec(); + config.drawing.font_cycle = draw::system_font_families().to_vec(); app.draft = ConfigDraft::from_config(&config); let before = app.draft.drawing_font_cycle.entries().to_vec(); diff --git a/configurator/src/app/update/mod.rs b/configurator/src/app/update/mod.rs index d2a5f5c4d..459cbb870 100644 --- a/configurator/src/app/update/mod.rs +++ b/configurator/src/app/update/mod.rs @@ -134,7 +134,7 @@ impl ConfiguratorApp { self.handle_session_catalog_clear_confirmed(id) } Message::SessionCatalogClearCanceled(id) => { - self.handle_session_catalog_clear_canceled(id) + self.handle_session_catalog_clear_canceled(&id) } Message::SearchChanged(value) => self.handle_search_changed(value), Message::SearchCleared => self.handle_search_cleared(), @@ -180,7 +180,7 @@ impl ConfiguratorApp { Message::FontCycleRemoved(index) => self.handle_font_cycle_removed(index), Message::FontCycleMoved(index, delta) => self.handle_font_cycle_moved(index, delta), Message::FontCycleChanged(index, family) => { - self.handle_font_cycle_changed(index, family) + self.handle_font_cycle_changed(index, &family) } Message::QuickColorModeChanged(index, mode) => { self.handle_quick_color_mode_changed(index, mode) @@ -344,7 +344,7 @@ impl ConfiguratorApp { self.handle_shortcut_sequence_remove_last_step() } Message::ShortcutRemoved(field, binding) => { - self.handle_shortcut_removed(field, binding) + self.handle_shortcut_removed(field, &binding) } Message::ShortcutResetRequested(field) => self.handle_shortcut_reset_requested(field), Message::ShortcutTextEditStarted(field) => { diff --git a/configurator/src/app/update/presets.rs b/configurator/src/app/update/presets.rs index 688a6e416..1cf36468e 100644 --- a/configurator/src/app/update/presets.rs +++ b/configurator/src/app/update/presets.rs @@ -1,8 +1,8 @@ use wayscriber::config::PRESET_SLOTS_MAX; use crate::models::{ - ColorMode, NamedColorOption, PresetEraserKindOption, PresetEraserModeOption, PresetTextField, - PresetToggleField, + ColorMode, NamedColorOption, OverrideOption, PresetEraserKindOption, PresetEraserModeOption, + PresetTextField, PresetToggleField, ToolOption, }; use super::super::effects::Effect; @@ -73,7 +73,7 @@ impl ConfiguratorApp { pub(super) fn handle_preset_tool_changed( &mut self, slot_index: usize, - tool: crate::models::ToolOption, + tool: ToolOption, ) -> Vec { self.status = StatusMessage::idle(); if let Some(slot) = self.draft.presets.slot_mut(slot_index) { @@ -167,7 +167,7 @@ impl ConfiguratorApp { &mut self, slot_index: usize, field: PresetToggleField, - value: crate::models::OverrideOption, + value: OverrideOption, ) -> Vec { self.status = StatusMessage::idle(); if let Some(slot) = self.draft.presets.slot_mut(slot_index) { diff --git a/configurator/src/app/update/render_profiles.rs b/configurator/src/app/update/render_profiles.rs index 171c23794..1be960f46 100644 --- a/configurator/src/app/update/render_profiles.rs +++ b/configurator/src/app/update/render_profiles.rs @@ -1,3 +1,5 @@ +use wayscriber::render_profiles; + use crate::models::{ ColorPickerId, RenderProfileExportOption, RenderProfileMappingDraft, RenderProfileMappingSide, RenderProfileTextField, @@ -57,7 +59,7 @@ impl ConfiguratorApp { if let Some(profile) = self.draft.render_profiles.profiles.get_mut(index) { match field { RenderProfileTextField::Id => { - let next_id = wayscriber::render_profiles::normalize_profile_id(&value); + let next_id = render_profiles::normalize_profile_id(&value); profile.id = value; if self.draft.render_profiles.active == old_id { self.draft.render_profiles.active = next_id.clone(); diff --git a/configurator/src/app/update/session_catalog.rs b/configurator/src/app/update/session_catalog.rs index fcb217020..9f6f60321 100644 --- a/configurator/src/app/update/session_catalog.rs +++ b/configurator/src/app/update/session_catalog.rs @@ -228,8 +228,8 @@ impl ConfiguratorApp { vec![Effect::ClearSessionEntry { id }] } - pub(super) fn handle_session_catalog_clear_canceled(&mut self, id: String) -> Vec { - if self.pending_session_clear_id() != Some(id.as_str()) { + pub(super) fn handle_session_catalog_clear_canceled(&mut self, id: &str) -> Vec { + if self.pending_session_clear_id() != Some(id) { return Vec::new(); } diff --git a/configurator/src/app/update/session_catalog/tests.rs b/configurator/src/app/update/session_catalog/tests.rs index 10ed8fc7b..ed79cfe26 100644 --- a/configurator/src/app/update/session_catalog/tests.rs +++ b/configurator/src/app/update/session_catalog/tests.rs @@ -1,5 +1,12 @@ use std::path::PathBuf; +use wayscriber::i18n; + +use crate::models::DaemonRuntimeStatus; +use crate::models::session::SessionArtifactSummary; +use crate::test_env::EnvGuard; +use crate::{test_env, test_temp}; + use super::*; use crate::models::{ DesktopEnvironment, LightShortcutApplyCapability, SessionCatalogState, ShortcutApplyCapability, @@ -18,7 +25,7 @@ fn catalog_item(id: &str, display_name: &str) -> SessionCatalogItem { created_at_millis: 1_767_261_600_000, last_opened_at_millis: None, last_saved_at_millis: None, - artifacts: crate::models::session::SessionArtifactSummary { + artifacts: SessionArtifactSummary { primary_exists: false, backup_exists: false, recovery_exists: false, @@ -30,8 +37,8 @@ fn catalog_item(id: &str, display_name: &str) -> SessionCatalogItem { } } -fn inactive_daemon_status() -> crate::models::DaemonRuntimeStatus { - crate::models::DaemonRuntimeStatus { +fn inactive_daemon_status() -> DaemonRuntimeStatus { + DaemonRuntimeStatus { desktop: DesktopEnvironment::Unknown, shortcut_backend: ShortcutBackend::Manual, shortcut_apply_capability: ShortcutApplyCapability::Manual, @@ -124,14 +131,14 @@ fn catalog_load_preserves_a_defaults_confirmation() { #[test] fn a_finished_load_clears_its_notice_in_every_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::a_finished_load_clears_its_notice_in_every_language" ), || { for language in [Language::English, Language::SimplifiedChinese] { - wayscriber::i18n::set_language(language); + i18n::set_language(language); let (mut app, _effects) = ConfiguratorApp::new_app(); let effects = app.handle_session_catalog_refresh_requested(); @@ -148,7 +155,7 @@ fn a_finished_load_clears_its_notice_in_every_language() { #[test] fn the_loading_notice_is_drawn_in_the_current_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::the_loading_notice_is_drawn_in_the_current_language" @@ -158,7 +165,7 @@ fn the_loading_notice_is_drawn_in_the_current_language() { let _ = app.handle_session_catalog_refresh_requested(); assert_eq!(app.status.text(), Some("Loading sessions...")); - wayscriber::i18n::set_language(Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); assert_eq!(app.status.text(), Some("正在加载会话...")); }, @@ -191,8 +198,8 @@ fn a_failed_load_stops_loading_and_shows_the_error() { #[test] fn duplicate_request_blocks_without_daemon_status() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let (mut app, _effects) = ConfiguratorApp::new_app(); app.session_catalog = SessionCatalogState::loading(); app.session_catalog @@ -208,8 +215,8 @@ fn duplicate_request_blocks_without_daemon_status() { #[test] fn duplicate_request_sets_busy_when_safe() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let (mut app, _effects) = ConfiguratorApp::new_app(); app.session_catalog = SessionCatalogState::loading(); app.session_catalog @@ -228,8 +235,8 @@ fn duplicate_request_sets_busy_when_safe() { #[test] fn move_request_blocks_without_daemon_status() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let (mut app, _effects) = ConfiguratorApp::new_app(); app.session_catalog = SessionCatalogState::loading(); app.session_catalog @@ -245,8 +252,8 @@ fn move_request_blocks_without_daemon_status() { #[test] fn move_request_sets_busy_when_safe() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let (mut app, _effects) = ConfiguratorApp::new_app(); app.session_catalog = SessionCatalogState::loading(); app.session_catalog @@ -265,8 +272,8 @@ fn move_request_sets_busy_when_safe() { #[test] fn clear_request_blocks_without_daemon_status() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let (mut app, _effects) = ConfiguratorApp::new_app(); app.session_catalog = SessionCatalogState::loading(); app.session_catalog @@ -281,8 +288,8 @@ fn clear_request_blocks_without_daemon_status() { #[test] fn clear_tool_state_request_blocks_without_daemon_status() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let (mut app, _effects) = ConfiguratorApp::new_app(); app.session_catalog = SessionCatalogState::loading(); app.session_catalog @@ -298,8 +305,8 @@ fn clear_tool_state_request_blocks_without_daemon_status() { #[test] fn clear_tool_state_request_sets_busy_when_safe() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let (mut app, _effects) = ConfiguratorApp::new_app(); app.session_catalog = SessionCatalogState::loading(); app.session_catalog @@ -319,8 +326,8 @@ fn clear_tool_state_request_sets_busy_when_safe() { #[test] fn clear_request_sets_pending_confirmation_when_safe() { - let temp = crate::test_temp::tempdir().unwrap(); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().unwrap(); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let (mut app, _effects) = ConfiguratorApp::new_app(); app.session_catalog = SessionCatalogState::loading(); app.session_catalog @@ -391,7 +398,7 @@ fn clear_canceled_disarms_and_clears_its_confirmation_status() { app.daemon.status = Some(inactive_daemon_status()); let _ = app.handle_session_catalog_clear_requested("s-1".to_string()); - let effects = app.handle_session_catalog_clear_canceled("s-1".to_string()); + let effects = app.handle_session_catalog_clear_canceled("s-1"); assert!(effects.is_empty()); assert!(app.pending_session_clear_id().is_none()); @@ -424,7 +431,7 @@ fn clear_canceled_preserves_status_that_replaced_its_confirmation() { let _ = app.handle_session_catalog_clear_requested("s-1".to_string()); app.status = StatusMessage::error("A newer session operation failed"); - let _ = app.handle_session_catalog_clear_canceled("s-1".to_string()); + let _ = app.handle_session_catalog_clear_canceled("s-1"); assert!(app.pending_session_clear_id().is_none()); assert!(matches!(app.status, StatusMessage::Error(_))); @@ -440,7 +447,7 @@ fn stray_clear_cancel_preserves_unrelated_status() { app.pending_confirmation = None; app.status = StatusMessage::success("A completed operation"); - let _ = app.handle_session_catalog_clear_canceled("s-1".to_string()); + let _ = app.handle_session_catalog_clear_canceled("s-1"); assert!(matches!(app.status, StatusMessage::Success(_))); assert!(status_contains(&app.status, "completed operation")); @@ -458,7 +465,7 @@ fn stale_clear_cancel_does_not_disarm_a_newer_confirmation() { let _ = app.handle_session_catalog_clear_requested("s-1".to_string()); let _ = app.handle_session_catalog_clear_requested("s-2".to_string()); - let effects = app.handle_session_catalog_clear_canceled("s-1".to_string()); + let effects = app.handle_session_catalog_clear_canceled("s-1"); assert!(effects.is_empty()); assert_eq!(app.pending_session_clear_id(), Some("s-2")); @@ -470,8 +477,8 @@ fn stale_clear_cancel_does_not_disarm_a_newer_confirmation() { /// the busy flag, the status, the effect — is untouched. #[test] fn clear_confirmed_consumes_the_pending_confirmation() { - let temp = crate::test_temp::tempdir().expect("temporary test directory"); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().expect("temporary test directory"); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let (mut app, _effects) = ConfiguratorApp::new_app(); app.session_catalog = SessionCatalogState::loading(); app.session_catalog @@ -494,8 +501,8 @@ fn clear_confirmed_consumes_the_pending_confirmation() { /// stale one from a dialog — must not start it again. #[test] fn clear_confirmed_twice_starts_only_one_clear() { - let temp = crate::test_temp::tempdir().expect("temporary test directory"); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().expect("temporary test directory"); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let (mut app, _effects) = ConfiguratorApp::new_app(); app.session_catalog = SessionCatalogState::loading(); app.session_catalog @@ -516,8 +523,8 @@ fn clear_confirmed_twice_starts_only_one_clear() { /// starts a clear nor consumes the confirmation that is armed. #[test] fn clear_confirmed_for_another_row_leaves_the_pending_one_armed() { - let temp = crate::test_temp::tempdir().expect("temporary test directory"); - let _env = crate::test_env::EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); + let temp = test_temp::tempdir().expect("temporary test directory"); + let _env = EnvGuard::set(&[(XDG_RUNTIME_DIR_ENV, temp.path().as_os_str())]); let (mut app, _effects) = ConfiguratorApp::new_app(); app.session_catalog = SessionCatalogState::loading(); app.session_catalog.replace_items(vec![ diff --git a/configurator/src/app/update/shortcuts.rs b/configurator/src/app/update/shortcuts.rs index e26f973a5..3ee84f1d4 100644 --- a/configurator/src/app/update/shortcuts.rs +++ b/configurator/src/app/update/shortcuts.rs @@ -6,7 +6,7 @@ mod tests; use wayscriber::config::Shortcut; use wayscriber::i18n::tr; -use crate::app::search::keybinding_row_visible; +use crate::app::search::{AppSearchSummary, keybinding_row_visible}; use crate::app::state::{ConfirmationPrompt, PendingConfirmation}; use crate::models::keybindings::{ AppendOutcome, KeyboardModifiers, RecordedDevice, RecordedKeyboard, RecorderDeviceKind, @@ -16,7 +16,7 @@ use crate::models::keybindings::{ other_claimants, parse_keybindings, remove_binding, reset_field, reset_fields, text_conflicts_for, }; -use crate::models::{KeybindingField, keybinding_tab}; +use crate::models::{KeybindingField, PendingShortcutConflict, keybinding_tab}; use super::super::effects::Effect; use super::super::state::{ConfiguratorApp, StatusMessage}; @@ -127,12 +127,12 @@ impl ConfiguratorApp { pub(super) fn handle_shortcut_removed( &mut self, field: KeybindingField, - binding: Shortcut, + binding: &Shortcut, ) -> Vec { if self.shortcuts.conflict().is_some() { return Vec::new(); } - match remove_binding(&mut self.draft.keybindings, field, &binding) { + match remove_binding(&mut self.draft.keybindings, field, binding) { Ok(()) => { self.status = StatusMessage::idle(); self.refresh_dirty_flag(); @@ -197,7 +197,7 @@ impl ConfiguratorApp { } self.shortcuts.set_editor(None); self.shortcuts - .set_conflict(Some(crate::models::PendingShortcutConflict::Text { + .set_conflict(Some(PendingShortcutConflict::Text { target: editor.field, new_value: editor.text, conflicts, @@ -223,12 +223,12 @@ impl ConfiguratorApp { return Vec::new(); }; let result = match pending { - crate::models::PendingShortcutConflict::Recorded { + PendingShortcutConflict::Recorded { target, binding, claimants, } => apply_recorded_replace(&mut self.draft.keybindings, target, &binding, &claimants), - crate::models::PendingShortcutConflict::Text { + PendingShortcutConflict::Text { target, new_value, conflicts, @@ -274,7 +274,7 @@ impl ConfiguratorApp { pub(crate) fn prepare_shortcut_manager_refresh( &self, - search: &crate::app::search::AppSearchSummary, + search: &AppSearchSummary, ) -> (ShortcutManagerSummary, Vec) { let summary = self.shortcut_manager_summary(); let fields = self.visible_keybinding_fields_with(&summary, search); @@ -284,7 +284,7 @@ impl ConfiguratorApp { fn visible_keybinding_fields_with( &self, summary: &ShortcutManagerSummary, - search: &crate::app::search::AppSearchSummary, + search: &AppSearchSummary, ) -> Vec { let scope = (!self.keybindings_show_all).then_some(self.active_keybindings_tab); summary.visible_fields(self.shortcut_filter, self.shortcut_sort, scope, |field| { @@ -416,13 +416,12 @@ impl ConfiguratorApp { match next_review_conflict(&self.draft.keybindings) { Some((field, binding, claimants)) => { self.select_keybinding_field(field); - self.shortcuts.set_conflict(Some( - crate::models::PendingShortcutConflict::Recorded { + self.shortcuts + .set_conflict(Some(PendingShortcutConflict::Recorded { target: field, binding, claimants, - }, - )); + })); self.status = StatusMessage::info(tr("Review the next conflicting shortcut.")); } None => { @@ -440,7 +439,7 @@ impl ConfiguratorApp { let claimants = other_claimants(&self.draft.keybindings, field, &binding); if !claimants.is_empty() { self.shortcuts - .set_conflict(Some(crate::models::PendingShortcutConflict::Recorded { + .set_conflict(Some(PendingShortcutConflict::Recorded { target: field, binding, claimants, diff --git a/configurator/src/app/update/shortcuts/tests.rs b/configurator/src/app/update/shortcuts/tests.rs index d7cc82632..be5f83fb6 100644 --- a/configurator/src/app/update/shortcuts/tests.rs +++ b/configurator/src/app/update/shortcuts/tests.rs @@ -1,3 +1,9 @@ +use std::fs; +use std::path::PathBuf; + +#[cfg(feature = "tablet-input")] +use wayscriber::config::Action; + use crate::models::{ShortcutManagerFilter, ShortcutManagerSort}; use wayscriber::config::{CURRENT_CONFIG_REVISION, ConfigDocument, Shortcut}; @@ -5,18 +11,20 @@ use crate::app::effects::Effect; use crate::app::state::{ConfiguratorApp, PendingConfirmation, StatusMessage}; use crate::models::keybindings::keyval; use crate::models::{ - KeybindingField, KeyboardModifiers, RecorderDeviceKind, SearchQuery, keybinding_tab, + KeybindingField, KeyboardModifiers, PendingShortcutConflict, RecorderDeviceKind, SearchQuery, + keybinding_tab, }; +use crate::test_temp; use crate::test_temp::TempDir; fn status_contains(status: &StatusMessage, needle: &str) -> bool { status.text().is_some_and(|text| text.contains(needle)) } -fn load_config(app: &mut ConfiguratorApp, contents: &str) -> (TempDir, std::path::PathBuf) { - let dir = crate::test_temp::tempdir().expect("temporary test directory"); +fn load_config(app: &mut ConfiguratorApp, contents: &str) -> (TempDir, PathBuf) { + let dir = test_temp::tempdir().expect("temporary test directory"); let path = dir.path().join("config.toml"); - std::fs::write(&path, contents).expect("write test config"); + fs::write(&path, contents).expect("write test config"); let document = ConfigDocument::load_from_path(&path).expect("load test config document"); let _ = app.handle_config_loaded(Ok((Box::new(document), None))); (dir, path) @@ -125,7 +133,7 @@ fn recording_reset_and_removal_dirty_without_saving() { ); let effects = app.handle_shortcut_removed( KeybindingField::ToggleFloatingBadge, - Shortcut::parse("F5").expect("parses"), + &Shortcut::parse("F5").expect("parses"), ); assert!(effects.is_empty()); assert_eq!( @@ -201,7 +209,7 @@ fn save_after_confirmed_replacement_writes_only_the_intended_fields() { let _ = app.handle_shortcut_conflict_replace_confirmed(); save_draft(&mut app); - let contents = std::fs::read_to_string(&path).expect("read saved config"); + let contents = fs::read_to_string(&path).expect("read saved config"); assert!(contents.contains("# keep me"), "{contents}"); assert!(contents.contains("unknown_future = true"), "{contents}"); let revision = format!("config_revision = {CURRENT_CONFIG_REVISION}"); @@ -307,7 +315,7 @@ fn resetting_an_explicit_binding_stays_authored_through_save_reload() { assert!(app.draft.keybindings.is_authored(KeybindingField::Undo)); save_draft(&mut app); - let contents = std::fs::read_to_string(path).expect("read saved config"); + let contents = fs::read_to_string(path).expect("read saved config"); assert_eq!( config_setting(&contents, "undo").as_deref(), Some("undo = [\"Ctrl+Z\"]") @@ -374,7 +382,7 @@ fn recording_stylus_primary_prompts_to_move_the_default_legacy_barrel() { let (mut app, _effects) = ConfiguratorApp::new_app(); assert_eq!( app.draft.keybindings.legacy_tablet.stylus_primary, - Some(wayscriber::config::Action::ToggleRadialMenu) + Some(Action::ToggleRadialMenu) ); let _ = app.handle_shortcut_recording_started(KeybindingField::Undo); let _ = app.handle_shortcut_recorder_button( @@ -688,7 +696,7 @@ fn conflict_review_queue_arms_the_next_conflict_after_replace() { assert!(app.shortcuts.review); assert!(app.shortcuts.conflict().is_some()); let first_target = match app.shortcuts.conflict() { - Some(crate::models::PendingShortcutConflict::Recorded { target, .. }) => *target, + Some(PendingShortcutConflict::Recorded { target, .. }) => *target, other => panic!("expected a recorded conflict, got {other:?}"), }; @@ -702,7 +710,7 @@ fn conflict_review_queue_arms_the_next_conflict_after_replace() { .conflict() .expect("the next conflict should be armed"); match second { - crate::models::PendingShortcutConflict::Recorded { target, .. } => { + PendingShortcutConflict::Recorded { target, .. } => { assert_ne!(*target, first_target); } other => panic!("expected a recorded conflict, got {other:?}"), @@ -737,7 +745,7 @@ fn jump_to_conflict_selects_the_other_claimant() { let jump = app .shortcuts .conflict() - .and_then(crate::models::PendingShortcutConflict::jump_field) + .and_then(PendingShortcutConflict::jump_field) .expect("a claimant to jump to"); let _ = app.handle_shortcut_manager_jump_to(jump); assert_eq!(app.selected_keybinding, Some(jump)); @@ -765,7 +773,8 @@ fn prepared_manager_refresh_reuses_analysis_for_filters() { #[ignore = "release measurement: cargo test --release shortcut_refresh_cost -- --ignored --nocapture"] fn shortcut_refresh_cost() { use crate::models::keybindings::take_parse_calls; - use std::{hint::black_box, time::Instant}; + use std::hint::black_box; + use std::time::Instant; let (mut app, _) = ConfiguratorApp::new_app(); for query in ["", "tool", "Ctrl"] { app.handle_search_changed(query.into()); diff --git a/configurator/src/main.rs b/configurator/src/main.rs index e659c6d44..e784d79a3 100644 --- a/configurator/src/main.rs +++ b/configurator/src/main.rs @@ -6,10 +6,12 @@ mod test_env; #[cfg(test)] mod test_temp; +use std::env; + use models::{StartupRequest, startup_usage}; fn main() { - let startup = StartupRequest::from_args(std::env::args_os()); + let startup = StartupRequest::from_args(env::args_os()); if startup.wants_help() { print!("{}", startup_usage()); return; diff --git a/configurator/src/messages.rs b/configurator/src/messages.rs index 6ea638a73..284a33252 100644 --- a/configurator/src/messages.rs +++ b/configurator/src/messages.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use wayscriber::config::{ConfigDocument, Shortcut, ToolbarItemId, ToolbarItemOrderGroup}; +use wayscriber::domain::BoardGridKind; use crate::models::{ ArrowStyleOption, BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorMode, @@ -150,7 +151,7 @@ pub enum Message { BoardsCollapseToggled(usize), BoardsDefaultChanged(String), BoardsItemTextChanged(usize, BoardItemTextField, String), - BoardsGridKindChanged(usize, wayscriber::domain::BoardGridKind), + BoardsGridKindChanged(usize, BoardGridKind), BoardsBackgroundKindChanged(usize, BoardBackgroundOption), BoardsBackgroundColorChanged(usize, usize, String), BoardsDefaultPenEnabledChanged(usize, bool), diff --git a/configurator/src/models/color/input.rs b/configurator/src/models/color/input.rs index ee63865ae..344d95eb2 100644 --- a/configurator/src/models/color/input.rs +++ b/configurator/src/models/color/input.rs @@ -2,6 +2,7 @@ use wayscriber::i18n::{tr, trf}; use wayscriber::config::enums::ColorSpec; use wayscriber::draw::Color; +use wayscriber::input::state; use wayscriber::util::{ConfigHexColorError, name_to_color, parse_config_hex_color}; use super::super::error::FormError; @@ -22,7 +23,7 @@ impl ColorInput { mode: ColorMode::Named, name: name.clone(), rgb: color_to_rgb_strings(spec.to_color()), - selected_named: NamedColorOption::from_str(name), + selected_named: NamedColorOption::from_name(name), }, ColorSpec::Rgb([r, g, b]) => Self { mode: ColorMode::Rgb, @@ -35,7 +36,7 @@ impl ColorInput { // silently flattened to opaque by the three RGB boxes. ColorSpec::Rgba(_) => Self { mode: ColorMode::Named, - name: wayscriber::input::state::color_to_hex(spec.to_color()), + name: state::color_to_hex(spec.to_color()), rgb: color_to_rgb_strings(spec.to_color()), selected_named: NamedColorOption::Custom, }, @@ -116,7 +117,7 @@ impl ColorInput { } pub fn update_named_from_current(&mut self) { - self.selected_named = NamedColorOption::from_str(&self.name); + self.selected_named = NamedColorOption::from_name(&self.name); } pub fn sync_rgb_from_preview(&mut self) -> bool { diff --git a/configurator/src/models/color/named.rs b/configurator/src/models/color/named.rs index ae04cac5a..bb09e56f3 100644 --- a/configurator/src/models/color/named.rs +++ b/configurator/src/models/color/named.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -62,7 +65,7 @@ impl NamedColorOption { } } - pub fn from_str(value: &str) -> Self { + pub fn from_name(value: &str) -> Self { match value.trim().to_lowercase().as_str() { "red" => Self::Red, "green" => Self::Green, @@ -77,8 +80,8 @@ impl NamedColorOption { } } -impl std::fmt::Display for NamedColorOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for NamedColorOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/config/boards.rs b/configurator/src/models/config/boards.rs index 9909c112e..0f1b1ea04 100644 --- a/configurator/src/models/config/boards.rs +++ b/configurator/src/models/config/boards.rs @@ -1,6 +1,10 @@ mod mapping; mod validation; +use std::fmt; +use std::fmt::{Display, Formatter}; + +use wayscriber::domain::BoardGridKind; use wayscriber::i18n::tr; use super::super::color::ColorTripletInput; @@ -24,8 +28,8 @@ impl BoardBackgroundOption { } } -impl std::fmt::Display for BoardBackgroundOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for BoardBackgroundOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -56,7 +60,7 @@ pub struct BoardItemDraft { pub name: String, pub background_kind: BoardBackgroundOption, pub background_color: ColorTripletInput, - pub grid_kind: wayscriber::domain::BoardGridKind, + pub grid_kind: BoardGridKind, pub grid_spacing: String, pub default_pen_color: OptionalTripletInput, pub auto_adjust_pen: bool, diff --git a/configurator/src/models/config/boards/mapping.rs b/configurator/src/models/config/boards/mapping.rs index 3883da3c8..1599fb98b 100644 --- a/configurator/src/models/config/boards/mapping.rs +++ b/configurator/src/models/config/boards/mapping.rs @@ -3,8 +3,10 @@ use wayscriber::i18n::tr; use std::collections::HashSet; use wayscriber::config::{ - BoardBackgroundConfig, BoardColorConfig, BoardItemConfig, BoardsConfig, Config, + BoardBackgroundConfig, BoardColorConfig, BoardGridConfig, BoardItemConfig, BoardsConfig, Config, }; +use wayscriber::domain; +use wayscriber::domain::BoardGridKind; use crate::models::color::ColorTripletInput; use crate::models::error::FormError; @@ -103,8 +105,8 @@ impl BoardItemDraft { let spacing = match self.grid_spacing.trim().parse::() { Ok(value) - if (i64::from(wayscriber::domain::BOARD_GRID_MIN_SPACING) - ..=i64::from(wayscriber::domain::BOARD_GRID_MAX_SPACING)) + if (i64::from(domain::BOARD_GRID_MIN_SPACING) + ..=i64::from(domain::BOARD_GRID_MAX_SPACING)) .contains(&value) => { value @@ -117,9 +119,9 @@ impl BoardItemDraft { return None; } }; - let grid = wayscriber::config::BoardGridConfig { + let grid = BoardGridConfig { kind: if background.is_transparent() { - wayscriber::domain::BoardGridKind::None + BoardGridKind::None } else { self.grid_kind } @@ -230,8 +232,8 @@ impl BoardsDraft { name, background_kind: BoardBackgroundOption::Color, background_color: ColorTripletInput::from([0.992, 0.992, 0.992]), - grid_kind: wayscriber::domain::BoardGridKind::None, - grid_spacing: wayscriber::domain::BOARD_GRID_DEFAULT_SPACING.to_string(), + grid_kind: BoardGridKind::None, + grid_spacing: domain::BOARD_GRID_DEFAULT_SPACING.to_string(), default_pen_color: OptionalTripletInput::from_option( Some(&BoardColorConfig::Rgb([0.0, 0.0, 0.0])), [0.0, 0.0, 0.0], diff --git a/configurator/src/models/config/draft/from_config.rs b/configurator/src/models/config/draft/from_config.rs index 21d056af5..4230ff464 100644 --- a/configurator/src/models/config/draft/from_config.rs +++ b/configurator/src/models/config/draft/from_config.rs @@ -1,5 +1,7 @@ +use wayscriber::config; +use wayscriber::config::{KeybindingAuthorship, ToolbarSectionVisibility}; + use super::super::super::color::{ColorInput, ColorQuadInput}; -use super::super::super::fields::ZoomChipDisplayOption; use super::super::super::fields::{ ArrowStyleOption, EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption, InputHudPositionOption, LanguageOption, PdfFitModeOption, PdfLabelContentModeOption, @@ -7,7 +9,7 @@ use super::super::super::fields::{ PdfTransparentBackgroundOption, PresenterToolBehaviorOption, PresenterToolbarModeOption, ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, ToolOption, ToolbarLayoutModeOption, ToolbarRebindModifierOption, ToolbarStrokeControlsOption, - UiThemeOption, + UiThemeOption, ZoomChipDisplayOption, }; #[cfg(feature = "tablet-input")] use super::super::super::fields::{ @@ -31,7 +33,7 @@ impl ConfigDraft { FontWeightOption::from_value(&config.drawing.font_weight); let drawing_drag_tools = config.drawing.effective_drag_tools(); let mut toolbar_items = config.ui.toolbar.items.clone(); - let mut legacy_toolbar_visibility = wayscriber::config::ToolbarSectionVisibility { + let mut legacy_toolbar_visibility = ToolbarSectionVisibility { show_actions_section: config.ui.toolbar.show_actions_section, show_actions_advanced: config.ui.toolbar.show_actions_advanced, show_zoom_actions: config.ui.toolbar.show_zoom_actions, @@ -48,13 +50,13 @@ impl ConfigDraft { .mode_overrides .for_mode(config.ui.toolbar.layout_mode), ); - wayscriber::config::fold_legacy_section_flags( + config::fold_legacy_section_flags( &legacy_toolbar_visibility, config.ui.toolbar.layout_mode, &config.ui.toolbar.mode_overrides, &mut toolbar_items, ); - let toolbar_visibility = wayscriber::config::resolve_section_visibility( + let toolbar_visibility = config::resolve_section_visibility( config.ui.toolbar.layout_mode, &config.ui.toolbar.mode_overrides, &toolbar_items.resolved(), @@ -311,14 +313,12 @@ impl ConfigDraft { session_persist_history: config.session.persist_history, session_restore_tool_state: config.session.restore_tool_state, session_per_output: config.session.per_output, - session_storage_mode: SessionStorageModeOption::from_mode( - config.session.storage.clone(), - ), + session_storage_mode: SessionStorageModeOption::from_mode(config.session.storage), session_custom_directory: config.session.custom_directory.clone().unwrap_or_default(), session_max_shapes_per_frame: config.session.max_shapes_per_frame.to_string(), session_max_file_size_mb: config.session.max_file_size_mb.to_string(), session_compression: SessionCompressionOption::from_compression( - config.session.compress.clone(), + config.session.compress, ), session_auto_compress_threshold_kb: config .session @@ -373,8 +373,8 @@ impl ConfigDraft { // Built-in and in-memory configs are AllExplicit for // validation. The UI treats them as omitted defaults so // Reset All / Defaults does not badge every row Authored. - wayscriber::config::KeybindingAuthorship::AllExplicit => { - wayscriber::config::KeybindingAuthorship::FromFile(Default::default()) + KeybindingAuthorship::AllExplicit => { + KeybindingAuthorship::FromFile(Default::default()) } from_file => from_file.clone(), }); diff --git a/configurator/src/models/config/draft/mod.rs b/configurator/src/models/config/draft/mod.rs index 5fd39cbb0..9ccdda782 100644 --- a/configurator/src/models/config/draft/mod.rs +++ b/configurator/src/models/config/draft/mod.rs @@ -1,7 +1,6 @@ mod from_config; use super::super::color::{ColorInput, ColorQuadInput}; -use super::super::fields::ZoomChipDisplayOption; use super::super::fields::{ ArrowStyleOption, EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption, InputHudPositionOption, LanguageOption, PdfFitModeOption, PdfLabelContentModeOption, @@ -9,7 +8,7 @@ use super::super::fields::{ PdfTransparentBackgroundOption, PresenterToolBehaviorOption, PresenterToolbarModeOption, ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, ToolOption, ToolbarLayoutModeOption, ToolbarRebindModifierOption, ToolbarStrokeControlsOption, - UiThemeOption, + UiThemeOption, ZoomChipDisplayOption, }; #[cfg(feature = "tablet-input")] use super::super::fields::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption}; diff --git a/configurator/src/models/config/font_cycle.rs b/configurator/src/models/config/font_cycle.rs index 842f4c1a7..e7910a9f8 100644 --- a/configurator/src/models/config/font_cycle.rs +++ b/configurator/src/models/config/font_cycle.rs @@ -131,7 +131,7 @@ impl FontCycleDraft { } /// Set one row's family without allowing a blank or repeated identity. - pub(crate) fn set(&mut self, index: usize, family: String) -> Result { + pub(crate) fn set(&mut self, index: usize, family: &str) -> Result { let Some(current) = self.entries.get(index) else { return Err(FontCycleEditError::MissingEntry); }; @@ -202,12 +202,12 @@ mod tests { let mut draft = draft(&["Sans", "Serif"]); assert_eq!( - draft.set(1, "sans".to_string()), + draft.set(1, "sans"), Err(FontCycleEditError::DuplicateFamily("sans".to_string())) ); assert_eq!(draft.entries(), ["Sans", "Serif"]); - assert_eq!(draft.set(1, "Monospace".to_string()), Ok(true)); + assert_eq!(draft.set(1, "Monospace"), Ok(true)); assert_eq!(draft.entries(), ["Sans", "Monospace"]); } @@ -215,14 +215,8 @@ mod tests { fn blank_and_missing_row_edits_are_rejected() { let mut draft = draft(&["Sans"]); - assert_eq!( - draft.set(0, " ".to_string()), - Err(FontCycleEditError::BlankFamily) - ); - assert_eq!( - draft.set(4, "Serif".to_string()), - Err(FontCycleEditError::MissingEntry) - ); + assert_eq!(draft.set(0, " "), Err(FontCycleEditError::BlankFamily)); + assert_eq!(draft.set(4, "Serif"), Err(FontCycleEditError::MissingEntry)); assert_eq!(draft.entries(), ["Sans"]); } diff --git a/configurator/src/models/config/performance_fields.rs b/configurator/src/models/config/performance_fields.rs index bd95394d0..421506fae 100644 --- a/configurator/src/models/config/performance_fields.rs +++ b/configurator/src/models/config/performance_fields.rs @@ -82,13 +82,17 @@ impl ConfigDraft { #[cfg(test)] mod tests { - use wayscriber::config::{PERFORMANCE_FIELD_METADATA, PerformanceFieldId, ScalarConstraint}; + use wayscriber::config::{ + Config, PERFORMANCE_FIELD_METADATA, PerformanceFieldId, ScalarConstraint, + }; + + use crate::models::{TextField, ToggleField}; use super::*; #[test] fn every_performance_descriptor_routes_through_a_typed_binding() { - let mut draft = ConfigDraft::from_config(&wayscriber::config::Config::default()); + let mut draft = ConfigDraft::from_config(&Config::default()); for metadata in PERFORMANCE_FIELD_METADATA { match metadata.constraint { ScalarConstraint::Boolean => { @@ -106,17 +110,11 @@ mod tests { #[test] fn typed_bindings_update_each_performance_draft_field() { - let mut draft = ConfigDraft::from_config(&wayscriber::config::Config::default()); + let mut draft = ConfigDraft::from_config(&Config::default()); draft.set_performance_choice(PerformanceFieldId::BufferCount, 4); - draft.set_toggle(crate::models::ToggleField::PerformanceVsync, true); - draft.set_text( - crate::models::TextField::PerformanceMaxFpsNoVsync, - "144".to_string(), - ); - draft.set_text( - crate::models::TextField::PerformanceUiAnimationFps, - "60".to_string(), - ); + draft.set_toggle(ToggleField::PerformanceVsync, true); + draft.set_text(TextField::PerformanceMaxFpsNoVsync, "144".to_string()); + draft.set_text(TextField::PerformanceUiAnimationFps, "60".to_string()); assert_eq!(draft.performance_buffer_count, 4); assert!(draft.performance_enable_vsync); @@ -126,7 +124,7 @@ mod tests { #[test] fn conversion_rejects_values_outside_shared_performance_constraints() { - let base = wayscriber::config::Config::default(); + let base = Config::default(); let mut draft = ConfigDraft::from_config(&base); draft.performance_buffer_count = 5; draft.performance_ui_animation_fps = "241".to_string(); diff --git a/configurator/src/models/config/presets.rs b/configurator/src/models/config/presets.rs index fe5117484..35b2c7350 100644 --- a/configurator/src/models/config/presets.rs +++ b/configurator/src/models/config/presets.rs @@ -203,7 +203,7 @@ pub struct PresetsDraft { impl PresetsDraft { pub fn from_config(config: &Config) -> Self { let slots = (1..=PRESET_SLOTS_MAX) - .map(|slot| PresetSlotDraft::from_config(config.presets.get_slot(slot), config)) + .map(|slot| PresetSlotDraft::from_config(config.presets.slot(slot), config)) .collect(); Self { slot_count: config.presets.slot_count, diff --git a/configurator/src/models/config/render_profiles.rs b/configurator/src/models/config/render_profiles.rs index 6a2c13aec..c7826f2c2 100644 --- a/configurator/src/models/config/render_profiles.rs +++ b/configurator/src/models/config/render_profiles.rs @@ -1,3 +1,6 @@ +use std::fmt::{Display, Formatter}; +use std::{fmt, iter}; + use wayscriber::i18n::tr; use wayscriber::config::{ @@ -56,8 +59,8 @@ impl RenderProfileExportOption { } } -impl std::fmt::Display for RenderProfileExportOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for RenderProfileExportOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -70,7 +73,7 @@ pub enum RenderProfileSelectionOption { impl RenderProfileSelectionOption { pub fn list(profile_ids: &[String]) -> Vec { - std::iter::once(Self::Off) + iter::once(Self::Off) .chain(profile_ids.iter().cloned().map(Self::Profile)) .collect() } @@ -84,8 +87,8 @@ impl RenderProfileSelectionOption { } } -impl std::fmt::Display for RenderProfileSelectionOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for RenderProfileSelectionOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::Off => f.write_str("Off"), Self::Profile(id) => f.write_str(id), diff --git a/configurator/src/models/config/setters.rs b/configurator/src/models/config/setters.rs index ff5402b81..9cc383ff1 100644 --- a/configurator/src/models/config/setters.rs +++ b/configurator/src/models/config/setters.rs @@ -1,3 +1,5 @@ +use wayscriber::config; + use super::super::fields::{ DragColorOption, DragMouseButton, DragToolField, DragToolOption, FontStyleOption, FontWeightOption, OverrideOption, QuadField, TextField, ToggleField, ToolbarLayoutModeOption, @@ -19,7 +21,7 @@ impl ConfigDraft { } fn refresh_toolbar_section_visibility(&mut self) { - let visibility = wayscriber::config::resolve_section_visibility( + let visibility = config::resolve_section_visibility( self.ui_toolbar_layout_mode.to_mode(), &self.ui_toolbar_mode_overrides.to_config(), &self.ui_toolbar_items.resolved(), diff --git a/configurator/src/models/config/tests.rs b/configurator/src/models/config/tests.rs index 1dd02da67..44e9ca78f 100644 --- a/configurator/src/models/config/tests.rs +++ b/configurator/src/models/config/tests.rs @@ -1,3 +1,17 @@ +use crate::models::KeybindingField; +use crate::{test_env, test_temp}; + +use wayscriber::config::{ + BoardGridConfig, BoardGridKindConfig, BoardsConfig, RegionPicker, ToolbarRebindModifier, + TrayIconStyle, +}; +use wayscriber::domain::BoardGridKind; +use wayscriber::draw::ArrowStyle; +use wayscriber::input::DragBindableTool; +use wayscriber::{config, domain, i18n}; + +use std::fs; + use super::super::color::ColorInput; #[test] @@ -95,16 +109,16 @@ fn shape_pen_recognition_feedback_round_trips() { #[test] fn board_grid_draft_preserves_patterns_and_rejects_invalid_spacing() { let mut config = Config::default(); - let mut boards = wayscriber::config::BoardsConfig::default(); - boards.items[1].grid = wayscriber::config::BoardGridConfig { - kind: wayscriber::config::BoardGridKindConfig::IsometricDots, + let mut boards = BoardsConfig::default(); + boards.items[1].grid = BoardGridConfig { + kind: BoardGridKindConfig::IsometricDots, spacing: 20, }; config.boards = Some(boards); let mut draft = ConfigDraft::from_config(&config); assert_eq!( draft.boards.items[1].grid_kind, - wayscriber::domain::BoardGridKind::IsometricDots + BoardGridKind::IsometricDots ); assert_eq!( draft.to_config(&config).unwrap().boards.unwrap().items[1].grid, @@ -222,7 +236,7 @@ use super::super::fields::{ #[test] fn config_draft_round_trips_toolbar_rebind_modifier() { let mut config = Config::default(); - config.ui.toolbar.rebind_modifier = wayscriber::config::ToolbarRebindModifier::CtrlAlt; + config.ui.toolbar.rebind_modifier = ToolbarRebindModifier::CtrlAlt; let mut draft = ConfigDraft::from_config(&config); assert_eq!( draft.ui_toolbar_rebind_modifier, @@ -235,7 +249,7 @@ fn config_draft_round_trips_toolbar_rebind_modifier() { .expect("toolbar modifier round trip"); assert_eq!( round_trip.ui.toolbar.rebind_modifier, - wayscriber::config::ToolbarRebindModifier::ShiftAlt + ToolbarRebindModifier::ShiftAlt ); } @@ -245,13 +259,13 @@ fn config_draft_round_trips_arrow_style() { // that sets a style loads as Standard in the UI, and saving anything from // any tab writes that Standard back over the user's choice. let mut config = Config::default(); - config.arrow.style = wayscriber::draw::ArrowStyle::Curved; + config.arrow.style = ArrowStyle::Curved; let mut draft = ConfigDraft::from_config(&config); assert_eq!(draft.arrow_style, ArrowStyleOption::Curved); draft.arrow_style = ArrowStyleOption::Double; let round_trip = draft.to_config(&config).expect("arrow style round trip"); - assert_eq!(round_trip.arrow.style, wayscriber::draw::ArrowStyle::Double); + assert_eq!(round_trip.arrow.style, ArrowStyle::Double); } #[test] @@ -347,10 +361,7 @@ fn config_draft_round_trips_region_capture_settings() { let round_trip = draft .to_config(&config) .expect("region capture settings should round trip"); - assert_eq!( - round_trip.capture.region.picker, - wayscriber::config::RegionPicker::Slurp - ); + assert_eq!(round_trip.capture.region.picker, RegionPicker::Slurp); assert!(!round_trip.capture.region.show_size_readout); assert!(round_trip.capture.region.show_loupe); assert!(!round_trip.capture.region.show_legend); @@ -508,13 +519,13 @@ fn config_draft_rejects_path_escaping_save_names() { #[test] fn sparse_configurator_no_op_save_remains_byte_for_byte_sparse() { - let temp = crate::test_temp::tempdir().expect("create temp directory"); + let temp = test_temp::tempdir().expect("create temp directory"); let path = temp.path().join("config.toml"); let original = format!( "config_revision = {}\n# intentionally sparse\n", - wayscriber::config::CURRENT_CONFIG_REVISION + config::CURRENT_CONFIG_REVISION ); - std::fs::write(&path, &original).expect("write sparse config"); + fs::write(&path, &original).expect("write sparse config"); let document = ConfigDocument::load_from_path(&path).expect("load sparse config"); let draft = ConfigDraft::from_config(document.config()); let updated = draft @@ -526,7 +537,7 @@ fn sparse_configurator_no_op_save_remains_byte_for_byte_sparse() { .expect("save untouched sparse draft"); assert_eq!( - std::fs::read_to_string(path).expect("read sparse config"), + fs::read_to_string(path).expect("read sparse config"), original ); } @@ -559,9 +570,9 @@ fn config_draft_round_trips_precise_floats_without_truncation() { #[test] fn sparse_configurator_edit_adds_only_the_edited_field_path() { - let temp = crate::test_temp::tempdir().expect("create temp directory"); + let temp = test_temp::tempdir().expect("create temp directory"); let path = temp.path().join("config.toml"); - std::fs::write(&path, "# intentionally sparse\n").expect("write sparse config"); + fs::write(&path, "# intentionally sparse\n").expect("write sparse config"); let document = ConfigDocument::load_from_path(&path).expect("load sparse config"); let mut draft = ConfigDraft::from_config(document.config()); draft.performance_max_fps_no_vsync = "144".to_string(); @@ -573,7 +584,7 @@ fn sparse_configurator_edit_adds_only_the_edited_field_path() { .save_with_backup(updated) .expect("save sparse draft edit"); - let saved = std::fs::read_to_string(path).expect("read sparse config"); + let saved = fs::read_to_string(path).expect("read sparse config"); assert!(saved.contains("# intentionally sparse")); assert!(saved.contains("[performance]")); assert!(saved.contains("max_fps_no_vsync = 144")); @@ -634,13 +645,13 @@ fn config_draft_round_trips_quick_colors() { /// would come back on the next save. #[test] fn a_config_saved_in_chinese_keeps_its_font_and_color_names() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::a_config_saved_in_chinese_keeps_its_font_and_color_names" ), || { - wayscriber::i18n::set_language(wayscriber::domain::Language::SimplifiedChinese); + i18n::set_language(domain::Language::SimplifiedChinese); let mut config = Config::default(); config.drawing.font_style = "italic".to_string(); config.drawing.font_weight = "light".to_string(); @@ -687,7 +698,7 @@ fn config_draft_preserves_implicit_quick_color_defaults() { #[test] fn config_draft_preserves_sparse_explicit_quick_colors_without_padding_the_file() { - let temp = crate::test_temp::tempdir().expect("create temp directory"); + let temp = test_temp::tempdir().expect("create temp directory"); let path = temp.path().join("config.toml"); let original = format!( r#"config_revision = {} @@ -695,9 +706,9 @@ fn config_draft_preserves_sparse_explicit_quick_colors_without_padding_the_file( label = "Only configured color" color = "blue" "#, - wayscriber::config::CURRENT_CONFIG_REVISION + config::CURRENT_CONFIG_REVISION ); - std::fs::write(&path, &original).expect("write sparse quick colors"); + fs::write(&path, &original).expect("write sparse quick colors"); let document = ConfigDocument::load_from_path(&path).expect("load sparse quick colors"); assert_eq!( document @@ -723,7 +734,7 @@ color = "blue" .save_with_backup(updated) .expect("save sparse quick colors"); assert_eq!( - std::fs::read_to_string(path).expect("read sparse quick colors"), + fs::read_to_string(path).expect("read sparse quick colors"), original ); } @@ -1112,7 +1123,7 @@ fn config_draft_round_trips_render_profiles() { #[test] fn config_draft_round_trips_board_pan_settings() { let mut config = Config::default(); - config.boards = Some(wayscriber::config::BoardsConfig { + config.boards = Some(BoardsConfig { pan_enabled: false, show_pan_badge: false, ..Default::default() @@ -1380,7 +1391,7 @@ fn section_toggle_replaces_overlay_item_override_on_save() { .contains(&ToolbarSectionFlag::Presets.item_id()) ); assert!( - wayscriber::config::resolve_section_visibility( + config::resolve_section_visibility( saved.ui.toolbar.layout_mode, &saved.ui.toolbar.mode_overrides, &resolved, @@ -1580,7 +1591,7 @@ fn config_draft_round_trips_presets_and_history() { config.history.custom_redo_steps ); assert_eq!(round_trip.presets.slot_count, config.presets.slot_count); - assert_eq!(round_trip.presets.get_slot(1), config.presets.get_slot(1)); + assert_eq!(round_trip.presets.slot(1), config.presets.slot(1)); } #[test] @@ -1626,7 +1637,7 @@ fn preset_tool_change_loads_selected_tool_profile_values() { let round_trip = draft .to_config(&config) .expect("expected config to round trip"); - let preset = round_trip.presets.get_slot(1).expect("preset"); + let preset = round_trip.presets.slot(1).expect("preset"); let settings = preset.tool_settings.as_ref().expect("tool settings"); assert_eq!(preset.tool, Tool::Marker); @@ -1683,7 +1694,7 @@ fn preset_visible_edits_update_selected_tool_profile_only() { let round_trip = draft .to_config(&config) .expect("expected config to round trip"); - let preset = round_trip.presets.get_slot(1).expect("preset"); + let preset = round_trip.presets.slot(1).expect("preset"); let settings = preset.tool_settings.as_ref().expect("tool settings"); assert_eq!(preset.tool, Tool::Marker); @@ -1701,11 +1712,11 @@ fn preset_visible_edits_update_selected_tool_profile_only() { #[test] fn config_draft_round_trips_drag_tool_mapping() { let mut config = Config::default(); - config.drawing.drag_tool = wayscriber::input::DragBindableTool::Arrow; - config.drawing.shift_drag_tool = wayscriber::input::DragBindableTool::Eraser; - config.drawing.ctrl_drag_tool = wayscriber::input::DragBindableTool::Pen; - config.drawing.ctrl_shift_drag_tool = wayscriber::input::DragBindableTool::Rect; - config.drawing.tab_drag_tool = wayscriber::input::DragBindableTool::Ellipse; + config.drawing.drag_tool = DragBindableTool::Arrow; + config.drawing.shift_drag_tool = DragBindableTool::Eraser; + config.drawing.ctrl_drag_tool = DragBindableTool::Pen; + config.drawing.ctrl_shift_drag_tool = DragBindableTool::Rect; + config.drawing.tab_drag_tool = DragBindableTool::Ellipse; let mut drag_tools = config.drawing.effective_drag_tools(); drag_tools.right.drag_tool = DragTool::Pen; drag_tools.right.drag_color = Some(ColorSpec::Name("blue".to_string())); @@ -1826,10 +1837,10 @@ fn config_draft_round_trips_ui_language() { #[test] fn config_draft_ui_language_defaults_to_auto_when_key_missing() { - let temp = crate::test_temp::tempdir().expect("create temp directory"); + let temp = test_temp::tempdir().expect("create temp directory"); let missing = temp.path().join("missing-language.toml"); - std::fs::write(&missing, "[ui]\ntheme = \"dark\"\n").expect("write config without language"); + fs::write(&missing, "[ui]\ntheme = \"dark\"\n").expect("write config without language"); let document = ConfigDocument::load_from_path(&missing).expect("load config without language"); assert_eq!(document.config().ui.language, Language::Auto); assert_eq!( @@ -1843,10 +1854,10 @@ fn config_draft_ui_language_defaults_to_auto_when_key_missing() { /// of reaching the draft as a silently rewritten value. #[test] fn config_document_salvages_unknown_ui_language_to_auto() { - let temp = crate::test_temp::tempdir().expect("create temp directory"); + let temp = test_temp::tempdir().expect("create temp directory"); let unknown = temp.path().join("unknown-language.toml"); - std::fs::write(&unknown, "[ui]\nlanguage = \"fr\"\n").expect("write unknown language"); + fs::write(&unknown, "[ui]\nlanguage = \"fr\"\n").expect("write unknown language"); let (document, warning) = ConfigDocument::load_for_editing_from_path(&unknown) .expect("expected the editing load to salvage the unknown language"); @@ -1895,39 +1906,31 @@ fn config_draft_keeps_the_documents_revision_until_a_migration_is_applied() { .expect("expected config to round trip"); assert_eq!(untouched.config_revision, 0); - draft.config_revision = Some(wayscriber::config::CURRENT_CONFIG_REVISION); + draft.config_revision = Some(config::CURRENT_CONFIG_REVISION); let migrated = draft .to_config(&base) .expect("expected config to round trip"); - assert_eq!( - migrated.config_revision, - wayscriber::config::CURRENT_CONFIG_REVISION - ); + assert_eq!(migrated.config_revision, config::CURRENT_CONFIG_REVISION); } #[test] fn config_draft_preserves_tray_icon_style() { let mut config = Config::default(); - config.tray.icon_style = wayscriber::config::TrayIconStyle::Colored; + config.tray.icon_style = TrayIconStyle::Colored; let draft = ConfigDraft::from_config(&config); let round_trip = draft .to_config(&config) .expect("expected config to round trip"); - assert_eq!( - round_trip.tray.icon_style, - wayscriber::config::TrayIconStyle::Colored - ); + assert_eq!(round_trip.tray.icon_style, TrayIconStyle::Colored); } #[test] fn compiled_defaults_are_not_shown_as_authored_keybindings() { let draft = ConfigDraft::from_config(&Config::default()); assert!( - !draft - .keybindings - .is_authored(crate::models::KeybindingField::Undo), + !draft.keybindings.is_authored(KeybindingField::Undo), "Config::default() is AllExplicit for validation, not for UI source badges" ); } diff --git a/configurator/src/models/config/to_config/drawing.rs b/configurator/src/models/config/to_config/drawing.rs index ec97f6f9e..1a5844aef 100644 --- a/configurator/src/models/config/to_config/drawing.rs +++ b/configurator/src/models/config/to_config/drawing.rs @@ -3,12 +3,11 @@ use super::super::parse::{ parse_field_in_range, parse_u8_in_range, parse_usize_at_least, parse_usize_in_range, }; use crate::models::error::FormError; -use wayscriber::config::MAX_SHAPE_RECOGNITION_SENSITIVITY; use wayscriber::config::{ ARROW_ANGLE_MAX, ARROW_ANGLE_MIN, ARROW_LENGTH_MAX, ARROW_LENGTH_MIN, Config, + MAX_SHAPE_RECOGNITION_SENSITIVITY, }; -use wayscriber::domain::{DragBindableTool, DragTool}; -use wayscriber::domain::{MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; +use wayscriber::domain::{DragBindableTool, DragTool, MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; use wayscriber::draw::{MAX_PEN_SMOOTHING, REGULAR_POLYGON_MAX_SIDES, REGULAR_POLYGON_MIN_SIDES}; impl ConfigDraft { diff --git a/configurator/src/models/config/to_config/ui.rs b/configurator/src/models/config/to_config/ui.rs index 9a0976e61..a29086f1e 100644 --- a/configurator/src/models/config/to_config/ui.rs +++ b/configurator/src/models/config/to_config/ui.rs @@ -1,3 +1,5 @@ +use wayscriber::config; + use super::super::draft::ConfigDraft; use super::super::parse::{ parse_field, parse_field_in_range, parse_u64_field, parse_u64_in_range, parse_usize_field, @@ -65,7 +67,7 @@ impl ConfigDraft { config.ui.toolbar.stroke_controls = self.ui_toolbar_stroke_controls.to_config(); config.ui.toolbar.mode_overrides = self.ui_toolbar_mode_overrides.to_config(); config.ui.toolbar.items = self.ui_toolbar_items.clone(); - let visibility = wayscriber::config::resolve_section_visibility( + let visibility = config::resolve_section_visibility( config.ui.toolbar.layout_mode, &config.ui.toolbar.mode_overrides, &config.ui.toolbar.items.resolved(), diff --git a/configurator/src/models/daemon.rs b/configurator/src/models/daemon.rs index 94f5b4e3c..3898e7148 100644 --- a/configurator/src/models/daemon.rs +++ b/configurator/src/models/daemon.rs @@ -1,3 +1,5 @@ +use std::env; + use wayscriber::i18n::tr; use wayscriber::env_vars::{ @@ -18,11 +20,11 @@ pub enum DesktopEnvironment { impl DesktopEnvironment { pub fn detect_current() -> Self { - if std::env::var_os(HYPRLAND_INSTANCE_SIGNATURE_ENV).is_some() { + if env::var_os(HYPRLAND_INSTANCE_SIGNATURE_ENV).is_some() { return Self::Hyprland; } - let current = std::env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_default(); - let session = std::env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_default(); + let current = env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_default(); + let session = env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_default(); Self::from_desktop_strings(¤t, &session) } diff --git a/configurator/src/models/fields/arrow.rs b/configurator/src/models/fields/arrow.rs index f7ae0f595..ecb25d0e4 100644 --- a/configurator/src/models/fields/arrow.rs +++ b/configurator/src/models/fields/arrow.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; use wayscriber::draw::ArrowStyle; @@ -49,8 +52,8 @@ impl ArrowStyleOption { } } -impl std::fmt::Display for ArrowStyleOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for ArrowStyleOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/fields/capture.rs b/configurator/src/models/fields/capture.rs index 90c98e70b..63c2be6de 100644 --- a/configurator/src/models/fields/capture.rs +++ b/configurator/src/models/fields/capture.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; use wayscriber::config::RegionPicker; @@ -35,8 +38,8 @@ impl RegionPickerOption { } } -impl std::fmt::Display for RegionPickerOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for RegionPickerOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/fields/eraser.rs b/configurator/src/models/fields/eraser.rs index b85606e4d..e63cc0104 100644 --- a/configurator/src/models/fields/eraser.rs +++ b/configurator/src/models/fields/eraser.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; use wayscriber::domain::EraserMode; @@ -36,8 +39,8 @@ impl EraserModeOption { } } -impl std::fmt::Display for EraserModeOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for EraserModeOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -79,8 +82,8 @@ impl PresetEraserKindOption { } } -impl std::fmt::Display for PresetEraserKindOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for PresetEraserKindOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -122,8 +125,8 @@ impl PresetEraserModeOption { } } -impl std::fmt::Display for PresetEraserModeOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for PresetEraserModeOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/fields/export.rs b/configurator/src/models/fields/export.rs index 4bfab4e2c..3d944a0a0 100644 --- a/configurator/src/models/fields/export.rs +++ b/configurator/src/models/fields/export.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; use wayscriber::config::{ @@ -46,8 +49,8 @@ impl PdfPageSizeOption { } } -impl std::fmt::Display for PdfPageSizeOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for PdfPageSizeOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -89,8 +92,8 @@ impl PdfOrientationOption { } } -impl std::fmt::Display for PdfOrientationOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for PdfOrientationOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -136,8 +139,8 @@ impl PdfFitModeOption { } } -impl std::fmt::Display for PdfFitModeOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for PdfFitModeOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -175,8 +178,8 @@ impl PdfTransparentBackgroundOption { } } -impl std::fmt::Display for PdfTransparentBackgroundOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for PdfTransparentBackgroundOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -232,8 +235,8 @@ impl PdfLabelPositionOption { } } -impl std::fmt::Display for PdfLabelPositionOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for PdfLabelPositionOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -289,8 +292,8 @@ impl PdfLabelContentModeOption { } } -impl std::fmt::Display for PdfLabelContentModeOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for PdfLabelContentModeOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/fields/font.rs b/configurator/src/models/fields/font.rs index 73cccd53e..ba717c276 100644 --- a/configurator/src/models/fields/font.rs +++ b/configurator/src/models/fields/font.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -42,8 +45,8 @@ impl FontStyleOption { } } -impl std::fmt::Display for FontStyleOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for FontStyleOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -110,8 +113,8 @@ impl FontWeightOption { } } -impl std::fmt::Display for FontWeightOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for FontWeightOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/fields/input_hud.rs b/configurator/src/models/fields/input_hud.rs index c99d23347..53491043e 100644 --- a/configurator/src/models/fields/input_hud.rs +++ b/configurator/src/models/fields/input_hud.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; use wayscriber::config::{InputHudMode, InputHudPosition}; @@ -40,8 +43,8 @@ impl InputHudModeOption { } } -impl std::fmt::Display for InputHudModeOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for InputHudModeOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -118,8 +121,8 @@ impl InputHudPositionOption { } } -impl std::fmt::Display for InputHudPositionOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for InputHudPositionOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/fields/presenter.rs b/configurator/src/models/fields/presenter.rs index 2507d8ae8..213149cd7 100644 --- a/configurator/src/models/fields/presenter.rs +++ b/configurator/src/models/fields/presenter.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; use wayscriber::config::{PresenterToolBehavior, PresenterToolbarMode}; @@ -47,8 +50,8 @@ impl PresenterToolBehaviorOption { } } -impl std::fmt::Display for PresenterToolBehaviorOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for PresenterToolBehaviorOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -89,8 +92,8 @@ impl PresenterToolbarModeOption { } } -impl std::fmt::Display for PresenterToolbarModeOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for PresenterToolbarModeOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/fields/pressure.rs b/configurator/src/models/fields/pressure.rs index 6555f735a..2d8ba56f7 100644 --- a/configurator/src/models/fields/pressure.rs +++ b/configurator/src/models/fields/pressure.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; use wayscriber::input::state::{PressureThicknessEditMode, PressureThicknessEntryMode}; @@ -39,8 +42,8 @@ impl PressureThicknessEditModeOption { } } -impl std::fmt::Display for PressureThicknessEditModeOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for PressureThicknessEditModeOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -82,8 +85,8 @@ impl PressureThicknessEntryModeOption { } } -impl std::fmt::Display for PressureThicknessEntryModeOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for PressureThicknessEntryModeOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/fields/session.rs b/configurator/src/models/fields/session.rs index c29e9da4b..945cc2108 100644 --- a/configurator/src/models/fields/session.rs +++ b/configurator/src/models/fields/session.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; use wayscriber::config::{SessionCompression, SessionStorageMode}; @@ -40,8 +43,8 @@ impl SessionStorageModeOption { } } -impl std::fmt::Display for SessionStorageModeOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for SessionStorageModeOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -84,8 +87,8 @@ impl SessionCompressionOption { } } -impl std::fmt::Display for SessionCompressionOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for SessionCompressionOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/fields/status.rs b/configurator/src/models/fields/status.rs index fe1157636..f508a8e68 100644 --- a/configurator/src/models/fields/status.rs +++ b/configurator/src/models/fields/status.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; use wayscriber::config::StatusPosition; @@ -49,8 +52,8 @@ impl StatusPositionOption { } } -impl std::fmt::Display for StatusPositionOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for StatusPositionOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/fields/tests.rs b/configurator/src/models/fields/tests.rs index 7167414d7..d0d4ec043 100644 --- a/configurator/src/models/fields/tests.rs +++ b/configurator/src/models/fields/tests.rs @@ -1,3 +1,6 @@ +use crate::models::NamedColorOption; +use crate::test_env; + use super::*; #[test] @@ -6,7 +9,7 @@ fn arrow_style_options_cover_the_core_enum() { // lists ever disagree, a style is reachable from the overlay but not from // the configurator — or worse, the combo row offers one that no longer // exists and picks the wrong index for everything after it. - let core = wayscriber::draw::ArrowStyle::ALL; + let core = ArrowStyle::ALL; let options = ArrowStyleOption::list(); assert_eq!(options.len(), core.len()); for (option, style) in options.iter().zip(core.iter()) { @@ -20,17 +23,14 @@ fn arrow_style_options_cover_the_core_enum() { #[test] fn region_picker_options_cover_the_core_enum() { assert_eq!( - RegionPickerOption::from_picker(wayscriber::config::RegionPicker::Native), + RegionPickerOption::from_picker(RegionPicker::Native), RegionPickerOption::Native ); assert_eq!( - RegionPickerOption::from_picker(wayscriber::config::RegionPicker::Slurp), + RegionPickerOption::from_picker(RegionPicker::Slurp), RegionPickerOption::Slurp ); - assert_eq!( - RegionPickerOption::Slurp.to_picker(), - wayscriber::config::RegionPicker::Slurp - ); + assert_eq!(RegionPickerOption::Slurp.to_picker(), RegionPicker::Slurp); assert_eq!( RegionPickerOption::list(), vec![RegionPickerOption::Native, RegionPickerOption::Slurp] @@ -38,9 +38,11 @@ fn region_picker_options_cover_the_core_enum() { } use wayscriber::config::{ Language, PdfFitMode, PdfLabelContentMode, PdfLabelPosition, PdfOrientation, PdfPageSize, - PdfTransparentBackground, ReducedMotion, SessionCompression, SessionStorageMode, + PdfTransparentBackground, ReducedMotion, RegionPicker, SessionCompression, SessionStorageMode, StatusPosition, UiTheme, }; +use wayscriber::draw::ArrowStyle; +use wayscriber::i18n; #[test] fn font_style_option_handles_custom_values() { @@ -66,7 +68,7 @@ fn font_weight_option_handles_custom_values() { #[test] fn config_values_do_not_follow_the_interface_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::config_values_do_not_follow_the_interface_language" @@ -89,7 +91,7 @@ fn config_values_do_not_follow_the_interface_language() { "red", "green", "blue", "yellow", "orange", "pink", "white", "black", ]; for language in [Language::English, Language::SimplifiedChinese] { - wayscriber::i18n::set_language(language); + i18n::set_language(language); // The overlay parses these words; a translated one is an // invalid font value or an unknown color. let styles: Vec<_> = FontStyleOption::list() @@ -111,7 +113,7 @@ fn config_values_do_not_follow_the_interface_language() { assert_eq!(FontWeightOption::from_value(value).1, value, "{language:?}"); } - let named: Vec<_> = crate::models::NamedColorOption::list() + let named: Vec<_> = NamedColorOption::list() .iter() .map(|option| option.as_value()) .collect(); @@ -184,7 +186,7 @@ fn language_option_round_trips() { #[test] fn language_option_labels_name_each_language_in_itself() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::language_option_labels_name_each_language_in_itself" @@ -193,13 +195,13 @@ fn language_option_labels_name_each_language_in_itself() { use wayscriber::domain::Language; for language in [Language::English, Language::SimplifiedChinese] { - wayscriber::i18n::set_language(language); + i18n::set_language(language); // The way back from a preview reads the same in every UI language. assert_eq!(LanguageOption::English.label(), "English"); assert_eq!(LanguageOption::SimplifiedChinese.label(), "简体中文"); assert_eq!( LanguageOption::Auto.label(), - wayscriber::i18n::tr("Automatic (system locale)") + i18n::tr("Automatic (system locale)") ); } }, @@ -208,7 +210,7 @@ fn language_option_labels_name_each_language_in_itself() { #[test] fn language_row_title_names_the_setting_in_every_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::language_row_title_names_the_setting_in_every_language" @@ -222,7 +224,7 @@ fn language_row_title_names_the_setting_in_every_language() { (Language::English, "Language / 语言"), (Language::SimplifiedChinese, "语言 / Language"), ] { - wayscriber::i18n::set_language(language); + i18n::set_language(language); assert_eq!(LanguageOption::row_title(), title); } }, diff --git a/configurator/src/models/fields/theme.rs b/configurator/src/models/fields/theme.rs index fded39e84..efb99f9c8 100644 --- a/configurator/src/models/fields/theme.rs +++ b/configurator/src/models/fields/theme.rs @@ -1,6 +1,10 @@ +use std::fmt::{Display, Formatter}; +use std::{fmt, iter}; + use wayscriber::i18n::tr; use wayscriber::config::{Language, ReducedMotion, UiTheme}; +use wayscriber::i18n; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UiThemeOption { @@ -43,8 +47,8 @@ impl UiThemeOption { } } -impl std::fmt::Display for UiThemeOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for UiThemeOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -90,8 +94,8 @@ impl ReducedMotionOption { } } -impl std::fmt::Display for ReducedMotionOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for ReducedMotionOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -130,8 +134,8 @@ impl LanguageOption { /// language, then in each other supported language (`语言 / Language`), /// so a user who cannot read the interface still recognizes the row. pub fn row_title() -> String { - let active = wayscriber::i18n::language(); - std::iter::once(active) + let active = i18n::language(); + iter::once(active) .chain( Language::ALL .into_iter() @@ -159,8 +163,8 @@ impl LanguageOption { } } -impl std::fmt::Display for LanguageOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for LanguageOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/fields/tool.rs b/configurator/src/models/fields/tool.rs index 66fc64a7e..82b74935a 100644 --- a/configurator/src/models/fields/tool.rs +++ b/configurator/src/models/fields/tool.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; use wayscriber::config::ColorSpec; @@ -166,8 +169,8 @@ impl ToolOption { } } -impl std::fmt::Display for ToolOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for ToolOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -319,8 +322,8 @@ impl DragToolOption { } } -impl std::fmt::Display for DragToolOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for DragToolOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -408,8 +411,8 @@ impl DragColorOption { } } -impl std::fmt::Display for DragColorOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for DragColorOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/fields/toolbar.rs b/configurator/src/models/fields/toolbar.rs index dc76d7f9e..e7d73a03e 100644 --- a/configurator/src/models/fields/toolbar.rs +++ b/configurator/src/models/fields/toolbar.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; + use wayscriber::i18n::tr; use wayscriber::config::{ @@ -53,8 +56,8 @@ impl ToolbarRebindModifierOption { } } -impl std::fmt::Display for ToolbarRebindModifierOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for ToolbarRebindModifierOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -100,8 +103,8 @@ impl ToolbarLayoutModeOption { } } -impl std::fmt::Display for ToolbarLayoutModeOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for ToolbarLayoutModeOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -139,8 +142,8 @@ impl ZoomChipDisplayOption { } } -impl std::fmt::Display for ZoomChipDisplayOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for ZoomChipDisplayOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -187,8 +190,8 @@ impl ToolbarStrokeControlsOption { } } -impl std::fmt::Display for ToolbarStrokeControlsOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for ToolbarStrokeControlsOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } @@ -230,8 +233,8 @@ impl OverrideOption { } } -impl std::fmt::Display for OverrideOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for OverrideOption { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } diff --git a/configurator/src/models/keybindings/analysis.rs b/configurator/src/models/keybindings/analysis.rs index ae7779345..a565c6944 100644 --- a/configurator/src/models/keybindings/analysis.rs +++ b/configurator/src/models/keybindings/analysis.rs @@ -3,13 +3,13 @@ use super::conflicts::{ShortcutClaim, legacy_action_for, shortcuts_conflict}; use super::{KeybindingField, KeybindingsDraft, parse_keybindings}; use wayscriber::config::Shortcut; -pub(super) struct ParsedShortcuts<'a> { - pub(super) draft: &'a KeybindingsDraft, +pub(super) struct ParsedShortcuts<'draft> { + pub(super) draft: &'draft KeybindingsDraft, entries: Vec<(KeybindingField, Result, String>)>, } -impl<'a> ParsedShortcuts<'a> { - pub(super) fn new(draft: &'a KeybindingsDraft) -> Self { +impl<'draft> ParsedShortcuts<'draft> { + pub(super) fn new(draft: &'draft KeybindingsDraft) -> Self { Self { draft, entries: draft @@ -73,12 +73,13 @@ impl<'a> ParsedShortcuts<'a> { #[cfg(test)] mod tests { + use wayscriber::config::KeybindingsConfig; + use super::super::keybinding_fields; use super::*; #[test] fn summary_parses_each_field_once_per_draft_even_with_many_conflicts() { - let defaults = - KeybindingsDraft::from_config(&wayscriber::config::KeybindingsConfig::default()); + let defaults = KeybindingsDraft::from_config(&KeybindingsConfig::default()); let mut draft = defaults.clone(); for field in keybinding_fields() { draft.set(field, "Ctrl+Q, Ctrl+Q > C, Ctrl+Q".into()); diff --git a/configurator/src/models/keybindings/conflicts.rs b/configurator/src/models/keybindings/conflicts.rs index b69fcdfca..be7a17719 100644 --- a/configurator/src/models/keybindings/conflicts.rs +++ b/configurator/src/models/keybindings/conflicts.rs @@ -368,6 +368,10 @@ fn text_conflict_prompt(conflicts: &[TextShortcutConflict]) -> String { #[cfg(test)] mod tests { use wayscriber::config::keybindings::KeybindingsConfig; + use wayscriber::domain::Language; + use wayscriber::i18n; + + use crate::test_env; use super::*; use crate::models::keybindings::KeybindingField; @@ -494,7 +498,7 @@ mod tests { #[test] fn legacy_stylus_assignment_conflicts_with_canonical_stylus_trigger() { let mut draft = draft(); - draft.legacy_tablet.stylus_primary = Some(wayscriber::config::Action::ToggleRadialMenu); + draft.legacy_tablet.stylus_primary = Some(Action::ToggleRadialMenu); let binding = Shortcut::parse("StylusPrimary").expect("parses"); let claimants = other_claimants(&draft, KeybindingField::Undo, &binding); assert_eq!(claimants.len(), 1); @@ -521,7 +525,7 @@ mod tests { #[test] fn modifiered_stylus_trigger_does_not_claim_the_legacy_barrel() { let mut draft = draft(); - draft.legacy_tablet.stylus_primary = Some(wayscriber::config::Action::ToggleRadialMenu); + draft.legacy_tablet.stylus_primary = Some(Action::ToggleRadialMenu); let binding = Shortcut::parse("Ctrl+StylusPrimary").expect("parses"); assert!(other_claimants(&draft, KeybindingField::Undo, &binding).is_empty()); } @@ -645,7 +649,7 @@ mod tests { #[test] fn a_pending_conflict_names_its_claimants_in_the_current_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::a_pending_conflict_names_its_claimants_in_the_current_language" @@ -666,7 +670,7 @@ mod tests { conflict.prompt() ); - wayscriber::i18n::set_language(wayscriber::domain::Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); let prompt = conflict.prompt(); assert!(prompt.contains("- 清空画布"), "{prompt}"); diff --git a/configurator/src/models/keybindings/parse.rs b/configurator/src/models/keybindings/parse.rs index 6c57954f4..79d62fe11 100644 --- a/configurator/src/models/keybindings/parse.rs +++ b/configurator/src/models/keybindings/parse.rs @@ -1,10 +1,13 @@ #[cfg(test)] -thread_local! { static PARSE_CALLS: std::cell::Cell = const { std::cell::Cell::new(0) }; } +thread_local! { static PARSE_CALLS: Cell = const { Cell::new(0) }; } #[cfg(test)] pub(crate) fn take_parse_calls() -> usize { PARSE_CALLS.with(|calls| calls.replace(0)) } +#[cfg(test)] +use std::cell::Cell; + use wayscriber::config::Shortcut; pub(crate) fn authored_shortcut_parts(value: &str) -> Vec<&str> { diff --git a/configurator/src/models/keybindings/recording.rs b/configurator/src/models/keybindings/recording.rs index c0dac9d58..05612da4e 100644 --- a/configurator/src/models/keybindings/recording.rs +++ b/configurator/src/models/keybindings/recording.rs @@ -6,10 +6,10 @@ use wayscriber::i18n::{tr, trf}; +use gtk4::gdk::Key; #[cfg(feature = "tablet-input")] use wayscriber::config::StylusTrigger; -use wayscriber::config::keybindings::MAX_SEQUENCE_STEPS; -use wayscriber::config::keybindings::{MAX_POINTER_EXTRA, gdk}; +use wayscriber::config::keybindings::{MAX_POINTER_EXTRA, MAX_SEQUENCE_STEPS, gdk}; use wayscriber::config::{KeyBinding, PointerTrigger, Shortcut, ShortcutTrigger}; use super::field::KeybindingField; @@ -476,7 +476,7 @@ fn keyval_to_unicode(keyval: u32) -> Option { // SAFETY: `Key` is a GDK keyval newtype. `from_glib` does not validate, // and `to_unicode` is defined for any u32 keyval (it returns None when // GDK has no mapping). - let key = unsafe { gtk4::gdk::Key::from_glib(keyval) }; + let key = unsafe { Key::from_glib(keyval) }; key.to_unicode() } @@ -588,16 +588,38 @@ pub mod keyval { #[cfg(test)] mod tests { + use wayscriber::config::ChordModifiers; + use wayscriber::i18n; + + use crate::test_env; + use super::*; - fn mods(ctrl: bool, shift: bool, alt: bool, super_held: bool) -> KeyboardModifiers { - KeyboardModifiers { - ctrl, - shift, - alt, - super_held, - } - } + const NONE: KeyboardModifiers = KeyboardModifiers { + ctrl: false, + shift: false, + alt: false, + super_held: false, + }; + const CTRL: KeyboardModifiers = KeyboardModifiers { ctrl: true, ..NONE }; + const SHIFT: KeyboardModifiers = KeyboardModifiers { + shift: true, + ..NONE + }; + #[cfg(feature = "tablet-input")] + const ALT: KeyboardModifiers = KeyboardModifiers { alt: true, ..NONE }; + const SUPER: KeyboardModifiers = KeyboardModifiers { + super_held: true, + ..NONE + }; + const CTRL_SHIFT: KeyboardModifiers = KeyboardModifiers { + ctrl: true, + ..SHIFT + }; + const SHIFT_SUPER: KeyboardModifiers = KeyboardModifiers { + super_held: true, + ..SHIFT + }; fn chord(keyval: u32, modifiers: KeyboardModifiers) -> KeyBinding { match normalize_key_event(keyval, modifiers) { @@ -614,24 +636,28 @@ mod tests { #[test] fn ctrl_shift_x_uses_canonical_modifier_order() { - let binding = chord(u32::from(b'x'), mods(true, true, false, false)); + let binding = chord(u32::from(b'x'), CTRL_SHIFT); assert_eq!(binding.to_string(), "Ctrl+Shift+X"); - let from_uppercase = chord(u32::from(b'X'), mods(true, true, false, false)); + let from_uppercase = chord(u32::from(b'X'), CTRL_SHIFT); assert_eq!(from_uppercase.to_string(), "Ctrl+Shift+X"); assert_eq!(binding, from_uppercase); } #[test] fn shifted_punctuation_records_the_unshifted_key() { - let binding = chord(u32::from(b'!'), mods(false, true, false, false)); + let binding = chord(u32::from(b'!'), SHIFT); assert_eq!(binding.to_string(), "Shift+1"); - assert!(binding.matches("1", false, true, false, false)); - assert!(!binding.matches("!", false, true, false, false)); + let shift_held = ChordModifiers { + shift: true, + ..ChordModifiers::default() + }; + assert!(binding.matches("1", shift_held)); + assert!(!binding.matches("!", shift_held)); } #[test] fn ctrl_shift_plus_round_trips_the_plus_key() { - let binding = chord(keyval::PLUS, mods(true, true, false, false)); + let binding = chord(keyval::PLUS, CTRL_SHIFT); assert_eq!(binding.key, "+"); assert_eq!(binding.to_string(), "Ctrl+Shift++"); let parsed = KeyBinding::parse("Ctrl+Shift++").expect("plus key parses"); @@ -652,7 +678,7 @@ mod tests { keyval::META_L, keyval::HYPER_L, ] { - match normalize_key_event(keyval, mods(true, false, false, false)) { + match normalize_key_event(keyval, CTRL) { RecordedKeyboard::Pending { modifiers } => { assert_eq!(pending_preview(modifiers), "Ctrl+…"); } @@ -690,20 +716,17 @@ mod tests { #[test] fn super_chords_record_as_canonical_super() { - let binding = chord(u32::from(b'x'), mods(false, false, false, true)); + let binding = chord(u32::from(b'x'), SUPER); assert_eq!(binding.to_string(), "Super+X"); assert!(binding.logo); assert_eq!(binding, KeyBinding::parse("Meta+X").expect("Meta alias")); - assert_eq!( - chord(keyval::F5, mods(false, true, false, true)).to_string(), - "Shift+Super+F5" - ); + assert_eq!(chord(keyval::F5, SHIFT_SUPER).to_string(), "Shift+Super+F5"); } #[test] fn bare_super_stays_pending() { - match normalize_key_event(keyval::SUPER_L, mods(false, false, false, true)) { + match normalize_key_event(keyval::SUPER_L, SUPER) { RecordedKeyboard::Pending { modifiers } => { assert_eq!(pending_preview(modifiers), "Super+…"); } @@ -741,7 +764,7 @@ mod tests { #[test] fn named_keys_keep_keyboard_names_in_every_language() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::named_keys_keep_keyboard_names_in_every_language" @@ -750,7 +773,7 @@ mod tests { use wayscriber::domain::Language; for language in [Language::English, Language::SimplifiedChinese] { - wayscriber::i18n::set_language(language); + i18n::set_language(language); for (keyval, name) in [ (keyval::END, "End"), (keyval::KP_END, "End"), @@ -758,9 +781,13 @@ mod tests { (keyval::DELETE, "Delete"), (keyval::KP_DELETE, "Delete"), ] { - let binding = chord(keyval, mods(true, false, false, false)); + let binding = chord(keyval, CTRL); assert_eq!(binding.to_string(), format!("Ctrl+{name}"), "{language:?}"); - assert!(binding.matches(name, true, false, false, false)); + let ctrl_held = ChordModifiers { + ctrl: true, + ..ChordModifiers::default() + }; + assert!(binding.matches(name, ctrl_held)); } } }, @@ -783,11 +810,7 @@ mod tests { RecordedDevice::Trigger(trigger) => assert_eq!(trigger.to_string(), "MouseBack"), other => panic!("expected MouseBack, got {other:?}"), } - match normalize_button_event( - 9, - RecorderDeviceKind::Mouse, - mods(true, false, false, false), - ) { + match normalize_button_event(9, RecorderDeviceKind::Mouse, CTRL) { RecordedDevice::Trigger(trigger) => { assert_eq!(trigger.to_string(), "Ctrl+MouseForward"); } @@ -834,7 +857,7 @@ mod tests { RecordedDevice::Trigger(trigger) => assert_eq!(trigger.to_string(), "StylusPrimary"), other => panic!("expected StylusPrimary, got {other:?}"), } - match normalize_button_event(3, RecorderDeviceKind::Pen, mods(false, false, true, false)) { + match normalize_button_event(3, RecorderDeviceKind::Pen, ALT) { RecordedDevice::Trigger(trigger) => { assert_eq!(trigger.to_string(), "Alt+StylusSecondary"); } @@ -862,13 +885,13 @@ mod tests { assert!(!recorder.can_remove_last()); assert_eq!(recorder.prompt(), "Press the first chord."); - let first = chord(u32::from(b'k'), mods(true, false, false, false)); + let first = chord(u32::from(b'k'), CTRL); assert!(recorder.push_keyboard_step(first).is_none()); assert!(recorder.can_remove_last()); assert!(!recorder.can_finish()); assert_eq!(recorder.prompt(), "Ctrl+K then …"); - let second = chord(u32::from(b'c'), mods(true, false, false, false)); + let second = chord(u32::from(b'c'), CTRL); assert!(recorder.push_keyboard_step(second).is_none()); assert!(recorder.can_finish()); assert_eq!( @@ -878,7 +901,7 @@ mod tests { Some("Ctrl+K > Ctrl+C".to_string()) ); - let third = chord(u32::from(b'v'), mods(true, false, false, false)); + let third = chord(u32::from(b'v'), CTRL); let finished = recorder .push_keyboard_step(third) .expect("third step auto-finishes"); @@ -889,7 +912,7 @@ mod tests { #[test] fn sequence_recorder_can_remove_the_last_step() { let mut recorder = ShortcutRecorderState::new_sequence(KeybindingField::Undo); - let first = chord(u32::from(b'k'), mods(true, false, false, false)); + let first = chord(u32::from(b'k'), CTRL); recorder.push_keyboard_step(first); recorder.remove_last_step(); assert!(!recorder.can_remove_last()); @@ -898,7 +921,7 @@ mod tests { #[test] fn the_prompt_follows_the_ui_language_while_the_recorder_is_open() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::the_prompt_follows_the_ui_language_while_the_recorder_is_open" @@ -906,14 +929,13 @@ mod tests { || { use wayscriber::domain::Language; let mut sequence = ShortcutRecorderState::new_sequence(KeybindingField::Undo); - sequence - .push_keyboard_step(chord(u32::from(b'k'), mods(true, false, false, false))); + sequence.push_keyboard_step(chord(u32::from(b'k'), CTRL)); let mut rejected = ShortcutRecorderState::new(KeybindingField::Undo); rejected.reject(RecorderRejection::DrawingButton); let waiting = ShortcutRecorderState::new(KeybindingField::Undo); assert_eq!(sequence.prompt(), "Ctrl+K then …"); - wayscriber::i18n::set_language(Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); // The recording itself is untouched; only its words change. assert_eq!(sequence.prompt(), "Ctrl+K 然后 …"); diff --git a/configurator/src/models/keybindings/tests.rs b/configurator/src/models/keybindings/tests.rs index d81614f5c..16ca604d3 100644 --- a/configurator/src/models/keybindings/tests.rs +++ b/configurator/src/models/keybindings/tests.rs @@ -1,6 +1,7 @@ -use wayscriber::config::action_label; -use wayscriber::config::action_meta_iter; +use std::collections::HashSet; + use wayscriber::config::keybindings::KeybindingsConfig; +use wayscriber::config::{ConfigurableAction, action_label, action_meta_iter}; use super::draft::KeybindingsDraft; use super::field::{KeybindingField, keybinding_fields, keybinding_tab}; @@ -33,14 +34,11 @@ fn parse_keybinding_list_rejects_unparseable_shortcuts() { #[test] fn presentation_order_covers_each_generated_keybinding_field_once() { let fields = keybinding_fields(); - let unique: std::collections::HashSet<_> = fields.iter().copied().collect(); + let unique: HashSet<_> = fields.iter().copied().collect(); assert_eq!(unique.len(), fields.len()); - assert_eq!( - unique.len(), - wayscriber::config::ConfigurableAction::all().len() - ); - for field in wayscriber::config::ConfigurableAction::all() { + assert_eq!(unique.len(), ConfigurableAction::all().len()); + for field in ConfigurableAction::all() { assert!( unique.contains(field), "missing presentation row for {field:?}" diff --git a/configurator/src/models/session.rs b/configurator/src/models/session.rs index a8d22723c..fa4b4a98b 100644 --- a/configurator/src/models/session.rs +++ b/configurator/src/models/session.rs @@ -1,10 +1,15 @@ use wayscriber::i18n::{tr, trf}; use std::collections::HashMap; +use std::fs; +use std::fs::Metadata; +use std::io::ErrorKind; use std::path::{Path, PathBuf}; use std::time::{Duration, UNIX_EPOCH}; +use wayscriber::session::SessionArtifactKind; use wayscriber::session::catalog::CatalogEntry; +use wayscriber::{session, time_utils}; use super::daemon::DaemonRuntimeStatus; @@ -269,9 +274,9 @@ pub struct SessionArtifactSummary { impl SessionArtifactSummary { pub fn from_primary_path(path: &Path) -> Result { - let artifacts = wayscriber::session::named_session_artifact_paths(path); - let non_lock_paths = wayscriber::session::named_session_non_lock_artifact_paths(path) - .map_err(|err| err.to_string())?; + let artifacts = session::named_session_artifact_paths(path); + let non_lock_paths = + session::named_session_non_lock_artifact_paths(path).map_err(|err| err.to_string())?; let mut summary = Self { primary_exists: artifact_exists(&artifacts.primary)?, @@ -290,16 +295,10 @@ impl SessionArtifactSummary { }; summary.non_lock_size_bytes = summary.non_lock_size_bytes.saturating_add(metadata.len()); - match wayscriber::session::named_session_artifact_kind(path, &artifact) { - Some(wayscriber::session::SessionArtifactKind::Backup) => { - summary.backup_exists = true - } - Some(wayscriber::session::SessionArtifactKind::Recovery) => { - summary.recovery_exists = true - } - Some(wayscriber::session::SessionArtifactKind::UnreadableCopy) => { - summary.unreadable_copy_exists = true - } + match session::named_session_artifact_kind(path, &artifact) { + Some(SessionArtifactKind::Backup) => summary.backup_exists = true, + Some(SessionArtifactKind::Recovery) => summary.recovery_exists = true, + Some(SessionArtifactKind::UnreadableCopy) => summary.unreadable_copy_exists = true, _ => {} } } @@ -345,10 +344,10 @@ fn artifact_exists(path: &Path) -> Result { Ok(artifact_metadata(path)?.is_some()) } -fn artifact_metadata(path: &Path) -> Result, String> { - match std::fs::symlink_metadata(path) { +fn artifact_metadata(path: &Path) -> Result, String> { + match fs::symlink_metadata(path) { Ok(metadata) => Ok(Some(metadata)), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) if err.kind() == ErrorKind::NotFound => Ok(None), Err(err) => Err(trf("failed to inspect {}: {}", &[&path.display(), &err])), } } @@ -368,7 +367,7 @@ pub fn format_byte_count(bytes: u64) -> String { fn format_catalog_time(epoch_millis: u64) -> String { let time = UNIX_EPOCH + Duration::from_millis(epoch_millis); - wayscriber::time_utils::format_system_time(time, "%Y-%m-%d %H:%M") + time_utils::format_system_time(time, "%Y-%m-%d %H:%M") .unwrap_or_else(|| tr("Unknown").to_string()) } diff --git a/configurator/src/models/session/tests.rs b/configurator/src/models/session/tests.rs index 68947e71c..3b06a5cd3 100644 --- a/configurator/src/models/session/tests.rs +++ b/configurator/src/models/session/tests.rs @@ -1,3 +1,5 @@ +use crate::test_temp; + use super::*; #[test] @@ -60,14 +62,14 @@ fn default_session_target_paths_handle_nonstandard_names() { #[test] fn artifact_summary_reports_non_lock_artifacts_and_size() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("lecture.wayscriber-session"); - let artifacts = wayscriber::session::named_session_artifact_paths(&path); - std::fs::write(&artifacts.primary, b"primary").unwrap(); - std::fs::write(&artifacts.backup, b"backup").unwrap(); - std::fs::write(&artifacts.recovery, b"recovery").unwrap(); - std::fs::write(&artifacts.clear_marker, b"cleared").unwrap(); - std::fs::write(&artifacts.lock, b"lock").unwrap(); + let artifacts = session::named_session_artifact_paths(&path); + fs::write(&artifacts.primary, b"primary").unwrap(); + fs::write(&artifacts.backup, b"backup").unwrap(); + fs::write(&artifacts.recovery, b"recovery").unwrap(); + fs::write(&artifacts.clear_marker, b"cleared").unwrap(); + fs::write(&artifacts.lock, b"lock").unwrap(); let summary = SessionArtifactSummary::from_primary_path(&path).unwrap(); @@ -94,13 +96,13 @@ fn format_byte_count_uses_compact_units() { #[test] fn artifact_summary_counts_diagnostic_copies_without_counting_similar_foreign_files() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let primary = temp.path().join("board.wayscriber-session"); - std::fs::write(&primary, b"primary").unwrap(); + fs::write(&primary, b"primary").unwrap(); for suffix in [".corrupt-1", ".bak.corrupt-2", ".recovery.corrupt-3"] { - std::fs::write(format!("{}{suffix}", primary.display()), b"diagnostic").unwrap(); + fs::write(format!("{}{suffix}", primary.display()), b"diagnostic").unwrap(); } - std::fs::write( + fs::write( format!("{}.corrupt-01", primary.display()), b"foreign similar name", ) @@ -116,7 +118,7 @@ fn artifact_summary_counts_diagnostic_copies_without_counting_similar_foreign_fi assert_eq!(summary.status_label(), "primary, unreadable copy · 37 B"); // A primary moved aside still leaves something to show. - std::fs::remove_file(&primary).unwrap(); + fs::remove_file(&primary).unwrap(); let summary = SessionArtifactSummary::from_primary_path(&primary).unwrap(); assert_eq!(summary.status_label(), "unreadable copy · 30 B"); } diff --git a/configurator/src/models/startup.rs b/configurator/src/models/startup.rs index ce3d67c34..71a0bb476 100644 --- a/configurator/src/models/startup.rs +++ b/configurator/src/models/startup.rs @@ -152,10 +152,12 @@ pub(crate) fn startup_usage() -> String { #[cfg(test)] mod tests { + use std::iter; + use super::*; fn args(values: &[&str]) -> Vec { - std::iter::once("wayscriber-configurator") + iter::once("wayscriber-configurator") .chain(values.iter().copied()) .map(OsString::from) .collect() diff --git a/configurator/src/models/tab.rs b/configurator/src/models/tab.rs index 48d23a48e..16ade51c9 100644 --- a/configurator/src/models/tab.rs +++ b/configurator/src/models/tab.rs @@ -1,3 +1,4 @@ +use wayscriber::configurator_destination::KeybindingsSection; use wayscriber::i18n::tr; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -193,8 +194,8 @@ pub enum KeybindingsTabId { Presets, } -impl From for KeybindingsTabId { - fn from(section: wayscriber::configurator_destination::KeybindingsSection) -> Self { +impl From for KeybindingsTabId { + fn from(section: KeybindingsSection) -> Self { use wayscriber::configurator_destination::KeybindingsSection; match section { @@ -258,6 +259,10 @@ impl KeybindingsTabId { mod tests { use std::collections::BTreeSet; + use wayscriber::i18n; + + use crate::test_env; + use super::*; use wayscriber::domain::Language; @@ -277,7 +282,7 @@ mod tests { #[test] fn stack_names_are_fixed_unique_and_untranslated() { - crate::test_env::in_language_child( + test_env::in_language_child( concat!( module_path!(), "::stack_names_are_fixed_unique_and_untranslated" @@ -288,7 +293,7 @@ mod tests { fn stack_names_survive_translation() { let english: Vec<&str> = TabId::ALL.iter().map(TabId::stack_name).collect(); - wayscriber::i18n::set_language(Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); let chinese: Vec<&str> = TabId::ALL.iter().map(TabId::stack_name).collect(); assert_eq!(english, chinese); diff --git a/configurator/src/test_env.rs b/configurator/src/test_env.rs index 0ef6efe07..be4da01ff 100644 --- a/configurator/src/test_env.rs +++ b/configurator/src/test_env.rs @@ -1,38 +1,99 @@ -use std::sync::{Mutex, MutexGuard}; +use std::sync::{Condvar, Mutex}; -static ENV_MUTEX: Mutex<()> = Mutex::new(()); +use std::ffi::{OsStr, OsString}; +use std::process::Command; +use std::sync::Once; +use std::{env, iter, thread}; +use wayscriber::domain::Language; +use wayscriber::{env_vars, i18n}; + +use crate::test_temp; + +static ENV_LEASE: Lease = Lease::new(); /// The shared environment lock, taken after the language variables are /// cleared: clearing them takes the lock itself, so it must never wait for a /// test that already holds it. -pub(crate) fn lock() -> MutexGuard<'static, ()> { +pub(crate) fn lock() -> LeaseGuard { sanitize_language_environment(); acquire() } -fn acquire() -> MutexGuard<'static, ()> { - ENV_MUTEX - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) +fn acquire() -> LeaseGuard { + ENV_LEASE.acquire() +} + +/// Exclusive access to the process environment for one test. +/// +/// A `MutexGuard` held across a test body is poisoned when that test panics, +/// and every later test would then either fail or reach inside the poisoned +/// lock. The lease's mutex only guards a flag in short sections that cannot +/// panic, and the guard hands the lease on while unwinding, so one failing +/// test does not fail the rest. +struct Lease { + held: Mutex, + released: Condvar, +} + +/// Holds the environment lease until dropped, including when the holder +/// panics. +pub(crate) struct LeaseGuard { + lease: &'static Lease, +} + +impl Lease { + const fn new() -> Self { + Self { + held: Mutex::new(false), + released: Condvar::new(), + } + } + + fn acquire(&'static self) -> LeaseGuard { + let mut held = self + .held + .lock() + .expect("the lease flag is never locked across a panic"); + while *held { + held = self + .released + .wait(held) + .expect("the lease flag is never locked across a panic"); + } + *held = true; + + LeaseGuard { lease: self } + } +} + +impl Drop for LeaseGuard { + fn drop(&mut self) { + // Dropping must not panic while a failed test unwinds, and the flag's + // mutex is never poisoned, so a failed lock only skips the release. + if let Ok(mut held) = self.lease.held.lock() { + *held = false; + } + self.lease.released.notify_one(); + } } /// Holds the shared environment lock until every changed variable is restored. pub(crate) struct EnvGuard { - previous: Vec<(&'static str, Option)>, - _lock: MutexGuard<'static, ()>, + previous: Vec<(&'static str, Option)>, + _lock: LeaseGuard, } impl EnvGuard { - pub(crate) fn set(values: &[(&'static str, &std::ffi::OsStr)]) -> Self { + pub(crate) fn set(values: &[(&'static str, &OsStr)]) -> Self { let lock = lock(); let previous = values .iter() - .map(|(key, _)| (*key, std::env::var_os(key))) + .map(|(key, _)| (*key, env::var_os(key))) .collect(); for (key, value) in values { // SAFETY: mutations are serialized by the shared test lock. unsafe { - std::env::set_var(key, value); + env::set_var(key, value); } } Self { @@ -48,8 +109,8 @@ impl Drop for EnvGuard { // SAFETY: the lock remains held until restoration completes. unsafe { match value { - Some(value) => std::env::set_var(key, value), - None => std::env::remove_var(key), + Some(value) => env::set_var(key, value), + None => env::remove_var(key), } } } @@ -72,24 +133,20 @@ impl Drop for EnvGuard { /// English runs in its own process ([`in_language_child`]), so the process /// language every other test reads stays English, in parallel runs too. pub(crate) fn sanitize_language_environment() { - static SANITIZED: std::sync::Once = std::sync::Once::new(); + static SANITIZED: Once = Once::new(); SANITIZED.call_once(|| { let _lock = acquire(); let no_catalogs = concat!(env!("CARGO_MANIFEST_DIR"), "/no-locale-overrides-in-tests"); let mut values = vec![ - (wayscriber::env_vars::LANGUAGE_OVERRIDE_ENV, ""), - (wayscriber::env_vars::LOCALE_DIR_ENV, no_catalogs), + (env_vars::LANGUAGE_OVERRIDE_ENV, ""), + (env_vars::LOCALE_DIR_ENV, no_catalogs), ]; - values.extend( - wayscriber::env_vars::LOCALE_ENV_KEYS - .iter() - .map(|key| (*key, "")), - ); + values.extend(env_vars::LOCALE_ENV_KEYS.iter().map(|key| (*key, ""))); for (key, value) in values { // SAFETY: mutations are serialized by the shared test lock. unsafe { - std::env::set_var(key, value); + env::set_var(key, value); } } }); @@ -106,7 +163,7 @@ pub(crate) fn sanitize_language_environment() { pub(crate) fn in_language_child(test_path: &str, body: impl FnOnce()) { const CHILD_ENV: &str = "WAYSCRIBER_CONFIGURATOR_LANGUAGE_CHILD"; - if std::env::var_os(CHILD_ENV).is_some() { + if env::var_os(CHILD_ENV).is_some() { body(); return; } @@ -114,14 +171,14 @@ pub(crate) fn in_language_child(test_path: &str, body: impl FnOnce()) { let test_name = test_path .strip_prefix(concat!(env!("CARGO_CRATE_NAME"), "::")) .expect("test path starts with the crate name"); - let locale_dir = crate::test_temp::tempdir().expect("empty locale override directory"); - let mut command = std::process::Command::new(std::env::current_exe().expect("test binary")); + let locale_dir = test_temp::tempdir().expect("empty locale override directory"); + let mut command = Command::new(env::current_exe().expect("test binary")); command .args([test_name, "--exact", "--test-threads=1"]) .env(CHILD_ENV, "1") - .env(wayscriber::env_vars::LOCALE_DIR_ENV, locale_dir.path()) - .env_remove(wayscriber::env_vars::LANGUAGE_OVERRIDE_ENV); - for key in wayscriber::env_vars::LOCALE_ENV_KEYS { + .env(env_vars::LOCALE_DIR_ENV, locale_dir.path()) + .env_remove(env_vars::LANGUAGE_OVERRIDE_ENV); + for key in env_vars::LOCALE_ENV_KEYS { command.env_remove(key); } @@ -143,18 +200,19 @@ pub(crate) fn in_language_child(test_path: &str, body: impl FnOnce()) { #[test] fn guard_restores_environment_after_a_panic() { const KEY: &str = "WAYSCRIBER_CONFIGURATOR_TEST_PANIC_RESTORE"; - let before = std::env::var_os(KEY); - let result = std::panic::catch_unwind(|| { - let _env = EnvGuard::set(&[(KEY, std::ffi::OsStr::new("during"))]); - assert_eq!( - std::env::var_os(KEY).as_deref(), - Some(std::ffi::OsStr::new("during")) - ); - panic!("exercise fixture unwind"); + let before = env::var_os(KEY); + let result = thread::scope(|scope| { + scope + .spawn(|| { + let _env = EnvGuard::set(&[(KEY, OsStr::new("during"))]); + assert_eq!(env::var_os(KEY).as_deref(), Some(OsStr::new("during"))); + panic!("exercise fixture unwind"); + }) + .join() }); assert!(result.is_err()); let _lock = lock(); - assert_eq!(std::env::var_os(KEY), before); + assert_eq!(env::var_os(KEY), before); } #[test] @@ -165,13 +223,12 @@ fn a_language_child_runs_alone_with_the_language_variables_cleared() { "::a_language_child_runs_alone_with_the_language_variables_cleared" ), || { - for key in std::iter::once(wayscriber::env_vars::LANGUAGE_OVERRIDE_ENV) - .chain(wayscriber::env_vars::LOCALE_ENV_KEYS) + for key in iter::once(env_vars::LANGUAGE_OVERRIDE_ENV).chain(env_vars::LOCALE_ENV_KEYS) { - assert_eq!(std::env::var_os(key), None, "{key} reached the child"); + assert_eq!(env::var_os(key), None, "{key} reached the child"); } - wayscriber::i18n::set_language(wayscriber::domain::Language::SimplifiedChinese); - assert_eq!(wayscriber::i18n::tr("Boards"), "画板"); + i18n::set_language(Language::SimplifiedChinese); + assert_eq!(i18n::tr("Boards"), "画板"); }, ); } diff --git a/src/about_window/clipboard.rs b/src/about_window/clipboard.rs index dc4d633a2..0b8ee8ca5 100644 --- a/src/about_window/clipboard.rs +++ b/src/about_window/clipboard.rs @@ -1,18 +1,23 @@ +use std::thread; + use anyhow::Result; use log::warn; use std::thread::JoinHandle; +use crate::desktop_open::DesktopOpenInvocation; +use crate::{clipboard_text, desktop_open}; + pub(super) fn open_url(url: &str) -> Result> { open_url_with(url, |invocation| { - crate::desktop_open::open_in_background(invocation.clone()) + desktop_open::open_in_background(invocation.clone()) }) } fn open_url_with( url: &str, - open: impl FnOnce(&crate::desktop_open::DesktopOpenInvocation) -> Result, + open: impl FnOnce(&DesktopOpenInvocation) -> Result, ) -> Result { - let invocation = crate::desktop_open::trusted_url(url)?; + let invocation = desktop_open::trusted_url(url)?; open(&invocation) } @@ -22,7 +27,7 @@ pub(super) fn copy_text_to_clipboard(text: &str) -> Option> { } let text = text.to_string(); - Some(std::thread::spawn(move || { + Some(thread::spawn(move || { if let Err(err) = copy_text_with_command(&text, copy_text_via_command) { warn!("Failed to copy About text to clipboard: {err:#}"); } @@ -40,11 +45,12 @@ where } fn copy_text_via_command(text: &str) -> Result<()> { - crate::clipboard_text::copy_text_via_command(text).map_err(anyhow::Error::msg) + clipboard_text::copy_text_via_command(text).map_err(anyhow::Error::msg) } #[cfg(test)] mod tests { + use std::ffi::OsString; use std::sync::atomic::{AtomicUsize, Ordering}; use super::*; @@ -66,9 +72,7 @@ mod tests { assert!(!matches!(program.to_str(), Some("sh" | "bash" | "cmd"))); assert_eq!( arguments, - [std::ffi::OsString::from( - "https://wayscriber.com/report#d=abc" - )] + [OsString::from("https://wayscriber.com/report#d=abc")] ); } @@ -104,7 +108,7 @@ mod tests { .unwrap(); assert_eq!( observed.unwrap(), - [std::ffi::OsString::from( + [OsString::from( "https://www.wayscriber.com/docs/getting-started/updating.html" )] ); diff --git a/src/about_window/content.rs b/src/about_window/content.rs index 776f4e466..c17b48665 100644 --- a/src/about_window/content.rs +++ b/src/about_window/content.rs @@ -8,6 +8,7 @@ use crate::i18n::{tr, trf}; use crate::update_check::{ AvailableUpdate, CachedStatus, DEFAULT_NOTES_URL, Freshness, update_instructions_url, }; +use crate::{build_info, update_check}; /// wayscriber.com is the only outbound link: the site carries the docs, /// release notes, and per-distro update instructions, so the dialog does not @@ -97,7 +98,7 @@ pub(super) enum UpdateState { impl UpdateState { /// Seed the row from the cache so opening About costs no network. pub(super) fn from_cache(status: CachedStatus) -> Self { - if crate::update_check::compiled_out() { + if update_check::compiled_out() { return Self::Unavailable; } Self::from_status(status) @@ -191,10 +192,10 @@ pub(super) struct AboutContent { impl AboutContent { pub(super) fn build() -> Self { Self::from_parts( - crate::build_info::version(), - crate::build_info::commit_hash(), - crate::build_info::build_date(), - crate::build_info::install_source(), + build_info::version(), + build_info::commit_hash(), + build_info::build_date(), + build_info::install_source(), ) } @@ -281,7 +282,7 @@ impl AboutContent { fn capitalize(text: &str) -> String { let mut chars = text.chars(); match chars.next() { - Some(first) => first.to_uppercase().collect::() + chars.as_str(), + Some(first) => format!("{}{}", first.to_uppercase(), chars.as_str()), None => String::new(), } } @@ -311,6 +312,8 @@ fn plural(count: u64, unit: &str) -> String { #[cfg(test)] mod tests { + use crate::base64; + use super::*; use crate::update_check::DEFAULT_UPDATE_URL; @@ -358,7 +361,7 @@ mod tests { let fragment = url .strip_prefix(&format!("{REPORT_URL}#d=")) .expect("the report row opens the stable page with a diagnostics fragment"); - let decoded = crate::base64::decode_standard(fragment).expect("fragment is valid base64"); + let decoded = base64::decode_standard(fragment).expect("fragment is valid base64"); assert_eq!( String::from_utf8(decoded).unwrap(), @@ -408,7 +411,7 @@ mod tests { notes_url: DEFAULT_NOTES_URL.to_string(), }; let available = UpdateState::from_status(CachedStatus::Update { - update: update.clone(), + update, freshness: Freshness { checked_seconds_ago: Some(60), last_attempt_failed: false, @@ -487,7 +490,7 @@ mod tests { fn a_compiled_out_build_shows_an_inert_card() { let seeded = UpdateState::from_cache(CachedStatus::Never(Freshness::default())); - if crate::update_check::compiled_out() { + if update_check::compiled_out() { assert_eq!(seeded, UpdateState::Unavailable); assert_eq!(seeded.action(), None); assert!(seeded.headline().contains("off in this build")); diff --git a/src/about_window/diagnostics.rs b/src/about_window/diagnostics.rs index f401892d2..cdf568e90 100644 --- a/src/about_window/diagnostics.rs +++ b/src/about_window/diagnostics.rs @@ -4,10 +4,13 @@ //! report carries the build, desktop, and feature information in one paste //! instead of three follow-up questions. +use std::env; + use crate::env_vars::{ DESKTOP_SESSION_ENV, WAYLAND_DISPLAY_ENV, XDG_CURRENT_DESKTOP_ENV, XDG_SESSION_DESKTOP_ENV, }; use crate::i18n::{tr, trf}; +use crate::{base64, build_info}; /// Environment facts worth reporting, captured so the formatter stays pure. #[derive(Debug, Clone, Default, PartialEq, Eq)] @@ -25,10 +28,10 @@ pub(super) struct DiagnosticsEnv { impl DiagnosticsEnv { fn capture() -> Self { Self { - version: crate::build_info::version().to_string(), - commit: crate::build_info::commit_hash().to_string(), - build_date: crate::build_info::build_date().map(str::to_string), - install_source: crate::build_info::install_source().map(str::to_string), + version: build_info::version().to_string(), + commit: build_info::commit_hash().to_string(), + build_date: build_info::build_date().map(str::to_string), + install_source: build_info::install_source().map(str::to_string), current_desktop: env_value(XDG_CURRENT_DESKTOP_ENV), session_desktop: env_value(XDG_SESSION_DESKTOP_ENV), desktop_session: env_value(DESKTOP_SESSION_ENV), @@ -38,7 +41,7 @@ impl DiagnosticsEnv { } fn env_value(key: &str) -> Option { - std::env::var(key) + env::var(key) .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) @@ -46,7 +49,7 @@ fn env_value(key: &str) -> Option { /// Diagnostics for this process. pub(super) fn report() -> String { - format_report(&DiagnosticsEnv::capture(), compiled_features()) + format_report(&DiagnosticsEnv::capture(), &compiled_features()) } /// `#d=`, for handing the diagnostics to the @@ -64,7 +67,7 @@ pub(super) fn report_url(base: &str, diagnostics: &str) -> String { } fn encode_fragment(report: &str) -> String { - crate::base64::encode_standard(report.as_bytes()) + base64::encode_standard(report.as_bytes()) } /// Cargo features this binary was built with, which decide whether the tray, @@ -89,7 +92,7 @@ fn compiled_features() -> Vec<&'static str> { features } -fn format_report(env: &DiagnosticsEnv, features: Vec<&'static str>) -> String { +fn format_report(env: &DiagnosticsEnv, features: &[&'static str]) -> String { let mut lines = Vec::new(); let mut build = trf("Wayscriber {}", &[&env.version]); @@ -149,7 +152,7 @@ mod tests { #[test] fn reports_every_field_when_available() { - let report = format_report(&sample_env(), vec!["tray", "portal"]); + let report = format_report(&sample_env(), &["tray", "portal"]); assert_eq!( report, @@ -169,7 +172,7 @@ mod tests { ..DiagnosticsEnv::default() }; - let report = format_report(&env, Vec::new()); + let report = format_report(&env, &[]); assert_eq!( report, @@ -190,7 +193,7 @@ mod tests { ..sample_env() }; - assert!(format_report(&env, Vec::new()).contains("Desktop: sway")); + assert!(format_report(&env, &[]).contains("Desktop: sway")); } #[test] @@ -212,7 +215,7 @@ mod tests { .expect("report URL carries a #d= payload"); assert_eq!(base, "https://wayscriber.com/report"); - let decoded = crate::base64::decode_standard(fragment).expect("fragment is valid base64"); + let decoded = base64::decode_standard(fragment).expect("fragment is valid base64"); assert_eq!(String::from_utf8(decoded).unwrap(), diagnostics); } } diff --git a/src/about_window/handlers/compositor.rs b/src/about_window/handlers/compositor.rs index 34592ffac..61556b2bb 100644 --- a/src/about_window/handlers/compositor.rs +++ b/src/about_window/handlers/compositor.rs @@ -1,8 +1,6 @@ use smithay_client_toolkit::compositor::CompositorHandler; -use wayland_client::{ - Connection, QueueHandle, - protocol::{wl_output, wl_surface}, -}; +use wayland_client::protocol::{wl_output, wl_surface}; +use wayland_client::{Connection, QueueHandle}; use super::super::AboutWindowState; diff --git a/src/about_window/handlers/dispatch.rs b/src/about_window/handlers/dispatch.rs index 18d430726..305d4e153 100644 --- a/src/about_window/handlers/dispatch.rs +++ b/src/about_window/handlers/dispatch.rs @@ -1,4 +1,5 @@ -use wayland_client::{Connection, Dispatch, QueueHandle, protocol::wl_buffer}; +use wayland_client::protocol::wl_buffer; +use wayland_client::{Connection, Dispatch, QueueHandle}; use super::super::AboutWindowState; diff --git a/src/about_window/handlers/keyboard.rs b/src/about_window/handlers/keyboard.rs index ea30f11db..f7f0e80d8 100644 --- a/src/about_window/handlers/keyboard.rs +++ b/src/about_window/handlers/keyboard.rs @@ -1,10 +1,8 @@ use smithay_client_toolkit::seat::keyboard::{ KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers, }; -use wayland_client::{ - Connection, QueueHandle, - protocol::{wl_keyboard, wl_surface}, -}; +use wayland_client::protocol::{wl_keyboard, wl_surface}; +use wayland_client::{Connection, QueueHandle}; use super::super::AboutWindowState; diff --git a/src/about_window/handlers/output.rs b/src/about_window/handlers/output.rs index 6f2af9391..1fdf3ec55 100644 --- a/src/about_window/handlers/output.rs +++ b/src/about_window/handlers/output.rs @@ -1,5 +1,6 @@ use smithay_client_toolkit::output::{OutputHandler, OutputState}; -use wayland_client::{Connection, QueueHandle, protocol::wl_output}; +use wayland_client::protocol::wl_output; +use wayland_client::{Connection, QueueHandle}; use super::super::AboutWindowState; diff --git a/src/about_window/handlers/pointer.rs b/src/about_window/handlers/pointer.rs index a9b7453ac..77d38cb0e 100644 --- a/src/about_window/handlers/pointer.rs +++ b/src/about_window/handlers/pointer.rs @@ -2,7 +2,8 @@ use smithay_client_toolkit::seat::pointer::{ BTN_LEFT, PointerEvent, PointerEventKind, PointerHandler, }; use smithay_client_toolkit::shell::WaylandSurface; -use wayland_client::{Connection, QueueHandle, protocol::wl_pointer}; +use wayland_client::protocol::wl_pointer; +use wayland_client::{Connection, QueueHandle}; use super::super::AboutWindowState; diff --git a/src/about_window/handlers/seat.rs b/src/about_window/handlers/seat.rs index 69f700b5d..39d0f11a9 100644 --- a/src/about_window/handlers/seat.rs +++ b/src/about_window/handlers/seat.rs @@ -1,6 +1,8 @@ use log::warn; -use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState, pointer::ThemeSpec}; -use wayland_client::{Connection, QueueHandle, protocol::wl_seat}; +use smithay_client_toolkit::seat::pointer::ThemeSpec; +use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState}; +use wayland_client::protocol::wl_seat; +use wayland_client::{Connection, QueueHandle}; use super::super::AboutWindowState; diff --git a/src/about_window/interaction.rs b/src/about_window/interaction.rs index 3ea69b9f3..cd3968ad2 100644 --- a/src/about_window/interaction.rs +++ b/src/about_window/interaction.rs @@ -90,6 +90,8 @@ fn rect_contains(rect: Rect, position: (f64, f64)) -> bool { #[cfg(test)] mod tests { + use crate::update_check::Freshness; + use super::*; use crate::update_check::{AvailableUpdate, DEFAULT_NOTES_URL, DEFAULT_UPDATE_URL}; @@ -101,7 +103,7 @@ mod tests { update_url: DEFAULT_UPDATE_URL.to_string(), notes_url: DEFAULT_NOTES_URL.to_string(), }), - freshness: crate::update_check::Freshness { + freshness: Freshness { checked_seconds_ago: Some(0), last_attempt_failed: false, }, @@ -111,10 +113,7 @@ mod tests { #[test] fn focus_order_runs_content_first_and_close_last() { let content = AboutContent::build(); - let elements = focus_order( - &content, - &UpdateState::Unknown(crate::update_check::Freshness::default()), - ); + let elements = focus_order(&content, &UpdateState::Unknown(Freshness::default())); assert_eq!(elements.first(), Some(&Element::UpdateCard)); assert_eq!(elements.last(), Some(&Element::Close)); @@ -134,10 +133,7 @@ mod tests { let checking = focus_order(&content, &UpdateState::Checking); assert!(!checking.contains(&Element::UpdateCard)); - let idle = focus_order( - &content, - &UpdateState::Unknown(crate::update_check::Freshness::default()), - ); + let idle = focus_order(&content, &UpdateState::Unknown(Freshness::default())); assert_eq!(idle.len(), checking.len() + 1); } @@ -145,10 +141,7 @@ mod tests { fn hit_testing_matches_the_planned_rectangles() { let content = AboutContent::build(); let plan = super::super::layout::plan(&content); - let elements = focus_order( - &content, - &UpdateState::Unknown(crate::update_check::Freshness::default()), - ); + let elements = focus_order(&content, &UpdateState::Unknown(Freshness::default())); let card = plan.update_card; let inside = (card.0 + card.2 / 2.0, card.1 + card.3 / 2.0); diff --git a/src/about_window/mod.rs b/src/about_window/mod.rs index 22705d273..99a68786c 100644 --- a/src/about_window/mod.rs +++ b/src/about_window/mod.rs @@ -4,6 +4,9 @@ //! overlay's backend: the dialog is a plain toplevel with its own chrome, //! sized to its content and painted from the shared theme tokens. +use std::ffi::OsString; +use std::thread::JoinHandle; + use anyhow::{Context, Result}; use log::debug; use smithay_client_toolkit::compositor::CompositorState; @@ -13,13 +16,18 @@ use smithay_client_toolkit::seat::SeatState; use smithay_client_toolkit::shell::WaylandSurface; use smithay_client_toolkit::shell::xdg::XdgShell; use smithay_client_toolkit::shell::xdg::window::{Window, WindowDecorations}; -use smithay_client_toolkit::shm::{Shm, slot::SlotPool}; +use smithay_client_toolkit::shm::Shm; +use smithay_client_toolkit::shm::slot::SlotPool; use wayland_client::Connection; use wayland_client::globals::registry_queue_init; use crate::app_id::runtime_app_id; use crate::config::Config; +use crate::domain::Language; +use crate::i18n; use crate::i18n::tr; +use crate::ui::theme::Theme; +use crate::ui_text::UiTextEngine; mod clipboard; mod content; @@ -34,12 +42,13 @@ mod state; use content::{AboutContent, UpdateState}; use interaction::Element; use layout::Plan; +use smithay_client_toolkit::seat::pointer::{PointerData, ThemedPointer}; pub fn run_about_window() -> Result<()> { let (theme, language) = chrome_settings(); // This is its own process, so it selects its own language before any of // its text is built. - crate::i18n::initialize(language); + i18n::initialize(language); let conn = Connection::connect_to_env().context("Failed to connect to Wayland compositor")?; let (globals, mut event_queue) = @@ -118,11 +127,11 @@ fn run_about_event_loop( /// The arguments that start the About window: `--about`, and the `--language` /// this process runs with, if any, so the window matches the one that opened /// it. -pub(crate) fn launch_arguments() -> Vec { - launch_arguments_for(crate::i18n::command_line_language()) +pub(crate) fn launch_arguments() -> Vec { + launch_arguments_for(i18n::command_line_language()) } -fn launch_arguments_for(language: Option) -> Vec { +fn launch_arguments_for(language: Option) -> Vec { let mut arguments = vec!["--about".into()]; if let Some(language) = language { arguments.push("--language".into()); @@ -135,7 +144,7 @@ fn launch_arguments_for(language: Option) -> Vec (crate::ui::theme::Theme, crate::domain::Language) { +fn chrome_settings() -> (Theme, Language) { let ui = match Config::load() { Ok(loaded) => loaded.config.ui, Err(err) => { @@ -144,10 +153,7 @@ fn chrome_settings() -> (crate::ui::theme::Theme, crate::domain::Language) { } }; - ( - crate::ui::theme::Theme::resolve(ui.theme.to_theme_mode()), - ui.language, - ) + (Theme::resolve(ui.theme.to_theme_mode()), ui.language) } /// Logical surface size for a plan, rounded up so nothing is clipped by a @@ -160,8 +166,8 @@ fn surface_size(plan: &Plan) -> (u32, u32) { } struct AboutWindowState { - ui_text: crate::ui_text::UiTextEngine, - theme: crate::ui::theme::Theme, + ui_text: UiTextEngine, + theme: Theme, registry_state: RegistryState, compositor_state: CompositorState, shm: Shm, @@ -181,7 +187,7 @@ struct AboutWindowState { /// blocking fetch never runs inside a protocol handler. check_requested: bool, /// Open/copy workers that must finish before the process broker shuts down. - helper_workers: Vec>, + helper_workers: Vec>, content: AboutContent, plan: Plan, update: UpdateState, @@ -193,23 +199,22 @@ struct AboutWindowState { shift_held: bool, notice: Option, icon: Option, - themed_pointer: Option< - smithay_client_toolkit::seat::pointer::ThemedPointer< - smithay_client_toolkit::seat::pointer::PointerData, - >, - >, + themed_pointer: Option>, } #[cfg(test)] mod tests { - use std::fs; + use std::{fs, iter}; + + use crate::cli::{Cli, CliOutcome}; + use crate::test_env; use super::*; use crate::config::test_helpers::with_temp_config_home; use crate::domain::Language; fn write_config(contents: &str) { - let path = Config::get_config_path().expect("config path"); + let path = Config::config_path().expect("config path"); fs::create_dir_all(path.parent().expect("config dir")).expect("create config dir"); fs::write(path, contents).expect("write config"); } @@ -222,14 +227,14 @@ mod tests { ["--about", "--language", "zh-CN"] ); // The arguments it is given parse back to the same language. - let parsed = crate::cli::Cli::try_parse_from( - std::iter::once("wayscriber".into()) + let parsed = Cli::try_parse_from( + iter::once("wayscriber".into()) .chain(launch_arguments_for(Some(Language::SimplifiedChinese))), ); assert!( matches!( parsed, - Ok(crate::cli::CliOutcome::Run(ref cli)) + Ok(CliOutcome::Run(ref cli)) if cli.about && cli.language == Some(Language::SimplifiedChinese) ), "{parsed:?}" @@ -249,7 +254,7 @@ mod tests { #[test] fn the_dialog_text_is_built_in_the_saved_language() { - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::the_dialog_text_is_built_in_the_saved_language" @@ -260,7 +265,7 @@ mod tests { // What `run_about_window` does before it builds content. let (_theme, language) = chrome_settings(); - crate::i18n::initialize(language); + i18n::initialize(language); let content = AboutContent::build(); let labels: Vec<&str> = content @@ -269,7 +274,7 @@ mod tests { .map(|button| button.label) .collect(); assert!(labels.contains(&"复制诊断信息"), "{labels:?}"); - assert_eq!(crate::i18n::language(), Language::SimplifiedChinese); + assert_eq!(i18n::language(), Language::SimplifiedChinese); }); }, ); diff --git a/src/about_window/render/draw.rs b/src/about_window/render/draw.rs index 5b88c565a..fe6460efe 100644 --- a/src/about_window/render/draw.rs +++ b/src/about_window/render/draw.rs @@ -41,15 +41,15 @@ const HOVER_TINT: f64 = 0.12; const SUCCESS: Rgba = theme::rgba(theme::overlay::TOAST_SUCCESS, 1.0); /// Everything one frame needs. -pub(super) struct Frame<'a> { - pub(super) plan: &'a Plan, - pub(super) content: &'a AboutContent, - pub(super) update: &'a UpdateState, - pub(super) icon: Option<&'a cairo::ImageSurface>, +pub(super) struct Frame<'state> { + pub(super) plan: &'state Plan, + pub(super) content: &'state AboutContent, + pub(super) update: &'state UpdateState, + pub(super) icon: Option<&'state cairo::ImageSurface>, pub(super) hover: Option, pub(super) focus: Option, /// Transient footer message ("Copied to clipboard"). - pub(super) notice: Option<&'a str>, + pub(super) notice: Option<&'state str>, } impl Frame<'_> { diff --git a/src/about_window/render/draw/tests.rs b/src/about_window/render/draw/tests.rs index a57b79066..984e98494 100644 --- a/src/about_window/render/draw/tests.rs +++ b/src/about_window/render/draw/tests.rs @@ -1,3 +1,5 @@ +use crate::update_check::Freshness; + use super::*; use crate::about_window::layout; use crate::update_check::{AvailableUpdate, DEFAULT_NOTES_URL, DEFAULT_UPDATE_URL}; @@ -93,13 +95,13 @@ fn every_update_state_paints_cleanly() { let states = [ UpdateState::Unavailable, - UpdateState::Unknown(crate::update_check::Freshness::default()), + UpdateState::Unknown(Freshness::default()), UpdateState::Checking, - UpdateState::UpToDate(crate::update_check::Freshness { + UpdateState::UpToDate(Freshness { checked_seconds_ago: Some(3_600), last_attempt_failed: false, }), - UpdateState::UpToDate(crate::update_check::Freshness { + UpdateState::UpToDate(Freshness { checked_seconds_ago: Some(3_600), last_attempt_failed: true, }), @@ -110,7 +112,7 @@ fn every_update_state_paints_cleanly() { update_url: DEFAULT_UPDATE_URL.to_string(), notes_url: DEFAULT_NOTES_URL.to_string(), }), - freshness: crate::update_check::Freshness { + freshness: Freshness { checked_seconds_ago: Some(0), last_attempt_failed: false, }, @@ -135,7 +137,7 @@ fn hover_focus_and_notice_paint_cleanly() { let content = AboutContent::build(); let plan = layout::plan(&content); let ctx = context(&plan); - let update = UpdateState::Unknown(crate::update_check::Freshness::default()); + let update = UpdateState::Unknown(Freshness::default()); let mut frame = frame_for(&plan, &content, &update); frame.hover = Some(Element::Link(0)); diff --git a/src/about_window/render/mod.rs b/src/about_window/render/mod.rs index 77b495833..7b9531550 100644 --- a/src/about_window/render/mod.rs +++ b/src/about_window/render/mod.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use smithay_client_toolkit::shell::WaylandSurface; +use wayland_client::protocol::wl_shm::Format; use super::AboutWindowState; @@ -34,7 +35,7 @@ impl AboutWindowState { phys_w as i32, phys_h as i32, (phys_w * 4) as i32, - wayland_client::protocol::wl_shm::Format::Argb8888, + Format::Argb8888, ) .context("Failed to create about window buffer")?; diff --git a/src/about_window/render/widgets.rs b/src/about_window/render/widgets.rs index d0fbc68f4..b1264644d 100644 --- a/src/about_window/render/widgets.rs +++ b/src/about_window/render/widgets.rs @@ -1,6 +1,8 @@ //! Painting primitives for the About dialog: rounded surfaces, the close //! affordance, and the focus ring that makes keyboard navigation visible. +use std::f64::consts; + use crate::ui::theme::Rgba; /// Visual state shared by every interactive element. @@ -129,7 +131,7 @@ pub(super) fn draw_chevron(ctx: &cairo::Context, x: f64, center_y: f64, size: f6 /// A filled status dot; the update card uses it as an at-a-glance signal. pub(super) fn draw_status_dot(ctx: &cairo::Context, center: (f64, f64), radius: f64, color: Rgba) { set_color(ctx, color); - ctx.arc(center.0, center.1, radius, 0.0, std::f64::consts::TAU); + ctx.arc(center.0, center.1, radius, 0.0, consts::TAU); let _ = ctx.fill(); } @@ -156,28 +158,10 @@ fn rounded_rect_path(ctx: &cairo::Context, rect: (f64, f64, f64, f64), radius: f let (x, y, width, height) = rect; let r = radius.min(width / 2.0).min(height / 2.0).max(0.0); ctx.new_sub_path(); - ctx.arc(x + width - r, y + r, r, -std::f64::consts::FRAC_PI_2, 0.0); - ctx.arc( - x + width - r, - y + height - r, - r, - 0.0, - std::f64::consts::FRAC_PI_2, - ); - ctx.arc( - x + r, - y + height - r, - r, - std::f64::consts::FRAC_PI_2, - std::f64::consts::PI, - ); - ctx.arc( - x + r, - y + r, - r, - std::f64::consts::PI, - 3.0 * std::f64::consts::FRAC_PI_2, - ); + ctx.arc(x + width - r, y + r, r, -consts::FRAC_PI_2, 0.0); + ctx.arc(x + width - r, y + height - r, r, 0.0, consts::FRAC_PI_2); + ctx.arc(x + r, y + height - r, r, consts::FRAC_PI_2, consts::PI); + ctx.arc(x + r, y + r, r, consts::PI, 3.0 * consts::FRAC_PI_2); ctx.close_path(); } diff --git a/src/about_window/state.rs b/src/about_window/state.rs index 9ddc7054e..2b0dfe52a 100644 --- a/src/about_window/state.rs +++ b/src/about_window/state.rs @@ -1,10 +1,16 @@ //! About-window state: focus, hover, and the actions the handlers trigger. +use std::thread::JoinHandle; + use log::{debug, warn}; use smithay_client_toolkit::seat::pointer::CursorIcon; use wayland_client::Connection; use crate::i18n::tr; +use crate::ui::theme::Theme; +use crate::ui_text::UiTextEngine; +use crate::update_check; +use crate::update_check::{CheckOutcome, Freshness}; use super::content::{AboutAction, AboutContent, UpdateState}; use super::interaction::{self, Element}; @@ -23,16 +29,16 @@ impl AboutWindowState { window: super::Window, content: AboutContent, plan: Plan, - theme: crate::ui::theme::Theme, + theme: Theme, ) -> Self { // Opening the dialog costs no network: the row reports whatever the // last background check wrote, and the user can ask for a fresh one. - let update = UpdateState::from_cache(crate::update_check::cached_status()); + let update = UpdateState::from_cache(update_check::cached_status()); let elements = interaction::focus_order(&content, &update); let (width, height) = surface_size(&plan); Self { - ui_text: crate::ui_text::UiTextEngine::default(), + ui_text: UiTextEngine::default(), theme, registry_state, compositor_state, @@ -167,7 +173,7 @@ impl AboutWindowState { } } - fn track_helper_worker(&mut self, worker: std::thread::JoinHandle<()>) { + fn track_helper_worker(&mut self, worker: JoinHandle<()>) { self.helper_workers.push(worker); } @@ -187,20 +193,18 @@ impl AboutWindowState { /// Perform the check synchronously. Called from the event loop, never from a /// protocol handler. pub(super) fn run_update_check(&mut self) { - let next = match crate::update_check::check_now() { - Ok(crate::update_check::CheckOutcome::Update(update)) => UpdateState::Available { + let next = match update_check::check_now() { + Ok(CheckOutcome::Update(update)) => UpdateState::Available { update: Box::new(update), - freshness: crate::update_check::Freshness { + freshness: Freshness { checked_seconds_ago: Some(0), last_attempt_failed: false, }, }, - Ok(crate::update_check::CheckOutcome::UpToDate { .. }) => { - UpdateState::UpToDate(crate::update_check::Freshness { - checked_seconds_ago: Some(0), - last_attempt_failed: false, - }) - } + Ok(CheckOutcome::UpToDate { .. }) => UpdateState::UpToDate(Freshness { + checked_seconds_ago: Some(0), + last_attempt_failed: false, + }), Err(err) => { debug!("About dialog update check failed: {err}"); UpdateState::Failed(err) diff --git a/src/app/env.rs b/src/app/env.rs index 2020abcdc..b5f434430 100644 --- a/src/app/env.rs +++ b/src/app/env.rs @@ -1,5 +1,7 @@ +use std::env; + pub(crate) fn env_flag_enabled(name: &str) -> bool { - if let Ok(val) = std::env::var(name) { + if let Ok(val) = env::var(name) { matches!( val.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on" @@ -12,20 +14,17 @@ pub(crate) fn env_flag_enabled(name: &str) -> bool { #[cfg(test)] mod tests { use super::env_flag_enabled; + use crate::test_env; use std::env; - use std::sync::Mutex; const TEST_FLAG_ENV: &str = "WAYSCRIBER_TEST_FLAG"; - static ENV_MUTEX: Mutex<()> = Mutex::new(()); #[test] fn env_flag_enabled_accepts_truthy_values() { - let _guard = ENV_MUTEX - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + let _guard = test_env::lock(); for value in ["1", "true", "yes", "on", "TrUe"] { - // SAFETY: serialized via ENV_MUTEX + // SAFETY: the environment lease held above serializes environment access. unsafe { env::set_var(TEST_FLAG_ENV, value); } @@ -35,6 +34,8 @@ mod tests { ); } + // SAFETY: serialized via ENV_MUTEX, still held by `_guard`, with the + // only other test that writes this key. unsafe { env::remove_var(TEST_FLAG_ENV); } @@ -42,12 +43,10 @@ mod tests { #[test] fn env_flag_enabled_rejects_non_truthy_values() { - let _guard = ENV_MUTEX - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + let _guard = test_env::lock(); for value in ["0", "false", "no", "off", "", "random"] { - // SAFETY: serialized via ENV_MUTEX + // SAFETY: the environment lease held above serializes environment access. unsafe { env::set_var(TEST_FLAG_ENV, value); } @@ -57,6 +56,8 @@ mod tests { ); } + // SAFETY: serialized via ENV_MUTEX, still held by `_guard`, with the + // only other test that writes this key. unsafe { env::remove_var(TEST_FLAG_ENV); } diff --git a/src/app/languages.rs b/src/app/languages.rs index c35395dbf..a0f6d1430 100644 --- a/src/app/languages.rs +++ b/src/app/languages.rs @@ -2,6 +2,8 @@ //! //! Command-line output stays English, like the rest of the CLI. +use std::env; + use crate::cli::Cli; use crate::config::Config; use crate::domain::Language; @@ -11,7 +13,7 @@ use crate::i18n::{self, LanguageSource}; /// The language this run was given: its own `--language`, or, for an overlay /// a daemon started, that daemon's. pub(super) fn given_language(cli: &Cli) -> Option { - given_language_with(cli, std::env::var(OVERLAY_LANGUAGE_ENV).ok().as_deref()) + given_language_with(cli, env::var(OVERLAY_LANGUAGE_ENV).ok().as_deref()) } fn given_language_with(cli: &Cli, from_daemon: Option<&str>) -> Option { @@ -28,6 +30,7 @@ pub(super) fn print_languages() { .map_err(|err| format!("{err:#}")); let requested = i18n::requested_language(configured.clone().unwrap_or_default()); let in_effect = i18n::resolve(requested.0); + let configured = configured.as_ref().copied().map_err(String::as_str); print!("{}", render(configured, requested, in_effect)); } @@ -36,7 +39,7 @@ pub(super) fn print_languages() { /// config could not be read), the setting that decides, and what it resolves /// to. fn render( - configured: Result, + configured: Result, (requested, source): (Language, LanguageSource), in_effect: Language, ) -> String { @@ -50,7 +53,7 @@ fn render( } out.push('\n'); - match &configured { + match configured { Ok(language) => out.push_str(&format!( "Configured: {} ([ui] language)\n", language.code() @@ -94,11 +97,15 @@ fn describe(language: Language) -> String { #[cfg(test)] mod tests { + use std::iter; + + use crate::cli::CliOutcome; + use super::*; fn parse(arguments: &[&str]) -> Cli { - match Cli::try_parse_from(std::iter::once("wayscriber").chain(arguments.iter().copied())) { - Ok(crate::cli::CliOutcome::Run(cli)) => cli, + match Cli::try_parse_from(iter::once("wayscriber").chain(arguments.iter().copied())) { + Ok(CliOutcome::Run(cli)) => cli, other => panic!("{arguments:?} did not parse to a run: {other:?}"), } } @@ -156,7 +163,7 @@ mod tests { Language::SimplifiedChinese, ); let from_environment = render( - Err("TOML parse error at line 1, column 4\n |\n1 | [ui\n".to_string()), + Err("TOML parse error at line 1, column 4\n |\n1 | [ui\n"), (Language::English, LanguageSource::Environment), Language::English, ); diff --git a/src/app/mod.rs b/src/app/mod.rs index 225f66cce..b01f95189 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -3,13 +3,23 @@ mod languages; mod session; mod usage; +use std::env as std_env; +use std::os::fd::RawFd; +use std::{fs, io}; + use crate::backend::ExitAfterCaptureMode; use crate::cli::Cli; -use crate::daemon::DaemonToggleRequest; +use crate::daemon::{Daemon, DaemonToggleRequest, protocol_v2}; use crate::env_vars::{DETACHED_ENV, NO_DETACH_ENV, NO_TRAY_ENV, WAYLAND_DISPLAY_ENV}; use crate::paths::overlay_lock_file; +use crate::process_broker::{HelperKind, HelperLifetime}; +use crate::session as stored_session; use crate::session::try_lock_exclusive; use crate::session_override::set_runtime_session_override; +use crate::{ + about_window, backend, daemon, i18n, launch_environment, process_broker, runtime_capabilities, + update_check, +}; use anyhow::Context; use env::env_flag_enabled; use session::run_session_cli_commands; @@ -22,7 +32,7 @@ use usage::{log_overlay_controls, print_usage}; fn acquire_overlay_lock() -> anyhow::Result> { let lock_path = overlay_lock_file(); if let Some(parent) = lock_path.parent() { - std::fs::create_dir_all(parent)?; + fs::create_dir_all(parent)?; } let lock_file = OpenOptions::new() @@ -46,17 +56,17 @@ fn maybe_detach_active(cli: &Cli) -> anyhow::Result { if !(cli.active || cli.freeze) { return Ok(false); } - if env_flag_enabled(NO_DETACH_ENV) || std::env::var_os(DETACHED_ENV).is_some() { + if env_flag_enabled(NO_DETACH_ENV) || std_env::var_os(DETACHED_ENV).is_some() { return Ok(false); } - let exe = std::env::current_exe()?; - let args: Vec = std::env::args_os().skip(1).collect(); - crate::process_broker::current()?.spawn( - crate::process_broker::HelperKind::InitialDetach, - crate::process_broker::HelperLifetime::DetachedAfterExec, + let exe = std_env::current_exe()?; + let args: Vec = std_env::args_os().skip(1).collect(); + process_broker::current()?.spawn( + HelperKind::InitialDetach, + HelperLifetime::DetachedAfterExec, exe.as_os_str(), args, - detach_environment(crate::launch_environment::var_os), + detach_environment(launch_environment::var_os), )?; Ok(true) } @@ -70,7 +80,7 @@ fn detach_environment( ) -> Vec<(OsString, Option)> { let mut environment = vec![(DETACHED_ENV.into(), Some("1".into()))]; - for name in crate::launch_environment::STARTUP_NOTIFICATION_VARIABLES { + for name in launch_environment::STARTUP_NOTIFICATION_VARIABLES { if let Some(value) = launched_with(name) { environment.push((name.into(), Some(value))); } @@ -86,14 +96,14 @@ fn normalized_named_session_file(cli: &Cli) -> anyhow::Result> { let raw = raw_path .to_str() .ok_or_else(|| anyhow::anyhow!("--session-file path must be valid UTF-8"))?; - Ok(Some(crate::session::normalize_named_session_file_arg(raw))) + Ok(Some(stored_session::normalize_named_session_file_arg(raw))) } fn daemon_request_session_file(path: Option) -> anyhow::Result> { let Some(path) = path else { return Ok(None); }; - let current_dir = std::env::current_dir() + let current_dir = std_env::current_dir() .context("failed to resolve current directory for daemon session file")?; Ok(Some(anchor_session_file_for_daemon_request( path, @@ -114,10 +124,10 @@ fn preflight_named_overlay_session(cli: &Cli, path: Option<&Path>) -> anyhow::Re return Ok(()); }; if cli.active || cli.freeze || cli.daemon || cli.daemon_toggle { - crate::session::validate_named_session_file_for_foreground(path)?; + stored_session::validate_named_session_file_for_foreground(path)?; } if cli.active || cli.freeze { - crate::backend::preflight_wayland_connection()?; + backend::preflight_wayland_connection()?; } Ok(()) } @@ -178,7 +188,7 @@ fn detach_from_tty() { /// Replaces `fd` with a descriptor for /dev/null, keeping the number in use. #[cfg(unix)] -fn redirect_to_dev_null(fd: std::os::fd::RawFd) -> std::io::Result<()> { +fn redirect_to_dev_null(fd: RawFd) -> io::Result<()> { use std::os::fd::AsRawFd; let null = OpenOptions::new() @@ -188,7 +198,7 @@ fn redirect_to_dev_null(fd: std::os::fd::RawFd) -> std::io::Result<()> { // SAFETY: both descriptors are open. dup2 atomically replaces `fd` and // leaves `null` open for its owner to close. if unsafe { libc::dup2(null.as_raw_fd(), fd) } < 0 { - return Err(std::io::Error::last_os_error()); + return Err(io::Error::last_os_error()); } Ok(()) } @@ -197,8 +207,8 @@ pub fn run(cli: Cli) -> anyhow::Result<()> { if cli.runtime_capabilities { print!( "{}", - crate::runtime_capabilities::render_runtime_capabilities( - crate::runtime_capabilities::current_runtime_capabilities() + runtime_capabilities::render_runtime_capabilities( + runtime_capabilities::current_runtime_capabilities() ) ); return Ok(()); @@ -207,7 +217,7 @@ pub fn run(cli: Cli) -> anyhow::Result<()> { // Before anything selects a language, so every selection in this run, // and the overlays and About windows it starts, use it. if let Some(language) = languages::given_language(&cli) { - crate::i18n::set_command_line_language(language); + i18n::set_command_line_language(language); } if cli.list_languages { languages::print_languages(); @@ -217,12 +227,12 @@ pub fn run(cli: Cli) -> anyhow::Result<()> { // Runtime and update-checking modes create their process broker before // acquiring locks or starting threads. The guard spans the complete run. let _process_broker = needs_process_broker(&cli) - .then(crate::process_broker::start_for_runtime) + .then(process_broker::start_for_runtime) .transpose()?; - crate::daemon::protocol_v2::start_daemon_watchdog_from_environment()?; + protocol_v2::start_daemon_watchdog_from_environment()?; #[cfg(unix)] - if std::env::var_os(DETACHED_ENV).is_some() { + if std_env::var_os(DETACHED_ENV).is_some() { detach_from_tty(); } @@ -240,7 +250,7 @@ pub fn run(cli: Cli) -> anyhow::Result<()> { }; if cli.about { - crate::about_window::run_about_window()?; + about_window::run_about_window()?; return Ok(()); } @@ -252,7 +262,7 @@ pub fn run(cli: Cli) -> anyhow::Result<()> { .daemon_overlay_action() .map_err(|err| anyhow::anyhow!(err))? { - crate::daemon::send_daemon_overlay_action(action)?; + daemon::send_daemon_overlay_action(action)?; return Ok(()); } @@ -268,7 +278,7 @@ pub fn run(cli: Cli) -> anyhow::Result<()> { session_file, overlay_action: None, }; - crate::daemon::send_daemon_toggle_request(&request)?; + daemon::send_daemon_toggle_request(&request)?; return Ok(()); } @@ -279,7 +289,7 @@ pub fn run(cli: Cli) -> anyhow::Result<()> { } // Check for Wayland environment - if std::env::var(WAYLAND_DISPLAY_ENV).is_err() && (cli.daemon || cli.active || cli.freeze) { + if std_env::var(WAYLAND_DISPLAY_ENV).is_err() && (cli.daemon || cli.active || cli.freeze) { return Err(anyhow::anyhow!( "{WAYLAND_DISPLAY_ENV} not set - this application requires Wayland." )); @@ -292,7 +302,7 @@ pub fn run(cli: Cli) -> anyhow::Result<()> { if tray_disabled { log::info!("Tray disabled via --no-tray / {NO_TRAY_ENV}"); } - let mut daemon = crate::daemon::Daemon::new( + let mut daemon = Daemon::new( cli.mode, !tray_disabled, session_override, @@ -308,7 +318,7 @@ pub fn run(cli: Cli) -> anyhow::Result<()> { Some(lock) => lock, None => return Ok(()), }; - crate::daemon::protocol_v2::publish_ready_from_environment() + protocol_v2::publish_ready_from_environment() .context("failed to publish daemon overlay readiness")?; // One-shot mode: show overlay immediately and exit when done log_overlay_controls(cli.freeze); @@ -324,7 +334,7 @@ pub fn run(cli: Cli) -> anyhow::Result<()> { }; // Run Wayland backend - crate::backend::run_wayland( + backend::run_wayland( cli.mode, cli.freeze, exit_after_capture_mode, @@ -347,11 +357,18 @@ fn needs_process_broker(cli: &Cli) -> bool { // About's URL and clipboard helpers need the broker even when network // update checks were compiled out of this build. || cli.about - || (cli.check_update && !crate::update_check::compiled_out()) + || (cli.check_update && !update_check::compiled_out()) } #[cfg(test)] mod tests { + use std::ffi::OsStr; + use std::thread; + use std::time::{Duration, Instant}; + + use crate::durable_io::AtomicWriteOptions; + use crate::{durable_io, env_vars, test_env, test_temp}; + use super::*; #[test] @@ -398,7 +415,7 @@ mod tests { use std::io::Write; use std::os::fd::AsRawFd; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("was-a-terminal"); let mut file = File::create(&path).unwrap(); file.write_all(b"before ").unwrap(); @@ -408,7 +425,7 @@ mod tests { // SAFETY: F_GETFD only inspects the descriptor the file still owns. assert!(unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFD) } >= 0); - assert_eq!(std::fs::read(&path).unwrap(), b"before "); + assert_eq!(fs::read(&path).unwrap(), b"before "); } /// Names the report file, and so marks the detached child run, of the @@ -417,14 +434,14 @@ mod tests { const DETACH_LAUNCH_TOKEN: &str = "detach-launch-token"; fn launched_with_token(name: &str) -> Option { - (name == crate::env_vars::XDG_ACTIVATION_TOKEN_ENV).then(|| DETACH_LAUNCH_TOKEN.into()) + (name == env_vars::XDG_ACTIVATION_TOKEN_ENV).then(|| DETACH_LAUNCH_TOKEN.into()) } #[test] fn the_detached_relaunch_forwards_the_launch_startup_notification() { let detached = (OsString::from(DETACHED_ENV), Some(OsString::from("1"))); let token = ( - OsString::from(crate::env_vars::XDG_ACTIVATION_TOKEN_ENV), + OsString::from(env_vars::XDG_ACTIVATION_TOKEN_ENV), Some(OsString::from(DETACH_LAUNCH_TOKEN)), ); @@ -440,18 +457,18 @@ mod tests { /// token reaches the detached process. #[test] fn a_detached_relaunch_keeps_the_launch_token_only_when_forwarded() { - if let Some(report) = std::env::var_os(DETACH_CHILD_REPORT_ENV) { - let token = crate::launch_environment::startup_activation_token().unwrap_or_default(); - crate::durable_io::write_atomic( + if let Some(report) = std_env::var_os(DETACH_CHILD_REPORT_ENV) { + let token = launch_environment::startup_activation_token().unwrap_or_default(); + durable_io::write_atomic( Path::new(&report), token.as_bytes(), - crate::durable_io::AtomicWriteOptions::private_runtime_file(), + AtomicWriteOptions::private_runtime_file(), ) .unwrap(); return; } - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let report = temp.path().join("detached-token"); let test_name = concat!( module_path!(), @@ -459,36 +476,36 @@ mod tests { ) .strip_prefix(concat!(env!("CARGO_CRATE_NAME"), "::")) .expect("test path contains the crate prefix"); - let removed = crate::launch_environment::STARTUP_NOTIFICATION_VARIABLES - .map(|name| (name, None::<&std::ffi::OsStr>)); + let removed = + launch_environment::STARTUP_NOTIFICATION_VARIABLES.map(|name| (name, None::<&OsStr>)); let mut variables = vec![(DETACH_CHILD_REPORT_ENV, Some(report.as_os_str()))]; variables.extend(removed); - crate::test_env::with_env_vars(&variables, || { - let guard = crate::process_broker::start_for_runtime().unwrap(); + test_env::with_env_vars(&variables, || { + let guard = process_broker::start_for_runtime().unwrap(); let relaunch = |environment| { - let _ = std::fs::remove_file(&report); + let _ = fs::remove_file(&report); guard .broker() .spawn( - crate::process_broker::HelperKind::InitialDetach, - crate::process_broker::HelperLifetime::DetachedAfterExec, - std::env::current_exe().unwrap().as_os_str(), + HelperKind::InitialDetach, + HelperLifetime::DetachedAfterExec, + std_env::current_exe().unwrap().as_os_str(), [test_name, "--exact", "--test-threads=1"], environment, ) .unwrap(); - let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10); + let deadline = Instant::now() + Duration::from_secs(10); loop { - if let Ok(token) = std::fs::read_to_string(&report) { + if let Ok(token) = fs::read_to_string(&report) { break token; } assert!( - std::time::Instant::now() < deadline, + Instant::now() < deadline, "the detached child did not report" ); - std::thread::sleep(std::time::Duration::from_millis(10)); + thread::sleep(Duration::from_millis(10)); } }; diff --git a/src/app/session.rs b/src/app/session.rs index bf21a357c..1c5967bfd 100644 --- a/src/app/session.rs +++ b/src/app/session.rs @@ -1,12 +1,17 @@ +use std::env; + use crate::cli::Cli; +use crate::config::Config; use crate::env_vars::WAYLAND_DISPLAY_ENV; +use crate::session::{ClearToolStateOutcome, SessionOptions, catalog}; +use crate::{session, time_utils}; pub(crate) fn run_session_cli_commands(cli: &Cli) -> anyhow::Result<()> { if rename_session(cli)? { return Ok(()); } - let loaded = crate::config::Config::load()?; + let loaded = Config::load()?; // [session] configures which files these commands act on. A load that // fell back to defaults for that section would silently retarget a // destructive command - a custom storage path becomes `storage = auto` - @@ -17,20 +22,20 @@ pub(crate) fn run_session_cli_commands(cli: &Cli) -> anyhow::Result<()> { default settings may mistarget - fix the section and retry" ); } - let config_dir = crate::config::Config::config_directory_from_source(&loaded.source)?; - let display_env = std::env::var(WAYLAND_DISPLAY_ENV).ok(); + let config_dir = Config::config_directory_from_source(&loaded.source)?; + let display_env = env::var(WAYLAND_DISPLAY_ENV).ok(); let options = if let Some(raw_path) = cli.session_file.as_ref() { let raw = raw_path .to_str() .ok_or_else(|| anyhow::anyhow!("--session-file path must be valid UTF-8"))?; - let path = crate::session::normalize_named_session_file_arg(raw); + let path = session::normalize_named_session_file_arg(raw); if cli.clear_session || cli.clear_tool_state { - crate::session::validate_named_session_file_for_clear(&path)?; + session::validate_named_session_file_for_clear(&path)?; } else { - crate::session::validate_named_session_file_for_info(&path)?; + session::validate_named_session_file_for_info(&path)?; } - let mut options = crate::session::options_from_config_for_named_file( + let mut options = session::options_from_config_for_named_file( &loaded.config.session, path, display_env.as_deref(), @@ -38,11 +43,7 @@ pub(crate) fn run_session_cli_commands(cli: &Cli) -> anyhow::Result<()> { options.force_resume_persistence(); options } else { - crate::session::options_from_config( - &loaded.config.session, - &config_dir, - display_env.as_deref(), - )? + session::options_from_config(&loaded.config.session, &config_dir, display_env.as_deref())? }; if cli.clear_session { @@ -51,16 +52,16 @@ pub(crate) fn run_session_cli_commands(cli: &Cli) -> anyhow::Result<()> { } if cli.clear_tool_state { - let outcome = crate::session::clear_tool_state(&options)?; + let outcome = session::clear_tool_state(&options)?; println!("Session file: {}", options.session_file_path().display()); match outcome { - crate::session::ClearToolStateOutcome::NoSession => { + ClearToolStateOutcome::NoSession => { println!(" No session file present"); } - crate::session::ClearToolStateOutcome::NoToolState => { + ClearToolStateOutcome::NoToolState => { println!(" No saved tool state present"); } - crate::session::ClearToolStateOutcome::Cleared { + ClearToolStateOutcome::Cleared { preserved_board_data, } => { println!(" Cleared saved tool state"); @@ -75,7 +76,7 @@ pub(crate) fn run_session_cli_commands(cli: &Cli) -> anyhow::Result<()> { } if cli.session_info { - let inspection = crate::session::inspect_session(&options)?; + let inspection = session::inspect_session(&options)?; println!("Session persistence status:"); println!(" Persist transparent: {}", inspection.persist_transparent); println!(" Persist whiteboard : {}", inspection.persist_whiteboard); @@ -104,7 +105,7 @@ pub(crate) fn run_session_cli_commands(cli: &Cli) -> anyhow::Result<()> { } if let Some(ts) = inspection .modified - .and_then(|m| crate::time_utils::format_system_time(m, "%Y-%m-%d %H:%M:%S")) + .and_then(|m| time_utils::format_system_time(m, "%Y-%m-%d %H:%M:%S")) { println!(" Modified : {}", ts); } @@ -163,15 +164,15 @@ fn rename_session(cli: &Cli) -> anyhow::Result { let raw = raw_path .to_str() .ok_or_else(|| anyhow::anyhow!("--session-file path must be valid UTF-8"))?; - let path = crate::session::normalize_named_session_file_arg(raw); - let entry = crate::session::catalog::rename_session_display_name_by_path(&path, display_name)? + let path = session::normalize_named_session_file_arg(raw); + let entry = catalog::rename_session_display_name_by_path(&path, display_name)? .ok_or_else(|| anyhow::anyhow!("session is not in the named-session catalog"))?; println!("Renamed session to {}.", entry.display_name); Ok(true) } -fn clear_session(options: &crate::session::SessionOptions) -> anyhow::Result<()> { - let outcome = crate::session::clear_session(options)?; +fn clear_session(options: &SessionOptions) -> anyhow::Result<()> { + let outcome = session::clear_session(options)?; println!("Session file: {}", options.session_file_path().display()); if outcome.removed_session { println!(" Removed session file"); diff --git a/src/app_id.rs b/src/app_id.rs index d93420240..12a5b1b75 100644 --- a/src/app_id.rs +++ b/src/app_id.rs @@ -1,20 +1,24 @@ const DEFAULT_APP_ID: &str = "wayscriber"; +use std::env; + use crate::env_vars::{APP_ID_ENV, PORTAL_APP_ID_ENV}; pub(crate) fn runtime_app_id() -> String { - std::env::var(APP_ID_ENV) + env::var(APP_ID_ENV) .ok() + .as_deref() .and_then(non_empty_trimmed) .or_else(|| { - std::env::var(PORTAL_APP_ID_ENV) + env::var(PORTAL_APP_ID_ENV) .ok() + .as_deref() .and_then(non_empty_trimmed) }) .unwrap_or_else(|| DEFAULT_APP_ID.to_string()) } -fn non_empty_trimmed(value: String) -> Option { +fn non_empty_trimmed(value: &str) -> Option { let trimmed = value.trim(); if trimmed.is_empty() { None diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 44034aa6a..3c073fa83 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,3 +1,6 @@ +use std::env; +use std::path::PathBuf; + use anyhow::Result; use wayland_client::Connection; @@ -25,9 +28,9 @@ pub fn run_wayland( initial_mode: Option, freeze_on_start: bool, exit_after_capture_mode: ExitAfterCaptureMode, - named_session_file: Option, + named_session_file: Option, ) -> Result<()> { - let mut backend = wayland::WaylandBackend::new( + let mut backend = wayland::WaylandBackend::try_new( initial_mode, freeze_on_start, exit_after_capture_mode, @@ -40,7 +43,7 @@ pub fn run_wayland( } pub fn preflight_wayland_connection() -> Result<()> { - if std::env::var(WAYLAND_DISPLAY_ENV).is_err() { + if env::var(WAYLAND_DISPLAY_ENV).is_err() { return Err(anyhow::anyhow!( "{WAYLAND_DISPLAY_ENV} not set - this application requires Wayland." )); @@ -52,10 +55,12 @@ pub fn preflight_wayland_connection() -> Result<()> { #[cfg(test)] mod tests { + use std::env; + #[test] #[ignore] fn wayland_backend_smoke_test() { - if std::env::var(super::WAYLAND_DISPLAY_ENV).is_err() { + if env::var(super::WAYLAND_DISPLAY_ENV).is_err() { eprintln!( "{} not set; skipping Wayland smoke test", super::WAYLAND_DISPLAY_ENV diff --git a/src/backend/wayland/backend/event_loop/capture.rs b/src/backend/wayland/backend/event_loop/capture.rs index ba6754b92..e23eecc1b 100644 --- a/src/backend/wayland/backend/event_loop/capture.rs +++ b/src/backend/wayland/backend/event_loop/capture.rs @@ -1,6 +1,7 @@ use crate::backend::wayland::acquisition::{ AcquisitionRecord, AcquisitionStage, ScreenAcquisitionOutcome, ScreenAcquisitionOwner, }; +use crate::draw::EmbeddedImage; use crate::input::state::{Toast, ToastPriority}; use log::{info, warn}; use std::time::{Duration, Instant}; @@ -386,7 +387,7 @@ fn resolve_board_capture_outcome( match (outcome, pending_board) { (CaptureOutcome::RenderedImageReady(image), Some(pending)) => { state.capture.finish_capture_lifecycle(); - let embedded = crate::draw::EmbeddedImage { + let embedded = EmbeddedImage { mime_type: image.format.mime_type, width: image.width, height: image.height, @@ -574,18 +575,10 @@ fn handle_capture_results(state: &mut WaylandState) { // Send normal notification. let notification_body = if message_parts.is_empty() { match result.operation { - crate::capture::ImageOperationKind::Screenshot => { - tr("Screenshot captured").to_string() - } - crate::capture::ImageOperationKind::CanvasExport => { - tr("Canvas exported").to_string() - } - crate::capture::ImageOperationKind::BoardPdfExport => { - tr("Board exported").to_string() - } - crate::capture::ImageOperationKind::AllBoardsPdfExport => { - tr("Boards exported").to_string() - } + ImageOperationKind::Screenshot => tr("Screenshot captured").to_string(), + ImageOperationKind::CanvasExport => tr("Canvas exported").to_string(), + ImageOperationKind::BoardPdfExport => tr("Board exported").to_string(), + ImageOperationKind::AllBoardsPdfExport => tr("Boards exported").to_string(), } } else { message_parts.join(" - ") @@ -615,12 +608,11 @@ fn handle_capture_results(state: &mut WaylandState) { message, } => { state.capture.clear_pending_pdf_export(); - let friendly_error = - if matches!(operation, crate::capture::ImageOperationKind::Screenshot) { - friendly_capture_error(kind) - } else { - message.clone() - }; + let friendly_error = if matches!(operation, ImageOperationKind::Screenshot) { + friendly_capture_error(kind) + } else { + message.clone() + }; warn!("{} failed: {}", operation.saved_log_label(), message); @@ -720,6 +712,10 @@ fn handle_capture_manager_failure( #[cfg(test)] mod tests { + use crate::backend::wayland::state::RenderOutcome; + use crate::config::Config; + use crate::ui::toolbar::ToolbarEvent; + use super::*; use crate::backend::wayland::acquisition::ScreenAcquisitionRegistry; use crate::backend::wayland::handlers::test_support::{CaptureFixtureBackend, HandlerFixture}; @@ -742,20 +738,19 @@ mod tests { (ToolbarEvent::ZoomIn, CaptureFixtureBackend::WlrScreencopy), ] { for enter_before_frame in [false, true] { - toolbar_output_discovery(event.clone(), enter_before_frame, backend); + toolbar_output_discovery(&event, enter_before_frame, backend); } } } fn toolbar_output_discovery( - event: crate::ui::toolbar::ToolbarEvent, + event: &ToolbarEvent, enter_before_frame: bool, backend: CaptureFixtureBackend, ) { use smithay_client_toolkit::compositor::CompositorHandler; - let mut fixture = - HandlerFixture::with_capture_output(crate::config::Config::default(), backend); + let mut fixture = HandlerFixture::with_capture_output(Config::default(), backend); let output = fixture.complete_output_metadata("DP-3"); let qh = fixture.queue.handle(); let surface = fixture.state.surface.wl_surface().unwrap().clone(); @@ -772,7 +767,7 @@ mod tests { .state .suppression .keyboard_passthrough_requested(false); - let freezing = matches!(event, crate::ui::toolbar::ToolbarEvent::ToggleFreeze); + let freezing = matches!(event, ToolbarEvent::ToggleFreeze); assert_eq!( reason, if freezing { @@ -843,20 +838,17 @@ mod tests { } fn complete_hidden_frame( - fixture: &mut crate::backend::wayland::handlers::test_support::HandlerFixture, + fixture: &mut HandlerFixture, qh: &wayland_client::QueueHandle, ) { assert!(matches!( fixture.state.render(qh).unwrap(), - crate::backend::wayland::state::RenderOutcome::Committed { .. } + RenderOutcome::Committed { .. } )); fixture.complete_main_frame(); } - fn record( - owner: ScreenAcquisitionOwner, - stage: AcquisitionStage, - ) -> crate::backend::wayland::acquisition::AcquisitionRecord { + fn record(owner: ScreenAcquisitionOwner, stage: AcquisitionStage) -> AcquisitionRecord { let mut registry = ScreenAcquisitionRegistry::default(); let id = registry.request(owner).expect("test acquisition"); if stage == AcquisitionStage::Started { diff --git a/src/backend/wayland/backend/event_loop/dispatch.rs b/src/backend/wayland/backend/event_loop/dispatch.rs index 315aaa512..cd1737745 100644 --- a/src/backend/wayland/backend/event_loop/dispatch.rs +++ b/src/backend/wayland/backend/event_loop/dispatch.rs @@ -64,6 +64,8 @@ pub(super) fn dispatch_events( #[cfg(test)] mod tests { + use std::io; + use std::io::ErrorKind; use std::os::fd::AsRawFd; use std::path::PathBuf; use std::time::Instant; @@ -140,8 +142,8 @@ mod tests { if ready == 0 { panic!("persistence worker did not wake the production route"); } - let err = std::io::Error::last_os_error(); - if err.kind() != std::io::ErrorKind::Interrupted { + let err = io::Error::last_os_error(); + if err.kind() != ErrorKind::Interrupted { panic!("runtime wake poll failed: {err}"); } } @@ -159,7 +161,7 @@ mod tests { session.record_input_dirty(started, true); let dirty_window = session.prepare_autosave_submission().unwrap(); - let runtime_wake = RuntimeWakeSource::new().unwrap(); + let runtime_wake = RuntimeWakeSource::try_new().unwrap(); let mut controller = PersistenceController::start(runtime_wake.handle()).unwrap(); let request_id = controller .try_submit(0, PersistenceOperation::PanicForTest) diff --git a/src/backend/wayland/backend/event_loop/interaction.rs b/src/backend/wayland/backend/event_loop/interaction.rs index d6a424675..7fe53dcb7 100644 --- a/src/backend/wayland/backend/event_loop/interaction.rs +++ b/src/backend/wayland/backend/event_loop/interaction.rs @@ -35,15 +35,19 @@ pub(super) fn interaction_timeout( #[cfg(test)] mod tests { + use crate::draw::TextMeasurer; + use crate::ui_text::UiTextEngine; + use super::*; use crate::config::Action; use crate::draw::Shape; - use crate::input::state::{SpotlightWheelOutcome, test_support::make_test_input_state}; + use crate::input::state::SpotlightWheelOutcome; + use crate::input::state::test_support::make_test_input_state; #[test] fn polling_the_owning_path_finishes_one_idle_wheel_burst() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, diff --git a/src/backend/wayland/backend/event_loop/mod.rs b/src/backend/wayland/backend/event_loop/mod.rs index 554101d5f..e92811b4b 100644 --- a/src/backend/wayland/backend/event_loop/mod.rs +++ b/src/backend/wayland/backend/event_loop/mod.rs @@ -1,3 +1,6 @@ +use std::io; + +use crate::daemon::protocol_v2; use crate::notification; use log::{info, warn}; use std::time::{Duration, Instant}; @@ -47,9 +50,8 @@ pub(super) fn run_event_loop( let mut signals = match install_then_scan( || { let signals = setup_signal_handlers(runtime_wake.handle())?; - if let Err(error) = crate::daemon::protocol_v2::publish_signal_ready_from_environment() - { - return Err(std::io::Error::other(format!( + if let Err(error) = protocol_v2::publish_signal_ready_from_environment() { + return Err(io::Error::other(format!( "failed to publish overlay signal readiness: {error:#}" ))); } @@ -370,9 +372,9 @@ fn handle_xdg_focus_loss(state: &mut WaylandState, qh: &wayland_client::QueueHan } fn install_then_scan( - install: impl FnOnce() -> std::io::Result, + install: impl FnOnce() -> io::Result, scan: impl FnOnce(), -) -> std::io::Result { +) -> io::Result { let installed = install()?; scan(); Ok(installed) @@ -393,7 +395,7 @@ fn terminal_signal_failure( fn finalize_event_loop( loop_error: &mut Option, persist_final_session: impl FnOnce(), - stop_signal_listener: impl FnOnce() -> std::io::Result<()>, + stop_signal_listener: impl FnOnce() -> io::Result<()>, ) { persist_final_session(); if let Err(err) = stop_signal_listener() { @@ -407,13 +409,21 @@ fn finalize_event_loop( } } -fn should_defer_xdg_unfocused_exit( +/// What the overlay knows when an exit request reaches the event loop. +#[derive(Debug, Clone, Copy)] +struct XdgExitContext { is_xdg_window: bool, + /// Losing focus keeps the xdg overlay open instead of exiting. stay_mode: bool, has_keyboard_focus: bool, - explicit_xdg_close_requested: bool, -) -> bool { - is_xdg_window && stay_mode && !has_keyboard_focus && !explicit_xdg_close_requested + explicit_close_requested: bool, +} + +fn should_defer_xdg_unfocused_exit(context: XdgExitContext) -> bool { + context.is_xdg_window + && context.stay_mode + && !context.has_keyboard_focus + && !context.explicit_close_requested } fn break_on_requested_exit(state: &mut WaylandState) -> bool { @@ -422,12 +432,12 @@ fn break_on_requested_exit(state: &mut WaylandState) -> bool { } let explicit_xdg_close_requested = state.focus.take_xdg_explicit_close_requested() || state.input_state.take_explicit_exit_requested(); - if should_defer_xdg_unfocused_exit( - state.surface.is_xdg_window(), - !state.xdg_focus_loss_exits_overlay(), - state.focus.keyboard_focused(), - explicit_xdg_close_requested, - ) { + if should_defer_xdg_unfocused_exit(XdgExitContext { + is_xdg_window: state.surface.is_xdg_window(), + stay_mode: !state.xdg_focus_loss_exits_overlay(), + has_keyboard_focus: state.focus.keyboard_focused(), + explicit_close_requested: explicit_xdg_close_requested, + }) { warn!("Exit requested while unfocused in xdg stay mode; keeping overlay open"); state.input_state.should_exit = false; false @@ -443,7 +453,8 @@ mod tests { use std::time::Duration; use super::{ - finalize_event_loop, install_then_scan, min_timeout, should_defer_xdg_unfocused_exit, + XdgExitContext, finalize_event_loop, install_then_scan, min_timeout, + should_defer_xdg_unfocused_exit, }; #[test] @@ -464,14 +475,32 @@ mod tests { #[test] fn defers_exit_only_for_unfocused_xdg_stay_without_explicit_close() { - assert!(should_defer_xdg_unfocused_exit(true, true, false, false)); - assert!(!should_defer_xdg_unfocused_exit(true, true, true, false)); - assert!(!should_defer_xdg_unfocused_exit(true, false, false, false)); - assert!(!should_defer_xdg_unfocused_exit(false, true, false, false)); + let unfocused_stay_mode = XdgExitContext { + is_xdg_window: true, + stay_mode: true, + has_keyboard_focus: false, + explicit_close_requested: false, + }; + assert!(should_defer_xdg_unfocused_exit(unfocused_stay_mode)); + assert!(!should_defer_xdg_unfocused_exit(XdgExitContext { + has_keyboard_focus: true, + ..unfocused_stay_mode + })); + assert!(!should_defer_xdg_unfocused_exit(XdgExitContext { + stay_mode: false, + ..unfocused_stay_mode + })); + assert!(!should_defer_xdg_unfocused_exit(XdgExitContext { + is_xdg_window: false, + ..unfocused_stay_mode + })); // Desktop-open handoff marks the close explicit so stay-mode does not // cancel exit and reactivate over the opened application. Exit-after- // capture uses the same explicit-close bit for the same reason. - assert!(!should_defer_xdg_unfocused_exit(true, true, false, true)); + assert!(!should_defer_xdg_unfocused_exit(XdgExitContext { + explicit_close_requested: true, + ..unfocused_stay_mode + })); } #[test] diff --git a/src/backend/wayland/backend/event_loop/session_save.rs b/src/backend/wayland/backend/event_loop/session_save.rs index a1ad77853..636f8eaec 100644 --- a/src/backend/wayland/backend/event_loop/session_save.rs +++ b/src/backend/wayland/backend/event_loop/session_save.rs @@ -1,12 +1,11 @@ use super::super::super::state::WaylandState; -use crate::{ - backend::wayland::session::{ - self as runtime_session, PersistenceCompletion, PersistenceOperation, PersistenceOutcome, - SaveStrategy, SessionState, SubmitFailure, - }, - session, - session::SaveSnapshotReport, +use crate::backend::wayland::session::{ + self as runtime_session, ContentlessSaveCheck, PersistenceCompletion, PersistenceOperation, + PersistenceOutcome, SaveStrategy, SessionState, SubmitFailure, }; +use crate::session; +#[cfg(test)] +use crate::session::SaveSnapshotReport; use std::time::{Duration, Instant}; mod diagnostics; @@ -17,14 +16,14 @@ use diagnostics::{SessionSaveReason, log_session_save_result, log_snapshot_captu use final_save::persist_final_session_and_shutdown; pub(in crate::backend::wayland) use interaction::should_defer_for_interaction; +#[cfg(test)] use interaction::{ - defer_autosave_for_active_interaction, finalize_spotlight_wheel_for_shutdown_persistence, - min_optional_timeout, + PersistenceInteractions, defer_pending_autosave_for_interaction, + input_persistence_interaction_active, persistence_interaction_active, }; -#[cfg(test)] use interaction::{ - defer_pending_autosave_for_interaction, input_persistence_interaction_active, - persistence_interaction_active, + defer_autosave_for_active_interaction, finalize_spotlight_wheel_for_shutdown_persistence, + min_optional_timeout, }; pub(super) use notifications::notify_session_failure; @@ -320,7 +319,7 @@ pub(in crate::backend::wayland) fn drain_persistence_completion_for_runtime( pub(in crate::backend::wayland) fn report_autosave_success( state: &mut WaylandState, - save: runtime_session::SaveCompletion, + save: &runtime_session::SaveCompletion, execution_time: Duration, ) { log_session_save_result( @@ -442,13 +441,13 @@ fn should_skip_unloaded_contentless_save( "unexpected session artifact inspection outcome" )); }; - let skip = runtime_session::should_skip_unloaded_contentless_save( - state.session.has_loaded_board_data(), - state.session.is_dirty(), - state.input_state.is_session_dirty(), + let skip = runtime_session::should_skip_unloaded_contentless_save(ContentlessSaveCheck { + loaded_board_data: state.session.has_loaded_board_data(), + session_dirty: state.session.is_dirty(), + input_dirty: state.input_state.is_session_dirty(), has_board_data, - has_artifacts, - ); + session_artifact_exists: has_artifacts, + }); if skip { log::warn!( "Skipping session save to {} because no session was loaded, no session changes were recorded, and the current snapshot has no board data", diff --git a/src/backend/wayland/backend/event_loop/session_save/diagnostics.rs b/src/backend/wayland/backend/event_loop/session_save/diagnostics.rs index d4fba2573..9354b235d 100644 --- a/src/backend/wayland/backend/event_loop/session_save/diagnostics.rs +++ b/src/backend/wayland/backend/event_loop/session_save/diagnostics.rs @@ -1,4 +1,6 @@ -use super::*; +use crate::session; +use crate::session::SaveSnapshotReport; +use std::time::Duration; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum SessionSaveReason { diff --git a/src/backend/wayland/backend/event_loop/session_save/final_save.rs b/src/backend/wayland/backend/event_loop/session_save/final_save.rs index 013364ac2..465b2a615 100644 --- a/src/backend/wayland/backend/event_loop/session_save/final_save.rs +++ b/src/backend/wayland/backend/event_loop/session_save/final_save.rs @@ -1,6 +1,22 @@ -use std::path::PathBuf; +use std::ffi::OsStr; +use std::fmt::{Display, Formatter}; +use std::path::{Path, PathBuf}; +use std::time::SystemTime; +use std::{error, fmt, time}; -use super::*; +use super::diagnostics::{SessionSaveReason, log_session_save_result, log_snapshot_capture}; +use super::notifications::notify_session_save_report; +use super::{ + final_save_barrier_policy, observe_input_dirty, persistence_barrier, run_persistence_operation, + runtime_session, should_skip_disabled_final_save, should_skip_protected_session_save, + should_skip_unloaded_contentless_save, snapshot_or_empty, +}; +use crate::backend::wayland::session::{ + ContentlessSaveCheck, PersistenceOperation, PersistenceOutcome, SaveStrategy, +}; +use crate::backend::wayland::state::WaylandState; +use crate::session; +use std::time::{Duration, Instant}; pub(super) fn persist_final_session_and_shutdown( state: &mut WaylandState, @@ -51,7 +67,7 @@ fn persist_final_session_direct(state: &mut WaylandState) -> Result<(), anyhow:: { Ok(true) => {} Ok(false) => return Ok(()), - Err(refusal) => return save_refused_drawings(state, &options, refusal, SideWrite::Direct), + Err(refusal) => return save_refused_drawings(state, &options, &refusal, SideWrite::Direct), } if should_skip_disabled_final_save(&options) { return Ok(()); @@ -65,13 +81,13 @@ fn persist_final_session_direct(state: &mut WaylandState) -> Result<(), anyhow:: let has_board_data = snapshot .as_ref() .is_some_and(session::SessionSnapshot::has_board_data); - if runtime_session::should_skip_unloaded_contentless_save( - state.session.has_loaded_board_data(), - state.session.is_dirty(), - state.input_state.is_session_dirty(), + if runtime_session::should_skip_unloaded_contentless_save(ContentlessSaveCheck { + loaded_board_data: state.session.has_loaded_board_data(), + session_dirty: state.session.is_dirty(), + input_dirty: state.input_state.is_session_dirty(), has_board_data, - runtime_session::has_session_artifact(&options), - ) { + session_artifact_exists: runtime_session::has_session_artifact(&options), + }) { return Ok(()); } let snapshot = snapshot_or_empty(state, &options, snapshot)?; @@ -129,7 +145,7 @@ fn persist_final_session(state: &mut WaylandState) -> Result<(), anyhow::Error> { Ok(true) => {} Ok(false) => return Ok(()), - Err(refusal) => return save_refused_drawings(state, &options, refusal, SideWrite::Worker), + Err(refusal) => return save_refused_drawings(state, &options, &refusal, SideWrite::Worker), } if should_skip_disabled_final_save(&options) { return Ok(()); @@ -200,8 +216,8 @@ pub(super) struct RefusedDrawingsSaved { pub(super) recovery: Option, } -impl std::fmt::Display for RefusedDrawingsSaved { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for RefusedDrawingsSaved { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { write!( formatter, "drawings that could not be written to {} were saved to {}", @@ -215,7 +231,7 @@ impl std::fmt::Display for RefusedDrawingsSaved { } } -impl std::error::Error for RefusedDrawingsSaved {} +impl error::Error for RefusedDrawingsSaved {} #[derive(Clone, Copy)] enum SideWrite { @@ -229,7 +245,7 @@ enum SideWrite { fn save_refused_drawings( state: &mut WaylandState, options: &session::SessionOptions, - refusal: anyhow::Error, + refusal: &anyhow::Error, write: SideWrite, ) -> Result<(), anyhow::Error> { let side = refused_drawings_options(options); @@ -341,19 +357,19 @@ fn save_side_session_as( /// `.unsaved-.wayscriber-session` beside the session file. fn refused_drawings_options(options: &session::SessionOptions) -> session::SessionOptions { let path = options.session_file_path(); - let millis = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) + let millis = SystemTime::now() + .duration_since(time::UNIX_EPOCH) .map(|elapsed| elapsed.as_millis()) .unwrap_or_default(); let mut name = path .file_stem() - .map(std::ffi::OsStr::to_os_string) + .map(OsStr::to_os_string) .unwrap_or_else(|| "session".into()); name.push(format!(".unsaved-{millis}.wayscriber-session")); let parent = path .parent() .filter(|parent| !parent.as_os_str().is_empty()) - .unwrap_or_else(|| std::path::Path::new(".")); + .unwrap_or_else(|| Path::new(".")); let mut side = options.clone(); side.set_named_file_target(parent.join(name)); diff --git a/src/backend/wayland/backend/event_loop/session_save/interaction.rs b/src/backend/wayland/backend/event_loop/session_save/interaction.rs index cbb254a4c..cbce10f01 100644 --- a/src/backend/wayland/backend/event_loop/session_save/interaction.rs +++ b/src/backend/wayland/backend/event_loop/session_save/interaction.rs @@ -1,33 +1,45 @@ -use super::*; +use crate::input::{DrawingState, InputState}; + +use super::runtime_session; +use crate::backend::wayland::session::SessionState; +use crate::backend::wayland::state::WaylandState; +use crate::session; +use std::time::{Duration, Instant}; pub(in crate::backend::wayland) fn should_defer_for_interaction(state: &WaylandState) -> bool { - persistence_interaction_active( - input_persistence_interaction_active(&state.input_state), - state.toolbar_drag.item_dragging(), - state.toolbar_drag.is_moving(), - state.pointer.board_pan_active(), - state.zoom_panning_active(), - stylus_tip_down(state), - ) + persistence_interaction_active(PersistenceInteractions { + input: input_persistence_interaction_active(&state.input_state), + toolbar_drag: state.toolbar_drag.item_dragging(), + move_drag: state.toolbar_drag.is_moving(), + board_pan: state.pointer.board_pan_active(), + zoom_pan: state.zoom_panning_active(), + stylus_tip: stylus_tip_down(state), + }) } -pub(super) fn persistence_interaction_active( - input: bool, - toolbar_drag: bool, - move_drag: bool, - board_pan: bool, - zoom_pan: bool, - stylus_tip: bool, -) -> bool { - input || toolbar_drag || move_drag || board_pan || zoom_pan || stylus_tip +/// The in-progress interactions that hold off session persistence. +#[derive(Debug, Clone, Copy, Default)] +pub(super) struct PersistenceInteractions { + pub(super) input: bool, + pub(super) toolbar_drag: bool, + pub(super) move_drag: bool, + pub(super) board_pan: bool, + pub(super) zoom_pan: bool, + pub(super) stylus_tip: bool, +} + +pub(super) fn persistence_interaction_active(active: PersistenceInteractions) -> bool { + active.input + || active.toolbar_drag + || active.move_drag + || active.board_pan + || active.zoom_pan + || active.stylus_tip } -pub(super) fn input_persistence_interaction_active(input_state: &crate::input::InputState) -> bool { +pub(super) fn input_persistence_interaction_active(input_state: &InputState) -> bool { input_state.has_active_pointer_interaction() - || matches!( - input_state.state, - crate::input::DrawingState::TextInput { .. } - ) + || matches!(input_state.state, DrawingState::TextInput { .. }) || input_state.has_pending_spotlight_magnification_gesture() } @@ -67,7 +79,7 @@ pub(super) fn defer_autosave_for_active_interaction( } pub(super) fn finalize_spotlight_wheel_for_shutdown_persistence( - input_state: &mut crate::input::InputState, + input_state: &mut InputState, spotlight_wheel_idle_deadline: &mut Option, ) { input_state.flush_spotlight_magnification_gesture(); diff --git a/src/backend/wayland/backend/event_loop/session_save/notifications.rs b/src/backend/wayland/backend/event_loop/session_save/notifications.rs index a8f529ed9..902aaaa29 100644 --- a/src/backend/wayland/backend/event_loop/session_save/notifications.rs +++ b/src/backend/wayland/backend/event_loop/session_save/notifications.rs @@ -1,11 +1,11 @@ -use super::*; +use crate::backend::wayland::session::SessionState; +use crate::backend::wayland::state::WaylandState; +use crate::config::{Action, Config}; use crate::i18n::{tr, trf}; use crate::input::state::{Toast, ToastPriority}; -use crate::{ - config::{Action, Config}, - notification, - session::SaveSnapshotOutcome, -}; +use crate::session::{SaveSnapshotOutcome, SaveSnapshotReport}; +use crate::{notification, session}; +use std::time::Instant; const SESSION_SAVE_NOTIFICATION_TIMEOUT_MS: i32 = 15_000; const SESSION_SAVE_WARNING_TOAST_MS: u64 = 20_000; @@ -267,26 +267,30 @@ fn suggested_limit_mb(projected_written_size: u64, current_limit_bytes: u64) -> } fn config_path_display() -> String { - Config::get_config_path() + Config::config_path() .map(|path| path.display().to_string()) .unwrap_or_else(|_| "~/.config/wayscriber/config.toml".to_string()) } #[cfg(test)] mod tests { + use crate::draw::TextMeasurer; + use crate::input::{InputState, MouseButton}; + use crate::test_temp; + use super::*; #[test] fn real_size_failures_name_the_limit_and_durable_recovery_without_save_as_advice() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = session::SessionOptions::new(temp.path().to_path_buf(), "size-notice"); options.persist_transparent = true; options.max_file_size_bytes = 1; - let measurer = crate::draw::TextMeasurer::default(); - let mut input = crate::input::InputState::from_config(&Config::default()); - input.on_mouse_press(crate::input::MouseButton::Left, 300, 300); + let measurer = TextMeasurer::default(); + let mut input = InputState::from_config(&Config::default()); + input.on_mouse_press(MouseButton::Left, 300, 300); input.on_mouse_motion(400, 400); - input.on_mouse_release(crate::input::MouseButton::Left, 400, 400); + input.on_mouse_release(MouseButton::Left, 400, 400); let snapshot = input .snapshot_for_persistence_with(&measurer, &options) .unwrap(); diff --git a/src/backend/wayland/backend/event_loop/session_save/tests.rs b/src/backend/wayland/backend/event_loop/session_save/tests.rs index 7caf529c0..ee2b684ee 100644 --- a/src/backend/wayland/backend/event_loop/session_save/tests.rs +++ b/src/backend/wayland/backend/event_loop/session_save/tests.rs @@ -1,20 +1,29 @@ +use std::fs; + +use crate::config::Config; +use crate::draw::TextMeasurer; +use crate::input::MouseButton; +use crate::test_temp; +use crate::ui_text::UiTextEngine; + use super::*; use crate::backend::wayland::session::{PersistenceController, PersistenceOperation}; use crate::config::Action; use crate::draw::Shape; -use crate::input::state::{SpotlightWheelOutcome, test_support::make_test_input_state}; +use crate::input::state::SpotlightWheelOutcome; +use crate::input::state::test_support::make_test_input_state; use crate::session::SaveSnapshotOutcome; use std::path::PathBuf; #[test] fn clean_failed_initial_load_exits_without_a_save_failure_or_a_source_write() { use crate::backend::wayland::handlers::test_support::HandlerFixture; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = session::SessionOptions::new(temp.path().to_path_buf(), "failed-startup"); options.persist_transparent = true; options.set_named_file_target(temp.path().join("failed.wayscriber-session")); - std::fs::write(options.session_file_path(), b"unreadable").unwrap(); - std::fs::write( + fs::write(options.session_file_path(), b"unreadable").unwrap(); + fs::write( session::append_path_suffix( &options.session_file_path(), ".corrupt-18446744073709551615", @@ -22,7 +31,7 @@ fn clean_failed_initial_load_exits_without_a_save_failure_or_a_source_write() { b"occupied", ) .unwrap(); - let mut fixture = HandlerFixture::new(crate::config::Config::default()); + let mut fixture = HandlerFixture::new(Config::default()); fixture.state.session = SessionState::new(Some(options.clone())); fixture .state @@ -31,7 +40,7 @@ fn clean_failed_initial_load_exits_without_a_save_failure_or_a_source_write() { assert!(!fixture.state.session.is_dirty()); persist_session(&mut fixture.state).unwrap(); assert_eq!( - std::fs::read(options.session_file_path()).unwrap(), + fs::read(options.session_file_path()).unwrap(), b"unreadable" ); assert!(!options.backup_file_path().exists()); @@ -50,14 +59,14 @@ fn drawings_a_never_loaded_session_refuses_are_saved_beside_it_at_exit() { fn assert_refused_drawings_are_saved_beside_the_session(worker_lost: bool, oversized: bool) { use crate::backend::wayland::handlers::test_support::HandlerFixture; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = session::SessionOptions::new(temp.path().to_path_buf(), "failed-startup"); options.persist_transparent = true; options.set_named_file_target(temp.path().join("failed.wayscriber-session")); options.compression = session::CompressionMode::Off; options.max_file_size_bytes = 4096; - std::fs::write(options.session_file_path(), b"unreadable").unwrap(); - std::fs::write( + fs::write(options.session_file_path(), b"unreadable").unwrap(); + fs::write( session::append_path_suffix( &options.session_file_path(), ".corrupt-18446744073709551615", @@ -65,7 +74,7 @@ fn assert_refused_drawings_are_saved_beside_the_session(worker_lost: bool, overs b"occupied", ) .unwrap(); - let mut fixture = HandlerFixture::new(crate::config::Config::default()); + let mut fixture = HandlerFixture::new(Config::default()); fixture.state.session = SessionState::new(Some(options.clone())); fixture .state @@ -75,9 +84,9 @@ fn assert_refused_drawings_are_saved_beside_the_session(worker_lost: bool, overs for stroke in 0..strokes { let y = 100 + stroke; let input = &mut fixture.state.input_state; - input.on_mouse_press(crate::input::MouseButton::Left, 600, y); + input.on_mouse_press(MouseButton::Left, 600, y); input.on_mouse_motion(620, y + 20); - input.on_mouse_release(crate::input::MouseButton::Left, 620, y + 20); + input.on_mouse_release(MouseButton::Left, 620, y + 20); } if worker_lost { fixture @@ -107,7 +116,7 @@ fn assert_refused_drawings_are_saved_beside_the_session(worker_lost: bool, overs assert_eq!(kept.boards[0].pages.pages[0].shapes.len(), strokes as usize); assert_eq!(saved.recovery, oversized.then(|| side.recovery_file_path())); assert_eq!( - std::fs::read(options.session_file_path()).unwrap(), + fs::read(options.session_file_path()).unwrap(), b"unreadable", "the session that never loaded is not written" ); @@ -136,20 +145,22 @@ fn final_save_does_not_retry_through_an_unhealthy_worker() { #[test] fn pointer_and_stylus_both_gate_persistence_transitions() { - assert!(persistence_interaction_active( - true, false, false, false, false, false - )); - assert!(persistence_interaction_active( - false, false, false, false, false, true - )); + assert!(persistence_interaction_active(PersistenceInteractions { + input: true, + ..PersistenceInteractions::default() + })); + assert!(persistence_interaction_active(PersistenceInteractions { + stylus_tip: true, + ..PersistenceInteractions::default() + })); assert!(!persistence_interaction_active( - false, false, false, false, false, false + PersistenceInteractions::default() )); } #[test] fn a_due_autosave_is_deferred_while_spotlight_wheel_history_is_pending() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = make_test_input_state(); input.boards.active_frame_mut().add_shape(Shape::Spotlight { @@ -188,8 +199,8 @@ fn a_due_autosave_is_deferred_while_spotlight_wheel_history_is_pending() { #[test] fn shutdown_persistence_records_wheel_history_before_capturing_the_snapshot() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -216,10 +227,10 @@ fn shutdown_persistence_records_wheel_history_before_capturing_the_snapshot() { options.persist_transparent = true; options.persist_history = true; let snapshot = input - .snapshot_for_persistence_with(&crate::draw::TextMeasurer::default(), &options) + .snapshot_for_persistence_with(&TextMeasurer::default(), &options) .expect("changed loupe snapshot"); let mut restored = make_test_input_state(); - crate::session::apply_snapshot(&mut restored, snapshot, &options); + session::apply_snapshot(&mut restored, snapshot, &options); restored.handle_action_with_resources(test_text_resources, Action::Undo); let Shape::Spotlight { magnification, .. } = restored .boards @@ -374,7 +385,7 @@ fn record_autosave_failure_notifies_only_once_until_saved() { options.autosave_interval = Duration::from_millis(1); options.autosave_failure_backoff = Duration::from_millis(50); - let mut state = crate::backend::wayland::session::SessionState::new(Some(options.clone())); + let mut state = SessionState::new(Some(options.clone())); let now = Instant::now(); state.record_input_dirty(now, true); @@ -395,7 +406,7 @@ fn record_autosave_success_clears_dirty_state_when_saved() { options.autosave_idle = Duration::from_millis(1); options.autosave_interval = Duration::from_millis(1); - let mut state = crate::backend::wayland::session::SessionState::new(Some(options.clone())); + let mut state = SessionState::new(Some(options.clone())); state.commit_output_options(state.options().unwrap().clone(), true); let now = Instant::now(); state.record_input_dirty(now, true); diff --git a/src/backend/wayland/backend/helpers.rs b/src/backend/wayland/backend/helpers.rs index 10e3cb9b0..7f24f75b4 100644 --- a/src/backend/wayland/backend/helpers.rs +++ b/src/backend/wayland/backend/helpers.rs @@ -1,9 +1,13 @@ +use std::io; +use std::io::ErrorKind; + use anyhow::{Context, Result}; use log::warn; use std::env; use std::os::fd::{AsRawFd, RawFd}; use std::time::Duration; -use wayland_client::{EventQueue, backend::ReadEventsGuard, backend::WaylandError}; +use wayland_client::EventQueue; +use wayland_client::backend::{ReadEventsGuard, WaylandError}; use super::super::state::WaylandState; #[cfg(test)] @@ -51,7 +55,7 @@ pub(super) fn friendly_capture_error(kind: CaptureFailureKind) -> String { fn normalize_read_result(result: Result) -> Result { match result { Ok(n) => Ok(n), - Err(WaylandError::Io(err)) if err.kind() == std::io::ErrorKind::WouldBlock => Ok(0), + Err(WaylandError::Io(err)) if err.kind() == ErrorKind::WouldBlock => Ok(0), Err(e) => Err(e), } } @@ -87,8 +91,8 @@ fn poll_runtime_fds_with( wayland_fd: RawFd, wake_fd: RawFd, timeout: Option, - mut poll_once: impl FnMut(&mut [libc::pollfd], i32) -> std::io::Result, -) -> std::io::Result { + mut poll_once: impl FnMut(&mut [libc::pollfd], i32) -> io::Result, +) -> io::Result { poll_with_retry(timeout, |timeout_ms| { let mut pollfds = [ libc::pollfd { @@ -121,7 +125,7 @@ fn poll_runtime_fds_with( )?, }; if !readiness.wayland && !readiness.wake { - return Err(std::io::Error::other( + return Err(io::Error::other( "runtime poll reported readiness without a readable descriptor", )); } @@ -142,7 +146,7 @@ fn poll_runtime_fds( wayland_fd: RawFd, wake_fd: RawFd, timeout: Option, -) -> std::io::Result { +) -> io::Result { poll_runtime_fds_with(wayland_fd, wake_fd, timeout, |pollfds, timeout_ms| { // SAFETY: pollfds is a live mutable slice for the duration of this call, // and both descriptors remain borrowed by their runtime owners. @@ -154,7 +158,7 @@ fn poll_runtime_fds( ) }; if ready < 0 { - Err(std::io::Error::last_os_error()) + Err(io::Error::last_os_error()) } else { Ok(ready) } @@ -231,10 +235,10 @@ fn dispatch_runtime_cycle( Ok(()) } -struct RealRuntimeDispatchOps<'a, F> { - event_queue: &'a mut EventQueue, - state: &'a mut WaylandState, - runtime_wake: &'a RuntimeWakeSource, +struct RealRuntimeDispatchOps<'runtime, F> { + event_queue: &'runtime mut EventQueue, + state: &'runtime mut WaylandState, + runtime_wake: &'runtime RuntimeWakeSource, on_runtime_wake: F, } @@ -321,6 +325,8 @@ pub(super) fn launched_resume_policy() -> Option { #[cfg(test)] mod tests { + use crate::test_env; + use super::*; use crate::capture::CaptureError; use crate::set_runtime_session_override; @@ -344,17 +350,17 @@ mod tests { #[test] fn normalize_read_result_maps_would_block_to_zero() { - let err = WaylandError::Io(std::io::Error::from(std::io::ErrorKind::WouldBlock)); + let err = WaylandError::Io(io::Error::from(ErrorKind::WouldBlock)); assert_eq!(normalize_read_result(Err(err)).unwrap(), 0); } #[test] fn normalize_read_result_preserves_other_errors() { - let err = WaylandError::Io(std::io::Error::from(std::io::ErrorKind::BrokenPipe)); + let err = WaylandError::Io(io::Error::from(ErrorKind::BrokenPipe)); let actual = normalize_read_result(Err(err)).unwrap_err(); match actual { WaylandError::Io(io_err) => { - assert_eq!(io_err.kind(), std::io::ErrorKind::BrokenPipe); + assert_eq!(io_err.kind(), ErrorKind::BrokenPipe); } other => panic!("expected io error, got {other}"), } @@ -363,7 +369,7 @@ mod tests { #[test] fn runtime_poll_observes_wake_only_readiness() { let (wayland_read, _wayland_write) = UnixStream::pair().unwrap(); - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); wake.handle().wake().unwrap(); assert_eq!( @@ -383,7 +389,7 @@ mod tests { #[test] fn runtime_poll_observes_wayland_only_readiness() { let (wayland_read, mut wayland_write) = UnixStream::pair().unwrap(); - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); wayland_write.write_all(&[1]).unwrap(); assert_eq!( @@ -403,7 +409,7 @@ mod tests { #[test] fn runtime_poll_preserves_readable_wayland_data_when_peer_hangs_up() { let (wayland_read, mut wayland_write) = UnixStream::pair().unwrap(); - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); wayland_write.write_all(&[1]).unwrap(); drop(wayland_write); @@ -424,7 +430,7 @@ mod tests { #[test] fn runtime_wake_preempts_a_future_deadline() { let (wayland_read, _wayland_write) = UnixStream::pair().unwrap(); - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); wake.handle().wake().unwrap(); assert_eq!( @@ -444,7 +450,7 @@ mod tests { #[test] fn runtime_poll_observes_combined_wayland_and_wake_readiness() { let (wayland_read, mut wayland_write) = UnixStream::pair().unwrap(); - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); wayland_write.write_all(&[1]).unwrap(); wake.handle().wake().unwrap(); @@ -510,7 +516,7 @@ mod tests { fn wake_only_readiness_cancels_the_prepared_wayland_read() { let (wayland_read, _wayland_write) = UnixStream::pair().unwrap(); let (guard, reads, cancellations) = fake_prepared_read(wayland_read); - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); wake.handle().wake().unwrap(); let outcome = read_events_with_runtime_wake(guard, &wake, Some(Duration::ZERO)).unwrap(); @@ -526,7 +532,7 @@ mod tests { let (wayland_read, mut wayland_write) = UnixStream::pair().unwrap(); wayland_write.write_all(&[1]).unwrap(); let (guard, reads, cancellations) = fake_prepared_read(wayland_read); - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); wake.handle().wake().unwrap(); let outcome = read_events_with_runtime_wake(guard, &wake, Some(Duration::ZERO)).unwrap(); @@ -682,7 +688,7 @@ mod tests { let readiness = poll_runtime_fds_with(10, 11, None, |pollfds, _| { calls += 1; if calls == 1 { - return Err(std::io::Error::from(std::io::ErrorKind::Interrupted)); + return Err(io::Error::from(ErrorKind::Interrupted)); } pollfds[1].revents = libc::POLLIN; Ok(1) @@ -801,8 +807,7 @@ mod tests { #[test] fn a_failed_file_save_is_not_reported_as_a_screen_sharing_permission() { - let error = - CaptureError::SaveError(std::io::Error::from(std::io::ErrorKind::PermissionDenied)); + let error = CaptureError::SaveError(io::Error::from(ErrorKind::PermissionDenied)); assert_eq!( friendly_capture_error(error.failure_kind()), @@ -812,11 +817,11 @@ mod tests { #[test] fn resume_override_from_env_prefers_runtime_override() { - let _guard = crate::test_env::lock(); + let _guard = test_env::lock(); // SAFETY: test serialized by env mutex. unsafe { - std::env::set_var(RESUME_SESSION_ENV, "off"); + env::set_var(RESUME_SESSION_ENV, "off"); } set_runtime_session_override(Some(true)); @@ -825,36 +830,36 @@ mod tests { set_runtime_session_override(None); // SAFETY: test serialized by env mutex. unsafe { - std::env::remove_var(RESUME_SESSION_ENV); + env::remove_var(RESUME_SESSION_ENV); } } #[test] fn resume_override_from_env_parses_expected_values() { - let _guard = crate::test_env::lock(); + let _guard = test_env::lock(); set_runtime_session_override(None); // SAFETY: test serialized by env mutex. unsafe { - std::env::set_var(RESUME_SESSION_ENV, "enabled"); + env::set_var(RESUME_SESSION_ENV, "enabled"); } assert_eq!(resume_override_from_env(), Some(true)); // SAFETY: test serialized by env mutex. unsafe { - std::env::set_var(RESUME_SESSION_ENV, "0"); + env::set_var(RESUME_SESSION_ENV, "0"); } assert_eq!(resume_override_from_env(), Some(false)); // SAFETY: test serialized by env mutex. unsafe { - std::env::set_var(RESUME_SESSION_ENV, "maybe"); + env::set_var(RESUME_SESSION_ENV, "maybe"); } assert_eq!(resume_override_from_env(), None); // SAFETY: test serialized by env mutex. unsafe { - std::env::remove_var(RESUME_SESSION_ENV); + env::remove_var(RESUME_SESSION_ENV); } } } diff --git a/src/backend/wayland/backend/mod.rs b/src/backend/wayland/backend/mod.rs index 738bc1662..e4ecf1503 100644 --- a/src/backend/wayland/backend/mod.rs +++ b/src/backend/wayland/backend/mod.rs @@ -2,6 +2,7 @@ // rendering & protocol state to `WaylandState` and its handler modules. use anyhow::{Context, Result}; use std::path::PathBuf; +use tokio::runtime::Runtime; use crate::backend::ExitAfterCaptureMode; @@ -21,18 +22,18 @@ pub struct WaylandBackend { pub(super) exit_after_capture_mode: ExitAfterCaptureMode, pub(super) named_session_file: Option, /// Tokio runtime for async capture operations - pub(super) tokio_runtime: tokio::runtime::Runtime, + pub(super) tokio_runtime: Runtime, } impl WaylandBackend { - pub fn new( + pub fn try_new( initial_mode: Option, freeze_on_start: bool, exit_after_capture_mode: ExitAfterCaptureMode, named_session_file: Option, ) -> Result { - let tokio_runtime = tokio::runtime::Runtime::new() - .context("Failed to create Tokio runtime for capture operations")?; + let tokio_runtime = + Runtime::new().context("Failed to create Tokio runtime for capture operations")?; Ok(Self { initial_mode, freeze_on_start, diff --git a/src/backend/wayland/backend/runtime_wake.rs b/src/backend/wayland/backend/runtime_wake.rs index 172e27dd6..05db280a9 100644 --- a/src/backend/wayland/backend/runtime_wake.rs +++ b/src/backend/wayland/backend/runtime_wake.rs @@ -68,7 +68,7 @@ pub(super) fn poll_with_retry( } impl RuntimeWakeSource { - pub(crate) fn new() -> io::Result { + pub(crate) fn try_new() -> io::Result { // SAFETY: eventfd returns a new owned descriptor on success. EFD_NONBLOCK // keeps both producer writes and consumer drains bounded, and EFD_CLOEXEC // prevents subprocesses from extending the runtime descriptor lifetime. @@ -208,8 +208,8 @@ impl RuntimeWakeHandle { #[cfg(test)] mod tests { use std::sync::mpsc; - use std::thread; use std::time::{Duration, Instant}; + use std::{fs, thread}; use super::*; @@ -229,7 +229,7 @@ mod tests { let stat_path = format!("/proc/self/task/{tid}/stat"); let deadline = Instant::now() + Duration::from_secs(1); loop { - let state = std::fs::read_to_string(&stat_path).ok().and_then(|stat| { + let state = fs::read_to_string(&stat_path).ok().and_then(|stat| { stat.rsplit_once(") ") .and_then(|(_, suffix)| suffix.chars().next()) }); @@ -249,7 +249,7 @@ mod tests { #[test] fn wake_before_poll_is_observed_and_drained() { - let source = RuntimeWakeSource::new().unwrap(); + let source = RuntimeWakeSource::try_new().unwrap(); source.handle().wake().unwrap(); assert!(poll_readable(&source, 0)); @@ -259,7 +259,7 @@ mod tests { #[test] fn wake_unblocks_a_waiting_poll() { - let source = RuntimeWakeSource::new().unwrap(); + let source = RuntimeWakeSource::try_new().unwrap(); let handle = source.handle(); let (poll_entry_tx, poll_entry_rx) = mpsc::channel(); let poller = thread::spawn(move || { @@ -278,7 +278,7 @@ mod tests { #[test] fn multiple_wakes_coalesce() { - let source = RuntimeWakeSource::new().unwrap(); + let source = RuntimeWakeSource::try_new().unwrap(); let handle = source.handle(); for _ in 0..32 { handle.wake().unwrap(); @@ -290,7 +290,7 @@ mod tests { #[test] fn handle_can_safely_outlive_source() { - let source = RuntimeWakeSource::new().unwrap(); + let source = RuntimeWakeSource::try_new().unwrap(); let handle = source.handle(); drop(source); @@ -299,13 +299,13 @@ mod tests { #[test] fn empty_drain_reports_no_consumed_wake() { - let source = RuntimeWakeSource::new().unwrap(); + let source = RuntimeWakeSource::try_new().unwrap(); assert!(!source.drain().unwrap()); } #[test] fn wait_readable_times_out_without_a_wake() { - let source = RuntimeWakeSource::new().unwrap(); + let source = RuntimeWakeSource::try_new().unwrap(); assert!(!source.wait_readable(Some(Duration::ZERO)).unwrap()); } diff --git a/src/backend/wayland/backend/setup.rs b/src/backend/wayland/backend/setup.rs index 0ea381f94..4b617e098 100644 --- a/src/backend/wayland/backend/setup.rs +++ b/src/backend/wayland/backend/setup.rs @@ -1,32 +1,30 @@ +use std::env; + use anyhow::{Context, Result}; use log::{debug, warn}; use smithay_client_toolkit::globals::ProvidesBoundGlobal; -use smithay_client_toolkit::{ - activation::ActivationState, - compositor::CompositorState, - output::OutputState, - registry::RegistryState, - seat::{ - SeatState, pointer_constraints::PointerConstraintsState, - relative_pointer::RelativePointerState, - }, - shell::{wlr_layer::LayerShell, xdg::XdgShell}, - shm::Shm, -}; -use wayland_client::{Connection, EventQueue, globals::registry_queue_init}; -use wayland_protocols::ext::{ - image_capture_source::v1::client::ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1, - image_copy_capture::v1::client::ext_image_copy_capture_manager_v1::ExtImageCopyCaptureManagerV1, -}; +use smithay_client_toolkit::activation::ActivationState; +use smithay_client_toolkit::compositor::CompositorState; +use smithay_client_toolkit::output::OutputState; +use smithay_client_toolkit::registry::RegistryState; +use smithay_client_toolkit::seat::SeatState; +use smithay_client_toolkit::seat::pointer_constraints::PointerConstraintsState; +use smithay_client_toolkit::seat::relative_pointer::RelativePointerState; +use smithay_client_toolkit::shell::wlr_layer::LayerShell; +use smithay_client_toolkit::shell::xdg::XdgShell; +use smithay_client_toolkit::shm::Shm; +use wayland_client::{Connection, EventQueue}; +use wayland_client::globals::registry_queue_init; +use wayland_protocols::ext::image_capture_source::v1::client::ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1; +use wayland_protocols::ext::image_copy_capture::v1::client::ext_image_copy_capture_manager_v1::ExtImageCopyCaptureManagerV1; use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1; +use wayland_client::globals::GlobalList; use crate::env_vars::{XDG_CURRENT_DESKTOP_ENV, XDG_SESSION_DESKTOP_ENV}; -use super::super::{ - frozen::ExtImageCopyManagers, - state::{ProtocolGlobals, ProtocolGlobalsSeed, WaylandState}, -}; +use super::super::frozen::ExtImageCopyManagers; +use super::super::state::{ProtocolGlobals, ProtocolGlobalsSeed, WaylandState}; // Freeze/zoom capture currently consumes wl_shm buffer events and ignores linux-dmabuf. // Version 3 can negotiate linux-dmabuf-only frames on newer wlroots/NVIDIA stacks, so @@ -36,7 +34,7 @@ const MAX_SHM_SCREENCOPY_VERSION: u32 = 2; pub(super) struct WaylandSetup { pub(super) conn: Connection, #[cfg(feature = "tablet-input")] - pub(super) globals: wayland_client::globals::GlobalList, + pub(super) globals: GlobalList, pub(super) event_queue: EventQueue, pub(super) qh: wayland_client::QueueHandle, pub(super) state_globals: ProtocolGlobals, @@ -53,7 +51,7 @@ type ShellGlobals = ( ); fn bind_shell_globals( - globals: &wayland_client::globals::GlobalList, + globals: &GlobalList, qh: &wayland_client::QueueHandle, ) -> Result { let layer_shell = match LayerShell::bind(globals, qh) { @@ -63,9 +61,9 @@ fn bind_shell_globals( } Err(err) => { let desktop_env = - std::env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_else(|_| "unknown".into()); + env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_else(|_| "unknown".into()); let session_env = - std::env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_else(|_| "unknown".into()); + env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_else(|_| "unknown".into()); warn!( "Layer shell not available: {} (desktop='{}', session='{}'); toolbars will be disabled and xdg fallback may not cover docks/panels.", err, desktop_env, session_env @@ -102,7 +100,7 @@ fn bind_shell_globals( } fn bind_capture_globals( - globals: &wayland_client::globals::GlobalList, + globals: &GlobalList, qh: &wayland_client::QueueHandle, ) -> ( Option, @@ -149,7 +147,7 @@ fn bind_capture_globals( } fn bind_text_input_manager( - globals: &wayland_client::globals::GlobalList, + globals: &GlobalList, qh: &wayland_client::QueueHandle, ) -> Option { match globals.bind::(qh, 1..=1, ()) { diff --git a/src/backend/wayland/backend/signals.rs b/src/backend/wayland/backend/signals.rs index 6714efb15..455448d80 100644 --- a/src/backend/wayland/backend/signals.rs +++ b/src/backend/wayland/backend/signals.rs @@ -1,19 +1,20 @@ use std::io; -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, -}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(unix)] use libc::{SIGINT, SIGTERM, SIGUSR1, SIGUSR2}; +use crate::unix_signals; +use crate::unix_signals::{SignalListener, SignalListenerHealth}; + use super::runtime_wake::RuntimeWakeHandle; pub(super) struct OverlaySignalState { exit_requested: Arc, tray_action_requested: Arc, #[cfg(unix)] - listener: crate::unix_signals::SignalListener, + listener: SignalListener, } impl OverlaySignalState { @@ -29,9 +30,7 @@ impl OverlaySignalState { #[cfg(unix)] { match self.listener.health() { - crate::unix_signals::SignalListenerHealth::Failed(failure) => { - Some(failure.to_string()) - } + SignalListenerHealth::Failed(failure) => Some(failure.to_string()), _ => None, } } @@ -73,7 +72,7 @@ pub(super) fn setup_signal_handlers( let signal_exit_requested = Arc::clone(&exit_requested); let signal_tray_action_requested = Arc::clone(&tray_action_requested); let listener_wake = runtime_wake; - let listener = crate::unix_signals::spawn_listener( + let listener = unix_signals::spawn_listener( &[SIGTERM, SIGINT, SIGUSR1, SIGUSR2], move |signal| match signal { SIGUSR1 => { @@ -136,11 +135,11 @@ mod tests { #[cfg(unix)] #[test] fn tray_signal_publishes_state_then_wakes_overlay_owner() { - let _guard = crate::unix_signals::test_signal_lock(); - let wake = RuntimeWakeSource::new().unwrap(); + let _guard = unix_signals::test_signal_lock(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut signals = setup_signal_handlers(wake.handle()).unwrap(); - crate::unix_signals::deliver_signal_for_test(SIGUSR2); + unix_signals::deliver_signal_for_test(SIGUSR2); wait_for_wake(&wake); assert!(signals.take_tray_action_requested()); @@ -151,8 +150,8 @@ mod tests { #[cfg(unix)] #[test] fn listener_failure_is_published_before_overlay_owner_wake() { - let _guard = crate::unix_signals::test_signal_lock(); - let wake = RuntimeWakeSource::new().unwrap(); + let _guard = unix_signals::test_signal_lock(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut signals = setup_signal_handlers(wake.handle()).unwrap(); signals.listener.inject_read_error(libc::EIO); diff --git a/src/backend/wayland/backend/state_init/config.rs b/src/backend/wayland/backend/state_init/config.rs index 6da264adb..7f343c01d 100644 --- a/src/backend/wayland/backend/state_init/config.rs +++ b/src/backend/wayland/backend/state_init/config.rs @@ -1,15 +1,17 @@ use log::{debug, info, warn}; +use tokio::runtime::Handle; use crate::backend::ExitAfterCaptureMode; use crate::config::{ Action, Config, ConfigSectionError, ConfigSource, ConfigValidationReport, - DefaultShortcutSkipped, InvalidKeybinding, KeybindingConflictResolution, + DefaultShortcutSkipped, InvalidKeybinding, KeybindingConflictResolution, KeybindingsConfig, }; use crate::i18n::{tr, trf}; use crate::input::InputState; use crate::input::state::{Toast, ToastPriority}; -use crate::notification; use crate::onboarding::OnboardingStore; +use crate::ui::anim; +use crate::{i18n, notification}; /// How long the keybinding warnings stay up. All of them are on the long side: /// they describe a config problem the user has to leave the overlay to fix. @@ -95,8 +97,8 @@ pub(super) fn load(backend_exit_mode: ExitAfterCaptureMode) -> LoadedConfig { } }; - crate::ui::anim::set_motion_enabled(config.ui.reduced_motion.motion_enabled()); - crate::i18n::initialize(config.ui.language); + anim::set_motion_enabled(config.ui.reduced_motion.motion_enabled()); + i18n::initialize(config.ui.language); let exit_after_capture_mode = match backend_exit_mode { ExitAfterCaptureMode::Auto if config.capture.exit_after_capture => { @@ -126,7 +128,7 @@ pub(super) fn load(backend_exit_mode: ExitAfterCaptureMode) -> LoadedConfig { /// the file with nothing but a log line. pub(super) fn notify_config_load_failure( input_state: &mut InputState, - tokio_handle: &tokio::runtime::Handle, + tokio_handle: &Handle, failure: Option<&ConfigLoadFailure>, ) { let Some(failure) = failure else { @@ -190,7 +192,7 @@ pub(super) fn notify_config_load_failure( /// notification covers a launch the user is not looking at. pub(super) fn notify_keybinding_conflicts( input_state: &mut InputState, - tokio_handle: &tokio::runtime::Handle, + tokio_handle: &Handle, conflicts: &[KeybindingConflictResolution], ) { if conflicts.is_empty() { @@ -221,7 +223,7 @@ pub(super) fn notify_keybinding_conflicts( /// forever if this did not say so, since the file keeps it. pub(super) fn notify_invalid_keybindings( input_state: &mut InputState, - tokio_handle: &tokio::runtime::Handle, + tokio_handle: &Handle, invalid: &[InvalidKeybinding], ) { if invalid.is_empty() { @@ -258,7 +260,7 @@ pub(super) fn notify_invalid_keybindings( /// pushed last and styled as information rather than as a warning. pub(super) fn notify_skipped_default_shortcuts( input_state: &mut InputState, - tokio_handle: &tokio::runtime::Handle, + tokio_handle: &Handle, onboarding: &mut OnboardingStore, skipped: &[DefaultShortcutSkipped], ) { @@ -295,9 +297,8 @@ fn skipped_default_notice_id(skipped: &[DefaultShortcutSkipped]) -> String { .iter() .map(|entry| { let skipped_action = entry.config_key().unwrap_or("runtime_action"); - let claimed_by = - crate::config::KeybindingsConfig::config_key_for_action(entry.claimed_by()) - .unwrap_or("runtime_action"); + let claimed_by = KeybindingsConfig::config_key_for_action(entry.claimed_by()) + .unwrap_or("runtime_action"); format!("{}:{skipped_action}:{claimed_by}", entry.binding()) }) .collect::>(); @@ -406,7 +407,7 @@ fn keybinding_notification_body(entries: &[String], closing: &str) -> String { } fn config_path_display() -> String { - Config::get_config_path() + Config::config_path() .map(|path| path.display().to_string()) .unwrap_or_else(|_| "~/.config/wayscriber/config.toml".to_string()) } @@ -442,7 +443,9 @@ fn log_config(config: &Config) { #[cfg(test)] mod tests { - use std::fs; + use std::{fs, iter}; + + use crate::config; use super::*; use crate::config::test_helpers::with_temp_config_home; @@ -450,7 +453,7 @@ mod tests { #[test] fn load_applies_capture_exit_after_capture_to_auto_mode() { with_temp_config_home(|_| { - let path = Config::get_config_path().expect("config path"); + let path = Config::config_path().expect("config path"); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create config dir"); } @@ -470,7 +473,7 @@ mod tests { #[test] fn load_leaves_a_legacy_config_exactly_as_written() { with_temp_config_home(|_| { - let path = Config::get_config_path().expect("config path"); + let path = Config::config_path().expect("config path"); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create config dir"); } @@ -496,13 +499,13 @@ mod tests { #[test] fn load_reports_a_skipped_default_instead_of_resetting_the_section() { with_temp_config_home(|_| { - let path = Config::get_config_path().expect("config path"); + let path = Config::config_path().expect("config path"); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create config dir"); } let original = format!( "config_revision = {}\n\n[keybindings]\ntoggle_toolbar = [\"F2\", \"F9\"]\nexit = [\"Escape\", \"Ctrl+Q\", \"Q\"]\n", - crate::config::CURRENT_CONFIG_REVISION + config::CURRENT_CONFIG_REVISION ); fs::write(&path, &original).expect("write colliding config"); @@ -559,13 +562,13 @@ mod tests { #[test] fn load_reports_a_conflict_between_two_authored_shortcuts() { with_temp_config_home(|_| { - let path = Config::get_config_path().expect("config path"); + let path = Config::config_path().expect("config path"); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create config dir"); } let original = format!( "config_revision = {}\n\n[keybindings]\ntoggle_toolbar = [\"F2\"]\ncycle_toolbar_display = [\"F2\"]\n", - crate::config::CURRENT_CONFIG_REVISION + config::CURRENT_CONFIG_REVISION ); fs::write(&path, &original).expect("write colliding config"); @@ -607,7 +610,7 @@ mod tests { #[test] fn load_reports_an_unparseable_shortcut_instead_of_defaulting_the_section() { with_temp_config_home(|_| { - let path = Config::get_config_path().expect("config path"); + let path = Config::config_path().expect("config path"); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create config dir"); } @@ -615,7 +618,7 @@ mod tests { &path, format!( "config_revision = {}\n\n[keybindings]\nclear_canvas = [\"Ctrl+Shift\"]\nundo = [\"Ctrl+Alt+U\"]\n", - crate::config::CURRENT_CONFIG_REVISION + config::CURRENT_CONFIG_REVISION ), ) .expect("write config with a typo"); @@ -666,7 +669,7 @@ mod tests { ); assert!(toast.contains("and 1 more"), "unexpected toast: {toast}"); - let many = std::iter::repeat_n(conflicts[0].clone(), 7).collect::>(); + let many = iter::repeat_n(conflicts[0].clone(), 7).collect::>(); let body = keybinding_conflict_notification_body(&many, "/tmp/config.toml"); assert_eq!( body.matches('•').count(), @@ -678,7 +681,7 @@ mod tests { #[test] fn load_falls_back_to_defaults_when_config_is_invalid() { with_temp_config_home(|_| { - let path = Config::get_config_path().expect("config path"); + let path = Config::config_path().expect("config path"); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create config dir"); } @@ -711,7 +714,7 @@ mod tests { #[test] fn load_keeps_the_rest_of_the_file_when_one_section_is_invalid() { with_temp_config_home(|_| { - let path = Config::get_config_path().expect("config path"); + let path = Config::config_path().expect("config path"); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create config dir"); } diff --git a/src/backend/wayland/backend/state_init/mod.rs b/src/backend/wayland/backend/state_init/mod.rs index a8f3c62a8..140177cbf 100644 --- a/src/backend/wayland/backend/state_init/mod.rs +++ b/src/backend/wayland/backend/state_init/mod.rs @@ -4,20 +4,27 @@ use smithay_client_toolkit::globals::ProvidesBoundGlobal; use std::env; use std::path::Path; +use crate::backend::wayland::runtime_ui_state::ToolbarRuntimeState; +use crate::backend::wayland::session::PersistenceController; +use crate::draw::TextMeasurer; +use crate::input::state::{Toast, ToastPriority}; +use crate::palette_recents::{PaletteRecentsStore, PaletteRecentsWriter}; +use crate::ui::toolbar::{RuntimeUiPersistenceMode, RuntimeUiPersistenceSnapshot}; +use crate::ui_text::UiTextEngine; +use crate::{env_vars, launch_environment, paths}; + use super::super::state::{WaylandState, WaylandStateInit}; use super::WaylandBackend; use super::runtime_wake::RuntimeWakeSource; use super::setup::WaylandSetup; use crate::backend::wayland::portal_capture::portal_freeze_fallback; use crate::backend::wayland::session::{SessionHome, SessionLaunch, session_target}; +use crate::capture::CaptureManager; +use crate::config::Config; use crate::env_vars::{DESKTOP_SESSION_ENV, XDG_CURRENT_DESKTOP_ENV, XDG_SESSION_DESKTOP_ENV}; -use crate::{ - capture::CaptureManager, - config::Config, - input::InputState, - input::state::{CompositorCapabilities, DesktopEnvironment, ShellMode}, - onboarding::OnboardingStore, -}; +use crate::input::InputState; +use crate::input::state::{CompositorCapabilities, DesktopEnvironment, ShellMode}; +use crate::onboarding::OnboardingStore; mod config; mod output; @@ -65,10 +72,9 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul home_options, session_target(session_options.as_ref()), ); - let runtime_wake = RuntimeWakeSource::new() + let runtime_wake = RuntimeWakeSource::try_new() .map_err(|err| anyhow::anyhow!("failed to create runtime wake descriptor: {err}"))?; - let persistence = - crate::backend::wayland::session::PersistenceController::start(runtime_wake.handle())?; + let persistence = PersistenceController::start(runtime_wake.handle())?; let output_prefs = output::resolve(&config); #[cfg(feature = "tablet-input")] @@ -90,31 +96,30 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul backend.tokio_runtime.handle(), &keybindings.keybinding_conflicts, ); - let ui_text = crate::ui_text::UiTextEngine::default(); - let text_measurer = crate::draw::TextMeasurer::default(); - let runtime_ui_path = crate::paths::runtime_ui_state_file(); - let (runtime_ui, runtime_ui_unavailable) = - match crate::backend::wayland::runtime_ui_state::ToolbarRuntimeState::start( - &config, - &input_state, - &runtime_ui_path, - runtime_wake.handle(), - ) { - Ok(runtime_ui) => { - runtime_ui.apply_startup_state(&ui_text, &text_measurer, &mut input_state); - (Some(runtime_ui), None) - } - Err(error) => { - warn!( - "Runtime UI persistence is unavailable at {}: {error:#}", - runtime_ui_path.display() - ); - ( - None, - Some(runtime_ui_unavailable_snapshot(&runtime_ui_path, &error)), - ) - } - }; + let ui_text = UiTextEngine::default(); + let text_measurer = TextMeasurer::default(); + let runtime_ui_path = paths::runtime_ui_state_file(); + let (runtime_ui, runtime_ui_unavailable) = match ToolbarRuntimeState::start( + &config, + &input_state, + &runtime_ui_path, + runtime_wake.handle(), + ) { + Ok(runtime_ui) => { + runtime_ui.apply_startup_state(&ui_text, &text_measurer, &mut input_state); + (Some(runtime_ui), None) + } + Err(error) => { + warn!( + "Runtime UI persistence is unavailable at {}: {error:#}", + runtime_ui_path.display() + ); + ( + None, + Some(runtime_ui_unavailable_snapshot(&runtime_ui_path, &error)), + ) + } + }; input_state.set_session_preflight_options(session_options.clone()); let screencopy_supported = setup.screencopy_manager.is_some(); let image_copy_capture_supported = setup.ext_image_copy_managers.is_some(); @@ -125,7 +130,7 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul let tokio_handle = backend.tokio_runtime.handle().clone(); // Read from the launch environment: a linked GTK 4.16 or later unsets the // token from the live environment before `main`. - let startup_activation_token = crate::launch_environment::startup_activation_token(); + let startup_activation_token = launch_environment::startup_activation_token(); if startup_activation_token.is_some() { info!("Received startup activation token from launcher environment"); } @@ -157,9 +162,9 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul && config.ui.show_onboarding_hints { input_state.push_toast( - crate::input::state::ToastPriority::Critical, + ToastPriority::Critical, "onboarding.persistence", - crate::input::state::Toast::warning(format!( + Toast::warning(format!( "Automatic guidance is off because onboarding progress could not be saved: {error}" )), ); @@ -172,13 +177,13 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul ); // Seed the palette's recent-commands history from its persisted store. - let palette_recents_store = crate::palette_recents::PaletteRecentsStore::load(); + let palette_recents_store = PaletteRecentsStore::load(); input_state.set_command_palette_recents(palette_recents_store.recents().to_vec()); // Only a daemon-launched overlay child carries its generation. input_state.set_context_menu_exit_hides_overlay( - std::env::var_os(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV).is_some(), + env::var_os(env_vars::OVERLAY_CHILD_GENERATION_ENV).is_some(), ); - let palette_recents = crate::palette_recents::PaletteRecentsWriter::new(palette_recents_store); + let palette_recents = PaletteRecentsWriter::new(palette_recents_store); apply_initial_mode(backend, &config, &mut input_state, &text_measurer); @@ -253,10 +258,10 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul fn runtime_ui_unavailable_snapshot( path: &Path, error: &anyhow::Error, -) -> crate::ui::toolbar::RuntimeUiPersistenceSnapshot { - crate::ui::toolbar::RuntimeUiPersistenceSnapshot { +) -> RuntimeUiPersistenceSnapshot { + RuntimeUiPersistenceSnapshot { path: path.to_path_buf(), - mode: crate::ui::toolbar::RuntimeUiPersistenceMode::Unavailable, + mode: RuntimeUiPersistenceMode::Unavailable, detail: Some(format!( "Runtime preference persistence could not start: {error:#}. Runtime-only toolbar and board changes apply only to this process." )), @@ -268,7 +273,7 @@ fn apply_initial_mode( backend: &WaylandBackend, _config: &Config, input_state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ) { // Apply initial board from CLI (if provided). if let Some(initial_id) = backend.initial_mode.clone() { @@ -310,10 +315,7 @@ mod tests { ); assert_eq!(snapshot.path, path); - assert_eq!( - snapshot.mode, - crate::ui::toolbar::RuntimeUiPersistenceMode::Unavailable - ); + assert_eq!(snapshot.mode, RuntimeUiPersistenceMode::Unavailable); let detail = snapshot.detail.expect("startup failure detail"); assert!(detail.contains("writer channel could not start")); assert!( diff --git a/src/backend/wayland/backend/state_init/session.rs b/src/backend/wayland/backend/state_init/session.rs index b10f96214..589512c6c 100644 --- a/src/backend/wayland/backend/state_init/session.rs +++ b/src/backend/wayland/backend/state_init/session.rs @@ -148,6 +148,11 @@ fn log_session_options(session_options: Option<&session::SessionOptions>) { #[cfg(test)] mod tests { + use std::ffi::OsStr; + + use crate::backend::wayland::session::HomeSession; + use crate::test_env; + use super::*; #[test] @@ -176,8 +181,8 @@ mod tests { #[test] fn a_daemon_home_follows_the_daemon_resume_policy_not_the_runs_own() { - let off = std::ffi::OsStr::new("off"); - crate::test_env::with_env_var(RESUME_SESSION_ENV, Some(off), || { + let off = OsStr::new("off"); + test_env::with_env_var(RESUME_SESSION_ENV, Some(off), || { let previous = crate::runtime_session_override(); // What a run whose --session-file is not home sets for itself. crate::set_runtime_session_override(Some(true)); @@ -186,7 +191,7 @@ mod tests { &Config::default(), Path::new("/tmp/config"), &SessionLaunch { - home: crate::backend::wayland::session::HomeSession::Default, + home: HomeSession::Default, preferred: None, from_daemon, }, diff --git a/src/backend/wayland/backend/surface.rs b/src/backend/wayland/backend/surface.rs index 675e27b9b..41093f8be 100644 --- a/src/backend/wayland/backend/surface.rs +++ b/src/backend/wayland/backend/surface.rs @@ -1,8 +1,8 @@ use anyhow::Result; use log::info; -use smithay_client_toolkit::shell::{ - WaylandSurface, wlr_layer::Anchor, xdg::window::WindowDecorations, -}; +use smithay_client_toolkit::shell::WaylandSurface; +use smithay_client_toolkit::shell::wlr_layer::Anchor; +use smithay_client_toolkit::shell::xdg::window::WindowDecorations; use crate::app_id::runtime_app_id; diff --git a/src/backend/wayland/backend/tray.rs b/src/backend/wayland/backend/tray.rs index e8f3a5fd4..d70042a61 100644 --- a/src/backend/wayland/backend/tray.rs +++ b/src/backend/wayland/backend/tray.rs @@ -1,3 +1,6 @@ +use crate::daemon::protocol_v2::ClaimedAction; +use crate::{daemon, tray_action}; + use super::super::state::WaylandState; use crate::config::Action; use crate::daemon::protocol_v2::{ @@ -21,17 +24,13 @@ pub(super) fn durable_action_retry_timeout(state: &WaylandState, now: Instant) - .map(|deadline| deadline.saturating_duration_since(now)) } -fn defer_durable_action( - state: &mut WaylandState, - action: Option, -) { +fn defer_durable_action(state: &mut WaylandState, action: Option) { state.durable_action_finish = action; state.durable_action_retry_at = Some(Instant::now() + DURABLE_ACTION_RETRY_DELAY); } pub(super) fn process_tray_action(state: &mut WaylandState) -> bool { - let actions = - crate::tray_action::take_pending_legacy_actions(DaemonControlProtocolMode::production()); + let actions = tray_action::take_pending_legacy_actions(DaemonControlProtocolMode::production()); let mut processed = !actions.is_empty(); for action in actions { apply_tray_action(state, action); @@ -62,7 +61,7 @@ pub(super) fn process_tray_action(state: &mut WaylandState) -> bool { } for _ in 0..MAX_DURABLE_ACTIONS_PER_DRAIN { - match crate::daemon::try_claim_overlay_action() { + match daemon::try_claim_overlay_action() { Ok(ActionClaimOutcome::Claimed(action)) => { apply_tray_action(state, action.action()); processed = true; diff --git a/src/backend/wayland/capture.rs b/src/backend/wayland/capture.rs index c49b20d10..8f995b47b 100644 --- a/src/backend/wayland/capture.rs +++ b/src/backend/wayland/capture.rs @@ -3,14 +3,16 @@ //! Keeps the capture manager and in-progress flag together so the main //! Wayland loop can coordinate capture requests and results. -use crate::{ - capture::{ - CaptureManager, CaptureRequest, CaptureRequestId, DesktopBackdropCaptureRequest, - ImageOperationKind, file::FileSaveConfig, - }, - config::Action, - input::state::BoardPasteTarget, +use std::fmt::{Debug, Formatter}; +use std::{fmt, mem}; + +use crate::capture::file::FileSaveConfig; +use crate::capture::{ + CaptureManager, CaptureRequest, CaptureRequestId, DesktopBackdropCaptureRequest, + ImageOperationKind, }; +use crate::config::Action; +use crate::input::state::BoardPasteTarget; use super::state::RegionCaptureIntent; @@ -27,8 +29,8 @@ pub(in crate::backend::wayland) enum CapturePreflightRequest { DesktopBackdrop(DesktopBackdropCaptureRequest), } -impl std::fmt::Debug for CapturePreflightRequest { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Debug for CapturePreflightRequest { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::Screenshot(request) => f.debug_tuple("Screenshot").field(request).finish(), Self::DesktopBackdrop(request) => { @@ -224,12 +226,19 @@ impl CaptureState { return None; }; let RegionCapturePhase::Reserved(intent) = - std::mem::replace(&mut self.region, RegionCapturePhase::Idle) + mem::replace(&mut self.region, RegionCapturePhase::Idle) else { unreachable!("region phase changed after reservation check"); }; - debug_assert!(self.in_progress); - debug_assert!(self.accepted_id.is_none()); + debug_assert!( + self.in_progress, + "a reserved region picker must hold the capture lifecycle" + ); + debug_assert!( + self.accepted_id.is_none(), + "a reserved region picker has no accepted manager request, got {:?}", + self.accepted_id + ); Some(intent) } @@ -245,12 +254,19 @@ impl CaptureState { }; let submission_intent = intent.clone(); let RegionCapturePhase::Reserved(intent) = - std::mem::replace(&mut self.region, RegionCapturePhase::Idle) + mem::replace(&mut self.region, RegionCapturePhase::Idle) else { unreachable!("region phase changed after reservation check"); }; - debug_assert!(self.in_progress); - debug_assert!(self.accepted_id.is_none()); + debug_assert!( + self.in_progress, + "a reserved region picker must hold the capture lifecycle" + ); + debug_assert!( + self.accepted_id.is_none(), + "a region submission starts before the manager accepts it, got {:?}", + self.accepted_id + ); self.region = RegionCapturePhase::Submitting(intent); Some(submission_intent) } @@ -360,13 +376,16 @@ impl CaptureState { #[cfg(test)] mod tests { + use crate::backend::wayland::frozen::FrozenImage; + use crate::capture::DesktopBackdropGeometry; + use crate::input::state::test_support; + use crate::util::Rect; + use super::*; - use crate::{ - backend::ExitAfterCaptureMode, - backend::wayland::state::RegionPickerOptions, - capture::types::{CaptureDestination, CaptureType}, - input::state::RegionPurposeTag, - }; + use crate::backend::ExitAfterCaptureMode; + use crate::backend::wayland::state::RegionPickerOptions; + use crate::capture::types::{CaptureDestination, CaptureType}; + use crate::input::state::RegionPurposeTag; fn region_intent(action: Action) -> RegionCaptureIntent { RegionCaptureIntent::new( @@ -393,7 +412,7 @@ mod tests { board_id: "transparent".to_string(), page_index: 0, page_generation: 1, - world_bounds: crate::util::Rect::new(10, 20, 30, 40).unwrap(), + world_bounds: Rect::new(10, 20, 30, 40).unwrap(), } } @@ -642,7 +661,7 @@ mod tests { use wayland_client::protocol::wl_output; let mut source = FrozenState::new(None); - let mut backdrop = crate::capture::DesktopBackdropGeometry { + let mut backdrop = DesktopBackdropGeometry { logical_x: 0, logical_y: 0, logical_width: 2, @@ -668,7 +687,7 @@ mod tests { source.set_active_output(None, Some(7)); source.set_active_geometry(Some(geometry.clone())); source.set_pending_output_image( - crate::backend::wayland::frozen::FrozenImage { + FrozenImage { width: 2, height: 1, stride: 8, @@ -677,7 +696,7 @@ mod tests { 7, geometry.clone(), ); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); assert!(source.activate_pending_image(2, 1, &mut input).unwrap()); let context = CaptureLayoutContext::for_desktop(7, &source); let direct_provenance = source.image_provenance().unwrap(); diff --git a/src/backend/wayland/capture_preflight.rs b/src/backend/wayland/capture_preflight.rs index a32e4020a..5f431a78e 100644 --- a/src/backend/wayland/capture_preflight.rs +++ b/src/backend/wayland/capture_preflight.rs @@ -1,6 +1,9 @@ //! Capture admission and the layout identity retained through fallback. mod retry; +use std::fmt::{Debug, Display, Formatter}; +use std::{error, fmt}; + pub(super) use retry::{CaptureLayoutRetry, LayoutRetryError}; use super::frozen_geometry::OutputGeometry; @@ -33,7 +36,7 @@ impl CaptureLayoutScope { } } -pub(super) trait CaptureBackend: Copy + std::fmt::Debug { +pub(super) trait CaptureBackend: Copy + Debug { fn layout_scope(self) -> CaptureLayoutScope; fn suppression_reason(self) -> OverlaySuppression; @@ -46,8 +49,8 @@ pub(super) trait CaptureBackend: Copy + std::fmt::Debug { /// `key=value` diagnostics free of `Some(..)` wrappers. pub(super) struct LogField(pub(super) Option); -impl std::fmt::Display for LogField { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for LogField { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { match &self.0 { Some(value) => value.fmt(formatter), None => formatter.write_str("none"), @@ -124,13 +127,13 @@ impl CapturePreflightError { } } -impl std::fmt::Display for CapturePreflightError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for CapturePreflightError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter.write_str(&self.message(tr("Screen capture"))) } } -impl std::error::Error for CapturePreflightError {} +impl error::Error for CapturePreflightError {} impl From for CapturePreflightError { fn from(error: LayoutRetryError) -> Self { @@ -291,6 +294,8 @@ impl CapturePreflight { #[cfg(test)] mod tests { + use wayland_client::protocol::wl_output::Transform; + use super::*; use std::time::{Duration, Instant}; @@ -402,7 +407,7 @@ mod tests { Some((2, 1)), (2, 1), 1, - wayland_client::protocol::wl_output::Transform::Normal, + Transform::Normal, Some((2, 1)), ) .unwrap() diff --git a/src/backend/wayland/capture_preflight/retry.rs b/src/backend/wayland/capture_preflight/retry.rs index de35bd3fa..db9b37ce5 100644 --- a/src/backend/wayland/capture_preflight/retry.rs +++ b/src/backend/wayland/capture_preflight/retry.rs @@ -1,5 +1,9 @@ //! One retained retry on the backend that failed admission or produced stale pixels. -use super::*; +use super::super::frozen_geometry::OutputGeometry; +use super::{ + CAPTURE_LOG_TARGET, CaptureBackend, CaptureLayoutGenerations, CapturePreflight, + CapturePreflightError, +}; use std::time::{Duration, Instant}; const LAYOUT_QUIET_PERIOD: Duration = Duration::from_millis(100); diff --git a/src/backend/wayland/clipboard/file_list.rs b/src/backend/wayland/clipboard/file_list.rs index 29434a556..dd2217504 100644 --- a/src/backend/wayland/clipboard/file_list.rs +++ b/src/backend/wayland/clipboard/file_list.rs @@ -1,7 +1,6 @@ -use super::{ - CLIPBOARD_READ_TIMEOUT, ClipboardPasteResult, ClipboardReadError, - image::decode_clipboard_image, system::read_pipe_with_timeout, -}; +use super::image::decode_clipboard_image; +use super::system::read_pipe_with_timeout; +use super::{CLIPBOARD_READ_TIMEOUT, ClipboardPasteResult, ClipboardReadError}; use crate::file_uri; use crate::screen_pixels::EmbeddedImageLimits; #[cfg(unix)] @@ -17,10 +16,10 @@ const OCTET_STREAM_MIME: &str = "application/octet-stream"; pub(super) fn decode_clipboard_uri_list( mime_type: &str, - bytes: Vec, + bytes: &[u8], offered: Vec, ) -> ClipboardPasteResult { - let uris = match parse_clipboard_file_uris(mime_type, &bytes) { + let uris = match parse_clipboard_file_uris(mime_type, bytes) { Ok(uris) if uris.is_empty() => return ClipboardPasteResult::NoSupportedMime { offered }, Ok(uris) => uris, Err(err) => return ClipboardPasteResult::DecodeFailed(err), @@ -76,8 +75,8 @@ pub(super) fn is_uri_list_mime(mime_type: &str) -> bool { } fn parse_clipboard_file_uris(mime_type: &str, bytes: &[u8]) -> Result, String> { - let text = std::str::from_utf8(bytes) - .map_err(|err| format!("clipboard URI list is not UTF-8: {err}"))?; + let text = + str::from_utf8(bytes).map_err(|err| format!("clipboard URI list is not UTF-8: {err}"))?; let is_gnome = is_gnome_copied_files_mime(mime_type); let mut uris = Vec::new(); @@ -206,6 +205,9 @@ fn is_gnome_copied_files_mime(mime_type: &str) -> bool { #[cfg(test)] mod tests { + use std::io; + use std::os::unix; + use super::*; use crate::backend::wayland::clipboard::image::choose_supported_mime; use crate::test_temp::TempDir; @@ -244,7 +246,7 @@ mod tests { let uri = file_uri_for_path(&image_path); let offered = vec![TEXT_URI_LIST_MIME.to_string()]; - let result = decode_clipboard_uri_list(TEXT_URI_LIST_MIME, uri.into_bytes(), offered); + let result = decode_clipboard_uri_list(TEXT_URI_LIST_MIME, uri.as_bytes(), offered); match result { ClipboardPasteResult::Image(image) => { @@ -269,17 +271,14 @@ mod tests { let temp = TempDir::new().unwrap(); let fifo_path = temp.path().join("not-an-image"); let c_path = CString::new(fifo_path.as_os_str().as_bytes()).unwrap(); + // SAFETY: `c_path` is a NUL-terminated string that outlives the call, + // and mkfifo only reads it. let result = unsafe { libc::mkfifo(c_path.as_ptr(), 0o600) }; - assert_eq!( - result, - 0, - "mkfifo failed: {}", - std::io::Error::last_os_error() - ); + assert_eq!(result, 0, "mkfifo failed: {}", io::Error::last_os_error()); let uri = file_uri_for_path(&fifo_path); let offered = vec![TEXT_URI_LIST_MIME.to_string()]; - let result = decode_clipboard_uri_list(TEXT_URI_LIST_MIME, uri.into_bytes(), offered); + let result = decode_clipboard_uri_list(TEXT_URI_LIST_MIME, uri.as_bytes(), offered); match result { ClipboardPasteResult::DecodeFailed(err) => { @@ -296,11 +295,11 @@ mod tests { let image_path = temp.path().join("cat.png"); fs::write(&image_path, tiny_png()).unwrap(); let link_path = temp.path().join("link.png"); - std::os::unix::fs::symlink(&image_path, &link_path).unwrap(); + unix::fs::symlink(&image_path, &link_path).unwrap(); let uri = file_uri_for_path(&link_path); let offered = vec![TEXT_URI_LIST_MIME.to_string()]; - let result = decode_clipboard_uri_list(TEXT_URI_LIST_MIME, uri.into_bytes(), offered); + let result = decode_clipboard_uri_list(TEXT_URI_LIST_MIME, uri.as_bytes(), offered); match result { ClipboardPasteResult::DecodeFailed(err) => { @@ -321,7 +320,7 @@ mod tests { let uri = file_uri_for_path(&image_path); let offered = vec![TEXT_URI_LIST_MIME.to_string()]; - let result = decode_clipboard_uri_list(TEXT_URI_LIST_MIME, uri.into_bytes(), offered); + let result = decode_clipboard_uri_list(TEXT_URI_LIST_MIME, uri.as_bytes(), offered); match result { ClipboardPasteResult::DecodeFailed(err) => { diff --git a/src/backend/wayland/clipboard/system.rs b/src/backend/wayland/clipboard/system.rs index e84b71195..ccec8aa28 100644 --- a/src/backend/wayland/clipboard/system.rs +++ b/src/backend/wayland/clipboard/system.rs @@ -1,3 +1,7 @@ +use std::thread; + +use crate::process_broker::BrokerOutput; + use super::{ CLIPBOARD_FINGERPRINT_BYTES, CLIPBOARD_FINGERPRINT_TIMEOUT, CLIPBOARD_PUBLISH_COMMAND_TIMEOUT, ClipboardPrefixRead, ClipboardReadError, image, @@ -169,9 +173,7 @@ fn read_clipboard_mime_prefix_with_runner( }) } -fn clipboard_output( - output: crate::process_broker::BrokerOutput, -) -> Result { +fn clipboard_output(output: BrokerOutput) -> Result { if output.status == 0 { return Ok(output); } @@ -200,7 +202,7 @@ where R: Read + Send + 'static, { let (tx, rx) = mpsc::channel(); - std::thread::spawn(move || { + thread::spawn(move || { let _ = tx.send(read_limited(reader, limit)); }); rx.recv_timeout(timeout) diff --git a/src/backend/wayland/clipboard/system/command.rs b/src/backend/wayland/clipboard/system/command.rs index 86c12a056..90a171fa9 100644 --- a/src/backend/wayland/clipboard/system/command.rs +++ b/src/backend/wayland/clipboard/system/command.rs @@ -1,3 +1,5 @@ +use crate::process_broker; + use super::super::WAYSCRIBER_SELECTION_MIME; use crate::process_broker::{BrokerOutput, HelperKind}; use std::ffi::OsStr; @@ -18,7 +20,7 @@ pub(super) struct WlClipboardCommandRunner; impl ClipboardCommandRunner for WlClipboardCommandRunner { fn list_types(&self) -> anyhow::Result { - crate::process_broker::current()?.run( + process_broker::current()?.run( HelperKind::WlPaste, OsStr::new("wl-paste"), [OsStr::new("--list-types")], @@ -34,7 +36,7 @@ impl ClipboardCommandRunner for WlClipboardCommandRunner { timeout: Duration, output_cap: usize, ) -> anyhow::Result { - crate::process_broker::current()?.run_prefix( + process_broker::current()?.run_prefix( HelperKind::WlPaste, OsStr::new("wl-paste"), [OsStr::new("--type"), OsStr::new(mime_type)], @@ -45,7 +47,7 @@ impl ClipboardCommandRunner for WlClipboardCommandRunner { } fn copy_selection(&self, payload: &[u8], timeout: Duration) -> anyhow::Result { - crate::process_broker::current()?.publish( + process_broker::current()?.publish( HelperKind::WlCopy, OsStr::new("wl-copy"), [OsStr::new("--type"), OsStr::new(WAYSCRIBER_SELECTION_MIME)], diff --git a/src/backend/wayland/clipboard/transfer.rs b/src/backend/wayland/clipboard/transfer.rs index 8559ce597..d18d5012c 100644 --- a/src/backend/wayland/clipboard/transfer.rs +++ b/src/backend/wayland/clipboard/transfer.rs @@ -1,3 +1,5 @@ +use std::panic; + use super::{ CLIPBOARD_READ_TIMEOUT, ClipboardPasteCompletion, ClipboardPasteResult, ClipboardPublishCompletion, ClipboardReadError, MAX_CLIPBOARD_SELECTION_BYTES, @@ -78,7 +80,7 @@ pub(in crate::backend::wayland) enum PasteAction { }, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(in crate::backend::wayland) enum TransferWarning { PublishSelectionTooLarge, PublishUnavailable, @@ -95,9 +97,13 @@ pub(in crate::backend::wayland) enum TransferWarning { MalformedPrivateSelection, } +#[expect( + clippy::disallowed_methods, + reason = "defense in depth: a panic in the clipboard helper is logged and reported as a failed copy instead of ending the overlay" +)] pub(in crate::backend::wayland) fn resolve_selection_clipboard_publish( generation: u64, - payload_json: String, + payload_json: &str, ) -> ClipboardPublishCompletion { if payload_json.len() > MAX_CLIPBOARD_SELECTION_BYTES { return ClipboardPublishCompletion { @@ -108,18 +114,17 @@ pub(in crate::backend::wayland) fn resolve_selection_clipboard_publish( }; } - let copied = - match std::panic::catch_unwind(|| system::publish_selection_clipboard(&payload_json)) { - Ok(Ok(())) => true, - Ok(Err(err)) => { - log::warn!("Selection clipboard publish failed: {}", err); - false - } - Err(_) => { - log::error!("Selection clipboard publish panicked"); - false - } - }; + let copied = match panic::catch_unwind(|| system::publish_selection_clipboard(payload_json)) { + Ok(Ok(())) => true, + Ok(Err(err)) => { + log::warn!("Selection clipboard publish failed: {}", err); + false + } + Err(_) => { + log::error!("Selection clipboard publish panicked"); + false + } + }; ClipboardPublishCompletion { generation, @@ -179,7 +184,7 @@ pub(in crate::backend::wayland) fn resolve_system_clipboard() -> ClipboardPasteR } if file_list::is_uri_list_mime(&mime_type) { - return file_list::decode_clipboard_uri_list(&mime_type, bytes, offered); + return file_list::decode_clipboard_uri_list(&mime_type, &bytes, offered); } image::decode_clipboard_image(&mime_type, bytes) @@ -212,11 +217,11 @@ pub(in crate::backend::wayland) fn plan_paste_start( pub(in crate::backend::wayland) fn plan_after_fingerprint_probe( request: ClipboardPasteRequest, generation: u64, - expected: Option, - current: Option, + expected: Option<&ClipboardFingerprint>, + current: Option<&ClipboardFingerprint>, local_shapes: Option>, ) -> TransferPlan { - match (expected.as_ref(), current.as_ref()) { + match (expected, current) { (Some(previous), Some(current)) if previous == current => { if let Some(shapes) = local_shapes { TransferPlan::action(PasteAction::UseLocalShapes { diff --git a/src/backend/wayland/clipboard/transfer/tests.rs b/src/backend/wayland/clipboard/transfer/tests.rs index 5866d0927..b241aa861 100644 --- a/src/backend/wayland/clipboard/transfer/tests.rs +++ b/src/backend/wayland/clipboard/transfer/tests.rs @@ -1,3 +1,6 @@ +use crate::draw; +use crate::input::state::PasteAnchor; + use super::*; use crate::draw::Shape; use crate::util::Rect; @@ -8,7 +11,7 @@ fn fingerprint_mismatch_supersedes_then_reads_system_clipboard() { let previous = fingerprint(1); let current = fingerprint(2); - let plan = plan_after_fingerprint_probe(request, 7, Some(previous), Some(current), None); + let plan = plan_after_fingerprint_probe(request, 7, Some(&previous), Some(¤t), None); assert_eq!( plan.effects, @@ -94,7 +97,7 @@ fn clipboard_terminal_outcomes_keep_their_existing_event_loop_policy() { let image = plan_paste_completion( ClipboardPasteCompletion { request: request_with_fallback_generation(Some(7)), - result: ClipboardPasteResult::Image(crate::draw::EmbeddedImage { + result: ClipboardPasteResult::Image(EmbeddedImage { mime_type: "image/png".to_string(), width: 1, height: 1, @@ -197,7 +200,7 @@ fn request_with_fallback_generation( target_board_id: "board".to_string(), target_page_index: 0, target_page_generation: 1, - anchor: crate::input::state::PasteAnchor::VisibleCenter { x: 50, y: 50 }, + anchor: PasteAnchor::VisibleCenter { x: 50, y: 50 }, visible_canvas_rect: Rect::new(0, 0, 100, 100).unwrap(), screen_size: (100, 100), selection_clipboard_generation_at_request: local_selection_fallback_generation.unwrap_or(0), @@ -223,7 +226,7 @@ fn rect() -> Shape { h: 10, fill: false, fill_color: None, - color: crate::draw::BLACK, + color: draw::BLACK, thick: 1.0, } } diff --git a/src/backend/wayland/config_edits.rs b/src/backend/wayland/config_edits.rs index 0b7637ed6..30b5137ff 100644 --- a/src/backend/wayland/config_edits.rs +++ b/src/backend/wayland/config_edits.rs @@ -60,6 +60,8 @@ //! finished temp file into place — so the file is either that edit or what it //! said before, never half of either. +use std::io; + use super::RuntimeWakeHandle; use super::state::{ WaylandState, queue_keybinding_edit, queue_preset_action, queue_quick_color_edit, @@ -611,7 +613,7 @@ impl ConfigEditWorker { unwritten } - fn start(&mut self) -> std::io::Result<()> { + fn start(&mut self) -> io::Result<()> { let (command_tx, command_rx) = sync_channel(self.capacity); let (completion_tx, completion_rx) = sync_channel(self.capacity); let wake = self.wake.clone(); @@ -657,6 +659,10 @@ fn join_identities(identities: &[ConfigEditIdentity]) -> String { /// Nothing here can block on the failure: a dropped receiver makes `send` /// return rather than wait, and the wake is skipped with it, because there is no /// completion for the loop to pick up. +#[expect( + clippy::disallowed_methods, + reason = "a panic while writing one edit must still answer that edit, and the worker keeps writing the edits queued behind it" +)] fn run_worker( commands: &Receiver, completions: &SyncSender, diff --git a/src/backend/wayland/config_edits/tests.rs b/src/backend/wayland/config_edits/tests.rs index abc942d62..9fbf8c17d 100644 --- a/src/backend/wayland/config_edits/tests.rs +++ b/src/backend/wayland/config_edits/tests.rs @@ -8,6 +8,14 @@ //! full, which is the case that used to answer the newest gesture first — and an //! edit still waiting when the overlay quits reaches the file anyway. +use crate::config::{Action, ColorSpec, ConfigDocument, KeybindingsConfig, ToolPresetConfig}; +use crate::draw::Color; +use crate::input::state::test_support::TestInputStateBuilder; +use crate::{config, session}; + +use std::ffi::OsStr; +use std::path::PathBuf; + use super::*; use crate::backend::wayland::RuntimeWakeSource; use crate::config::ConfigEditWrite; @@ -23,11 +31,11 @@ const AUTHORED_FILE: &str = "\ setting_from_a_later_release = 7 "; -fn preset(name: &str) -> Box { - Box::new(crate::config::ToolPresetConfig { +fn preset(name: &str) -> Box { + Box::new(ToolPresetConfig { name: Some(name.to_string()), tool: Tool::Pen, - color: crate::config::ColorSpec::from(crate::draw::Color { + color: ColorSpec::from(Color { r: 0.13, g: 0.27, b: 0.41, @@ -58,8 +66,8 @@ fn save(slot: usize, name: &str) -> ConfigEdit { }) } -fn config_in(config_root: &Path) -> std::path::PathBuf { - let directory = config_root.join(crate::config::PRIMARY_CONFIG_DIR); +fn config_in(config_root: &Path) -> PathBuf { + let directory = config_root.join(config::PRIMARY_CONFIG_DIR); fs::create_dir_all(&directory).expect("the config directory this test named"); let path = directory.join("config.toml"); fs::write(&path, AUTHORED_FILE).expect("the fixture this test named a directory for"); @@ -105,7 +113,7 @@ fn completed_preset_name(completion: &ConfigEditCompletion) -> String { fn hold_config_write_lock(config_path: &Path) -> File { let name = config_path .file_name() - .and_then(std::ffi::OsStr::to_str) + .and_then(OsStr::to_str) .expect("the fixture path this test built has a file name"); let path = config_path.with_file_name(format!("{name}.lock")); let file = OpenOptions::new() @@ -115,7 +123,7 @@ fn hold_config_write_lock(config_path: &Path) -> File { .truncate(false) .open(&path) .expect("the lock file beside a fixture this test created"); - crate::session::try_lock_exclusive(&file) + session::try_lock_exclusive(&file) .expect("nothing else can hold the lock on a directory this test just made"); file } @@ -139,11 +147,11 @@ fn wait_for_slot(path: &Path, slot: usize, name: &str) { } fn slot_name(path: &Path, slot: usize) -> Option { - crate::config::ConfigDocument::load_from_path(path) + ConfigDocument::load_from_path(path) .expect("the written config reloads") .config() .presets - .get_slot(slot) + .slot(slot) .and_then(|preset| preset.name.clone()) } @@ -156,7 +164,7 @@ fn slot_name(path: &Path, slot: usize) -> Option { fn two_rapid_edits_are_written_and_completed_in_order() { with_temp_config_home(|config_root| { let path = config_in(config_root); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); let mut worker = ConfigEditWorker::new(wake.handle()); worker.submit(save(1, "First")); @@ -218,7 +226,7 @@ fn two_shortcut_edits_are_written_and_completed_in_order() { with_temp_config_home(|config_root| { let path = config_in(config_root); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); let mut worker = ConfigEditWorker::new(wake.handle()); worker.submit(rebind(Action::SelectPenTool, "Ctrl+Alt+Shift+P")); @@ -248,8 +256,7 @@ fn two_shortcut_edits_are_written_and_completed_in_order() { ); } - let reloaded = crate::config::ConfigDocument::load_from_path(&path) - .expect("the written config reloads"); + let reloaded = ConfigDocument::load_from_path(&path).expect("the written config reloads"); assert_eq!( reloaded .config() @@ -277,7 +284,7 @@ fn two_shortcut_edits_are_written_and_completed_in_order() { fn shutdown_drains_a_queued_edit_before_returning() { with_temp_config_home(|config_root| { let path = config_in(config_root); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); let mut worker = ConfigEditWorker::new(wake.handle()); worker.submit(save(3, "Quitting")); @@ -297,7 +304,7 @@ fn shutdown_drains_a_queued_edit_before_returning() { fn an_edit_submitted_after_shutdown_still_gets_exactly_one_completion() { with_temp_config_home(|config_root| { let _path = config_in(config_root); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); let mut worker = ConfigEditWorker::new(wake.handle()); worker.submit(save(4, "Before")); @@ -332,7 +339,7 @@ fn an_edit_submitted_after_shutdown_still_gets_exactly_one_completion() { fn a_full_queue_answers_edits_in_submission_order_and_keeps_the_newest() { with_temp_config_home(|config_root| { let path = config_in(config_root); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); // One slot in the worker's channel, so the third gesture finds it full. let mut worker = ConfigEditWorker::with_capacity(wake.handle(), 1); let names = ["First", "Second", "Third", "Fourth"]; @@ -382,7 +389,7 @@ fn a_full_queue_answers_edits_in_submission_order_and_keeps_the_newest() { fn shutdown_drains_edits_the_channel_had_no_room_for() { with_temp_config_home(|config_root| { let path = config_in(config_root); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); let mut worker = ConfigEditWorker::with_capacity(wake.handle(), 1); let staged = [(1, "First"), (2, "Second"), (3, "Third"), (4, "Fourth")]; @@ -425,7 +432,7 @@ fn shutdown_drains_edits_the_channel_had_no_room_for() { fn the_worker_writes_the_edits_behind_a_stalled_one_after_the_receiver_is_gone() { with_temp_config_home(|config_root| { let path = config_in(config_root); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); let mut worker = ConfigEditWorker::new(wake.handle()); let held = hold_config_write_lock(&path); @@ -474,14 +481,14 @@ fn the_worker_writes_the_edits_behind_a_stalled_one_after_the_receiver_is_gone() fn shutdown_that_gives_up_waiting_names_the_edits_it_could_not_confirm() { with_temp_config_home(|config_root| { let path = config_in(config_root); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); let mut worker = ConfigEditWorker::with_drain(wake.handle(), Duration::from_millis(50)); let held = hold_config_write_lock(&path); worker.submit(save(4, "Unconfirmed")); worker.submit(ConfigEdit::QuickColor(QuickColorEdit { index: 2, - color: crate::draw::Color { + color: Color { r: 0.1, g: 0.2, b: 0.3, @@ -523,10 +530,10 @@ fn shutdown_that_gives_up_waiting_names_the_edits_it_could_not_confirm() { /// reads the deltas already outstanding on the worker, and submitting straight /// past it would test nothing. fn rebind_through_the_palette( - keybindings: &crate::config::KeybindingsConfig, - input: &mut crate::input::state::InputState, + keybindings: &KeybindingsConfig, + input: &mut InputState, worker: &mut ConfigEditWorker, - action: crate::config::Action, + action: Action, binding: &str, ) { use crate::backend::wayland::state::queue_keybinding_edit; @@ -543,8 +550,8 @@ fn rebind_through_the_palette( ); } -fn written_bindings(path: &Path, action: crate::config::Action) -> Option> { - crate::config::ConfigDocument::load_from_path(path) +fn written_bindings(path: &Path, action: Action) -> Option> { + ConfigDocument::load_from_path(path) .expect("the written config reloads") .config() .keybindings @@ -571,7 +578,7 @@ fn a_chord_an_outstanding_edit_freed_is_rebindable_and_both_edits_reach_the_file with_temp_config_home(|config_root| { let path = config_in(config_root); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); let mut worker = ConfigEditWorker::new(wake.handle()); let mut config = Config::default(); let mut input = test_input_state(); @@ -631,7 +638,7 @@ fn a_freed_chord_is_rebindable_between_two_ordinary_drains() { with_temp_config_home(|config_root| { let path = config_in(config_root); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); let mut worker = ConfigEditWorker::new(wake.handle()); let mut input = test_input_state(); let keybindings = KeybindingsConfig::default(); @@ -698,7 +705,7 @@ fn an_answered_edit_gives_its_projection_back_even_when_the_write_failed() { with_temp_config_home(|config_root| { let path = config_in(config_root); fs::write(&path, "this is not TOML =\n").expect("an unparseable fixture"); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); let mut worker = ConfigEditWorker::new(wake.handle()); let mut input = test_input_state(); let keybindings = KeybindingsConfig::default(); @@ -731,7 +738,7 @@ fn an_answered_edit_gives_its_projection_back_even_when_the_write_failed() { /// An input state the way the overlay's own suites build one, so the gestures /// below are recorded by the real code rather than poked into a field. -fn test_input_state() -> crate::input::state::InputState { +fn test_input_state() -> InputState { use crate::config::{Action, Shortcut}; use std::collections::HashMap; @@ -741,7 +748,7 @@ fn test_input_state() -> crate::input::state::InputState { Shortcut::parse("Escape").expect("a chord this test spelled"), Action::Exit, ); - crate::input::state::test_support::TestInputStateBuilder::default() + TestInputStateBuilder::default() .action_map(action_map) .action_bindings(HashMap::new()) .thickness(3.0) @@ -777,7 +784,7 @@ fn gestures_still_pending_when_the_overlay_quits_reach_the_file() { with_temp_config_home(|config_root| { let path = config_in(config_root); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); let mut worker = ConfigEditWorker::new(wake.handle()); let mut config = Config::default(); let mut input = test_input_state(); @@ -807,7 +814,7 @@ fn gestures_still_pending_when_the_overlay_quits_reach_the_file() { let written = ConfigDocument::load_from_path(&path).expect("the written config reloads"); assert!( - written.config().presets.get_slot(1).is_some(), + written.config().presets.slot(1).is_some(), "the preset gesture must have reached the file" ); assert_eq!( @@ -827,8 +834,8 @@ fn gestures_still_pending_when_the_overlay_quits_reach_the_file() { .map(|entry| entry.color.to_color()) .expect("the palette always resolves a first slot"); assert_eq!( - crate::config::ColorSpec::from(swatch), - crate::config::ColorSpec::from(RECOLORED), + ColorSpec::from(swatch), + ColorSpec::from(RECOLORED), "the accepted recolor must have reached the file" ); }); @@ -840,7 +847,7 @@ fn gestures_still_pending_when_the_overlay_quits_reach_the_file() { fn a_finished_write_wakes_the_event_loop() { with_temp_config_home(|config_root| { let _path = config_in(config_root); - let wake = RuntimeWakeSource::new().expect("an eventfd"); + let wake = RuntimeWakeSource::try_new().expect("an eventfd"); let mut worker = ConfigEditWorker::new(wake.handle()); // Drain anything the descriptor already carried, so the wait below is // about this edit. diff --git a/src/backend/wayland/frozen/capture.rs b/src/backend/wayland/frozen/capture.rs index 37c268476..9fd22eb14 100644 --- a/src/backend/wayland/frozen/capture.rs +++ b/src/backend/wayland/frozen/capture.rs @@ -1,25 +1,19 @@ use anyhow::{Context, Result}; use log::{debug, info, warn}; -use smithay_client_toolkit::shm::{ - Shm, - slot::{Buffer, SlotPool}, -}; -use wayland_client::{Dispatch, QueueHandle, WEnum, protocol::wl_shm}; -use wayland_protocols::ext::{ - image_capture_source::v1::client::{ - ext_image_capture_source_v1::ExtImageCaptureSourceV1, - ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1, - }, - image_copy_capture::v1::client::{ - ext_image_copy_capture_frame_v1::ExtImageCopyCaptureFrameV1, - ext_image_copy_capture_manager_v1::ExtImageCopyCaptureManagerV1, - ext_image_copy_capture_session_v1::ExtImageCopyCaptureSessionV1, - }, -}; +use smithay_client_toolkit::shm::Shm; +use smithay_client_toolkit::shm::slot::{Buffer, SlotPool}; +use wayland_client::{Dispatch, QueueHandle, WEnum}; +use wayland_client::protocol::wl_shm; +use wayland_protocols::ext::image_capture_source::v1::client::ext_image_capture_source_v1::ExtImageCaptureSourceV1; +use wayland_protocols::ext::image_capture_source::v1::client::ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1; +use wayland_protocols::ext::image_copy_capture::v1::client::ext_image_copy_capture_frame_v1::ExtImageCopyCaptureFrameV1; +use wayland_protocols::ext::image_copy_capture::v1::client::ext_image_copy_capture_manager_v1::ExtImageCopyCaptureManagerV1; +use wayland_protocols::ext::image_copy_capture::v1::client::ext_image_copy_capture_session_v1::ExtImageCopyCaptureSessionV1; use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::{ Event as FrameEvent, Flags, ZwlrScreencopyFrameV1, }; use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1; +use tokio::runtime::Handle; use crate::backend::wayland::acquisition::ScreenAcquisitionOutcome; use crate::backend::wayland::capture::CaptureLayoutContext; @@ -108,7 +102,7 @@ impl FrozenState { backend: FrozenCaptureBackend, shm: &Shm, qh: &QueueHandle, - tokio_handle: &tokio::runtime::Handle, + tokio_handle: &Handle, ) -> Result<()> where State: Dispatch @@ -128,7 +122,7 @@ impl FrozenState { failed_backend: FrozenCaptureBackend, shm: &Shm, qh: &QueueHandle, - tokio_handle: &tokio::runtime::Handle, + tokio_handle: &Handle, ) -> Result<()> where State: Dispatch @@ -151,7 +145,7 @@ impl FrozenState { first_backend: FrozenCaptureBackend, shm: &Shm, qh: &QueueHandle, - tokio_handle: &tokio::runtime::Handle, + tokio_handle: &Handle, ) -> Result<()> where State: Dispatch @@ -189,7 +183,7 @@ impl FrozenState { backend: FrozenCaptureBackend, shm: &Shm, qh: &QueueHandle, - tokio_handle: &tokio::runtime::Handle, + tokio_handle: &Handle, ) -> Result<()> where State: Dispatch @@ -259,7 +253,7 @@ impl FrozenState { } /// Handle screencopy frame events. - pub fn handle_frame_event(&mut self, event: FrameEvent, input_state: &mut InputState) -> bool { + pub fn handle_frame_event(&mut self, event: &FrameEvent, input_state: &mut InputState) -> bool { if !matches!( self.direct_capture.as_ref(), Some(DirectCaptureAttempt::WlrScreencopy { .. }) @@ -268,7 +262,7 @@ impl FrozenState { return false; } - match event { + match *event { FrameEvent::Buffer { format, width, diff --git a/src/backend/wayland/frozen/ext_image_copy.rs b/src/backend/wayland/frozen/ext_image_copy.rs index 39b7d07a7..1c429a464 100644 --- a/src/backend/wayland/frozen/ext_image_copy.rs +++ b/src/backend/wayland/frozen/ext_image_copy.rs @@ -1,26 +1,16 @@ +use std::mem; + use anyhow::{Context, Result}; use log::{debug, warn}; -use smithay_client_toolkit::shm::{ - Shm, - slot::{Buffer, SlotPool}, -}; -use wayland_client::{ - Dispatch, QueueHandle, WEnum, - protocol::{wl_output, wl_shm}, -}; -use wayland_protocols::ext::{ - image_capture_source::v1::client::{ - ext_image_capture_source_v1::ExtImageCaptureSourceV1, - ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1, - }, - image_copy_capture::v1::client::{ - ext_image_copy_capture_frame_v1::{ - Event as FrameEvent, ExtImageCopyCaptureFrameV1, FailureReason, - }, - ext_image_copy_capture_manager_v1::{ExtImageCopyCaptureManagerV1, Options}, - ext_image_copy_capture_session_v1::{Event as SessionEvent, ExtImageCopyCaptureSessionV1}, - }, -}; +use smithay_client_toolkit::shm::Shm; +use smithay_client_toolkit::shm::slot::{Buffer, SlotPool}; +use wayland_client::{Dispatch, QueueHandle, WEnum}; +use wayland_client::protocol::{wl_output, wl_shm}; +use wayland_protocols::ext::image_capture_source::v1::client::ext_image_capture_source_v1::ExtImageCaptureSourceV1; +use wayland_protocols::ext::image_capture_source::v1::client::ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1; +use wayland_protocols::ext::image_copy_capture::v1::client::ext_image_copy_capture_frame_v1::{Event as FrameEvent, ExtImageCopyCaptureFrameV1, FailureReason}; +use wayland_protocols::ext::image_copy_capture::v1::client::ext_image_copy_capture_manager_v1::{ExtImageCopyCaptureManagerV1, Options}; +use wayland_protocols::ext::image_copy_capture::v1::client::ext_image_copy_capture_session_v1::{Event as SessionEvent, ExtImageCopyCaptureSessionV1}; use crate::backend::wayland::capture::CaptureLayoutContext; use crate::backend::wayland::frozen_geometry::require_verified_capture_source; @@ -112,7 +102,7 @@ impl ConstraintTracker { } fn finish_batch(&mut self, frame_pending: bool) -> Option { - let batch = std::mem::take(&mut self.pending); + let batch = mem::take(&mut self.pending); if frame_pending { self.replacement = Some(batch); None @@ -190,7 +180,7 @@ impl FrozenState { pub(in crate::backend::wayland) fn handle_ext_session_event( &mut self, - event: SessionEvent, + event: &SessionEvent, qh: &QueueHandle, ) -> bool where @@ -204,7 +194,7 @@ impl FrozenState { return false; } - let result = match event { + let result = match *event { SessionEvent::BufferSize { width, height } => { let capture = self.ext_capture_mut(); capture.map(|capture| capture.constraints.record_size(width, height)) @@ -237,7 +227,7 @@ impl FrozenState { capture.constraints.finish_batch(capture.frame.is_some()) }; if let Some(constraints) = constraints { - self.submit_ext_frame(qh, constraints)?; + self.submit_ext_frame(qh, &constraints)?; } else { debug!("Deferred replacement ext-image-copy constraints until the current frame ends"); } @@ -247,7 +237,7 @@ impl FrozenState { fn submit_ext_frame( &mut self, qh: &QueueHandle, - constraints: ExtBufferConstraints, + constraints: &ExtBufferConstraints, ) -> Result<()> where State: Dispatch + 'static, @@ -290,7 +280,7 @@ impl FrozenState { pub(in crate::backend::wayland) fn handle_ext_frame_event( &mut self, - event: FrameEvent, + event: &FrameEvent, qh: &QueueHandle, input_state: &mut InputState, ) -> bool @@ -305,7 +295,7 @@ impl FrozenState { return false; } - match event { + match *event { FrameEvent::Ready => match self.finish_ext_frame() { Ok(true) => input_state.needs_redraw = true, Ok(false) => { @@ -375,7 +365,7 @@ impl FrozenState { let replacement = replacement?; debug!("Retrying ext-image-copy with replacement buffer constraints"); - self.submit_ext_frame(qh, replacement) + self.submit_ext_frame(qh, &replacement) } fn finish_ext_frame(&mut self) -> Result { diff --git a/src/backend/wayland/frozen/image.rs b/src/backend/wayland/frozen/image.rs index be1f9335a..a79e92673 100644 --- a/src/backend/wayland/frozen/image.rs +++ b/src/backend/wayland/frozen/image.rs @@ -240,7 +240,7 @@ mod tests { fn image(width: u32, height: u32, values: &[u8]) -> FrozenImage { let mut data = Vec::with_capacity(values.len() * 4); for value in values { - data.extend_from_slice(&[*value, 0, 0, 0xFF]); + data.extend_from_slice(&[*value, 0, 0, 0xff]); } FrozenImage { diff --git a/src/backend/wayland/frozen/mod.rs b/src/backend/wayland/frozen/mod.rs index fb34f5f5e..d4f1e2e5b 100644 --- a/src/backend/wayland/frozen/mod.rs +++ b/src/backend/wayland/frozen/mod.rs @@ -42,14 +42,17 @@ pub(in crate::backend::wayland) use image::{copy_shm_argb, validate_shm_buffer_l pub(in crate::backend::wayland) use state::FrozenCaptureBackend; pub use state::FrozenState; +use crate::backend::wayland::frozen_geometry::OutputGeometry; +use crate::capture::types::CaptureError; + // Keep raster rejection beside the capture snapshot so layout validation runs // before interpreting a size mismatch. Portal and decode failures remain terminal. type PortalCaptureResult = Result< ( Option, u64, - Option, - Result, + Option, + Result, ), - crate::capture::types::CaptureError, + CaptureError, >; diff --git a/src/backend/wayland/frozen/portal.rs b/src/backend/wayland/frozen/portal.rs index 243c510e0..dffdb0e2e 100644 --- a/src/backend/wayland/frozen/portal.rs +++ b/src/backend/wayland/frozen/portal.rs @@ -1,19 +1,23 @@ +use tokio::runtime::Handle; + use crate::input::state::{Toast, ToastPriority}; use anyhow::Result; use log::warn; use std::time::{Duration, Instant}; use crate::backend::wayland::acquisition::ScreenAcquisitionOutcome; -#[cfg(test)] +use crate::backend::wayland::capture_preflight::CapturePreflightError; use crate::backend::wayland::frozen::FrozenImage; -use crate::backend::wayland::frozen_geometry::require_verified_capture_source; +use crate::backend::wayland::frozen_geometry::{OutputGeometry, require_verified_capture_source}; use crate::backend::wayland::portal_capture::{ capture_via_portal_fullscreen_bytes, portal_output_matches, }; +use crate::backend::wayland::portal_raster; use crate::backend::wayland::portal_task::{PortalPoll, PortalTask}; use crate::capture::sources::frozen::decode_image_to_argb; use crate::capture::types::CaptureError; use crate::input::InputState; +use crate::notification; use super::state::FrozenState; use crate::i18n::tr; @@ -21,7 +25,7 @@ use crate::i18n::tr; impl FrozenState { pub(in crate::backend::wayland) fn capture_via_portal( &mut self, - tokio_handle: &tokio::runtime::Handle, + tokio_handle: &Handle, ) -> Result<()> { if self.portal.is_running() { warn!("Portal capture already running; ignoring new request"); @@ -41,7 +45,7 @@ impl FrozenState { let layout_generation = self.layout_generations.desktop; // Notify user that portal fallback is in progress - crate::notification::send_notification_async( + notification::send_notification_async( tokio_handle, tr("Freezing screen").to_string(), tr("Requesting screen capture...").to_string(), @@ -56,12 +60,8 @@ impl FrozenState { CaptureError::ImageError(format!("Decode failed: {error}")) })?; - let image = crate::backend::wayland::portal_raster::crop_portal_raster( - data, - width, - height, - &source_geometry, - ); + let image = + portal_raster::crop_portal_raster(&data, width, height, &source_geometry); Ok(( Some(target_output_id), layout_generation, @@ -201,8 +201,8 @@ impl FrozenState { &mut self, target_output: Option, layout_generation: u64, - source_geometry: Option, - image: Result, + source_geometry: Option, + image: Result, input_state: &mut InputState, live_output_count: Option, ) { @@ -227,9 +227,7 @@ impl FrozenState { warn!("Portal frozen raster rejected: {err}"); if self.has_acquisition_attempt() { self.finish_preflight_failure( - crate::backend::wayland::capture_preflight::CapturePreflightError::Backend( - err.to_string(), - ), + &CapturePreflightError::Backend(err.to_string()), input_state, ); } else { @@ -274,6 +272,13 @@ impl FrozenState { #[cfg(test)] mod tests { + use std::future; + + use tokio::task; + use wayland_client::protocol::wl_output::Transform; + + use crate::backend::wayland::RuntimeWakeSource; + use super::*; use crate::backend::wayland::acquisition::{ ScreenAcquisitionOutcome, ScreenAcquisitionOwner, ScreenAcquisitionRegistry, @@ -291,16 +296,14 @@ mod tests { } } - fn crop_geometry( - origin: (u32, u32), - ) -> crate::backend::wayland::frozen_geometry::OutputGeometry { - crate::backend::wayland::frozen_geometry::OutputGeometry { + fn crop_geometry(origin: (u32, u32)) -> OutputGeometry { + OutputGeometry { logical_x: 0, logical_y: 0, logical_width: 2, logical_height: 1, scale: 1, - transform: wayland_client::protocol::wl_output::Transform::Normal, + transform: Transform::Normal, overlay_buffer_size: (2, 1), pixel_size: Some((2, 1)), screenshot_origin: Some(origin), @@ -319,14 +322,14 @@ mod tests { if !frozen.portal.is_running() { return Ok(()); } - tokio::task::yield_now().await; + task::yield_now().await; } anyhow::bail!("frozen portal task did not finish") } #[test] fn live_topology_is_checked_before_a_crop_error() { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let mut registry = ScreenAcquisitionRegistry::default(); @@ -357,11 +360,11 @@ mod tests { #[tokio::test] async fn portal_start_requires_verifiable_geometry_and_output_identity() -> anyhow::Result<()> { - let wake = crate::backend::wayland::RuntimeWakeSource::new()?; + let wake = RuntimeWakeSource::try_new()?; let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let error = frozen - .capture_via_portal(&tokio::runtime::Handle::current()) + .capture_via_portal(&Handle::current()) .expect_err("missing geometry must fail closed"); assert!(error.to_string().contains("geometry is unavailable")); assert!(!frozen.portal.is_running()); @@ -369,7 +372,7 @@ mod tests { frozen.set_active_geometry(Some(crop_geometry((0, 0)))); let error = frozen - .capture_via_portal(&tokio::runtime::Handle::current()) + .capture_via_portal(&Handle::current()) .expect_err("missing output identity must fail closed"); assert!(error.to_string().contains("identity is unavailable")); assert!(!frozen.portal.is_running()); @@ -379,13 +382,13 @@ mod tests { #[tokio::test] async fn poll_portal_applies_image() -> anyhow::Result<()> { - let wake = crate::backend::wayland::RuntimeWakeSource::new()?; + let wake = RuntimeWakeSource::try_new()?; let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); frozen.set_active_output(None, Some(1)); frozen.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async { Ok((Some(1), 0, Some(crop_geometry((0, 0))), Ok(image(0)))) }, )); @@ -410,15 +413,15 @@ mod tests { #[tokio::test] async fn domain_error_and_task_panic_restore_the_frozen_lifecycle() -> anyhow::Result<()> { for panic_task in [false, true] { - let wake = crate::backend::wayland::RuntimeWakeSource::new()?; + let wake = RuntimeWakeSource::try_new()?; let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); frozen.portal.start(if panic_task { - PortalTask::spawn(&tokio::runtime::Handle::current(), wake.handle(), async { + PortalTask::spawn(&Handle::current(), wake.handle(), async { panic!("expected frozen portal panic") }) } else { - PortalTask::spawn(&tokio::runtime::Handle::current(), wake.handle(), async { + PortalTask::spawn(&Handle::current(), wake.handle(), async { Err(CaptureError::PermissionDenied) }) }); @@ -438,17 +441,17 @@ mod tests { { let now = Instant::now(); for timed_out in [false, true] { - let wake = crate::backend::wayland::RuntimeWakeSource::new()?; + let wake = RuntimeWakeSource::try_new()?; let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); frozen.portal.start(if timed_out { PortalTask::spawn_at_for_test( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), now.checked_sub(PORTAL_CAPTURE_TIMEOUT).ok_or_else(|| { anyhow::anyhow!("monotonic clock cannot represent the test deadline") })?, - std::future::pending(), + future::pending(), ) } else { PortalTask::disconnected_for_test(now) @@ -466,11 +469,11 @@ mod tests { #[tokio::test] async fn user_cancellation_restores_quietly_without_an_error_toast() -> anyhow::Result<()> { - let wake = crate::backend::wayland::RuntimeWakeSource::new()?; + let wake = RuntimeWakeSource::try_new()?; let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); frozen.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async { Err(CaptureError::Cancelled( @@ -490,7 +493,7 @@ mod tests { #[tokio::test] async fn portal_cancellation_finishes_the_correlated_attempt() -> anyhow::Result<()> { - let wake = crate::backend::wayland::RuntimeWakeSource::new()?; + let wake = RuntimeWakeSource::try_new()?; let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let mut registry = ScreenAcquisitionRegistry::default(); @@ -499,7 +502,7 @@ mod tests { .expect("id"); frozen.start_capture_for(id, ScreenAcquisitionOwner::UserFreeze)?; frozen.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async { Err(CaptureError::Cancelled( @@ -524,13 +527,13 @@ mod tests { #[tokio::test] async fn stale_output_is_discarded_without_mutating_current_frozen_state() -> anyhow::Result<()> { - let wake = crate::backend::wayland::RuntimeWakeSource::new()?; + let wake = RuntimeWakeSource::try_new()?; let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); input.set_frozen_active(true); frozen.set_active_output(None, Some(2)); frozen.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async { Ok((Some(1), 0, None, Ok(image(9)))) }, )); @@ -546,13 +549,13 @@ mod tests { #[tokio::test] async fn stale_layout_is_discarded_without_a_pending_image() -> anyhow::Result<()> { - let wake = crate::backend::wayland::RuntimeWakeSource::new()?; + let wake = RuntimeWakeSource::try_new()?; let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); frozen.set_active_geometry(Some(crop_geometry((0, 0)))); let layout_generation = frozen.layout_generations.desktop; frozen.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async move { Ok((None, layout_generation, None, Ok(image(9)))) }, )); @@ -569,14 +572,14 @@ mod tests { #[tokio::test] async fn matching_layout_still_applies_after_an_identical_geometry_refresh() -> anyhow::Result<()> { - let wake = crate::backend::wayland::RuntimeWakeSource::new()?; + let wake = RuntimeWakeSource::try_new()?; let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let geometry = crop_geometry((0, 0)); frozen.set_active_geometry(Some(geometry.clone())); let layout_generation = frozen.layout_generations.desktop; frozen.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async move { Ok((None, layout_generation, None, Ok(image(0)))) }, )); @@ -592,16 +595,16 @@ mod tests { #[tokio::test] async fn supersession_is_ignored_and_explicit_cancel_owns_task_cancellation() -> anyhow::Result<()> { - let wake = crate::backend::wayland::RuntimeWakeSource::new()?; + let wake = RuntimeWakeSource::try_new()?; let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); frozen.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), - std::future::pending(), + future::pending(), )); - frozen.capture_via_portal(&tokio::runtime::Handle::current())?; + frozen.capture_via_portal(&Handle::current())?; assert!(frozen.portal.is_running()); frozen.cancel(&mut input); assert!(!frozen.portal.is_running()); diff --git a/src/backend/wayland/frozen/retry.rs b/src/backend/wayland/frozen/retry.rs index bcdce7265..664dbf6f8 100644 --- a/src/backend/wayland/frozen/retry.rs +++ b/src/backend/wayland/frozen/retry.rs @@ -77,6 +77,12 @@ impl FrozenState { #[cfg(test)] mod tests { + use tokio::runtime::Handle; + use tokio::task; + + use crate::backend::wayland::RuntimeWakeSource; + use crate::input::InputState; + use super::*; use crate::backend::wayland::acquisition::{ ScreenAcquisitionOutcome, ScreenAcquisitionOwner, ScreenAcquisitionRegistry, @@ -101,13 +107,13 @@ mod tests { .with_known_output_count(Some(1)) } - async fn drain(frozen: &mut FrozenState, input: &mut crate::input::InputState) { + async fn drain(frozen: &mut FrozenState, input: &mut InputState) { for _ in 0..100 { frozen.poll_portal_capture(input, Instant::now(), None); if !frozen.portal.is_running() { return; } - tokio::task::yield_now().await; + task::yield_now().await; } panic!("portal task did not finish"); @@ -116,7 +122,7 @@ mod tests { #[tokio::test] async fn retry_retains_the_acquisition_and_completes_once() { for second_change in [false, true] { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let mut registry = ScreenAcquisitionRegistry::default(); @@ -132,7 +138,7 @@ mod tests { let old_generation = frozen.layout_generations.desktop; frozen.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async move { Ok(( @@ -164,7 +170,7 @@ mod tests { let fresh_generation = frozen.layout_generations.desktop; frozen.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async move { Ok(( @@ -209,7 +215,7 @@ mod tests { #[test] fn activation_rechecks_layout_and_topology_before_installing_a_portal_crop() { for live_topology_changed in [false, true] { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let mut registry = ScreenAcquisitionRegistry::default(); @@ -257,7 +263,7 @@ mod tests { #[tokio::test] async fn cancellation_and_stable_raster_failure_do_not_schedule_retry() { for cancelled in [false, true] { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let mut registry = ScreenAcquisitionRegistry::default(); @@ -269,7 +275,7 @@ mod tests { frozen.take_preflight_pending(); let generation = frozen.layout_generations.desktop; frozen.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async move { if cancelled { diff --git a/src/backend/wayland/frozen/state.rs b/src/backend/wayland/frozen/state.rs index 901e37e4b..d8135fc58 100644 --- a/src/backend/wayland/frozen/state.rs +++ b/src/backend/wayland/frozen/state.rs @@ -2,6 +2,8 @@ mod activation; mod admission; mod direct; +use std::mem; + pub(super) use direct::{DirectCaptureAttempt, DirectCaptureContext}; use crate::backend::wayland::capture_preflight::{ @@ -148,7 +150,7 @@ impl FrozenState { } pub(in crate::backend::wayland) fn take_pending_on_start(&mut self) -> bool { - std::mem::take(&mut self.pending_on_start) + mem::take(&mut self.pending_on_start) } pub(in crate::backend::wayland) fn preferred_backend(&self) -> Option { @@ -273,7 +275,7 @@ impl FrozenState { pub(in crate::backend::wayland) fn finish_preflight_failure( &mut self, - error: CapturePreflightError, + error: &CapturePreflightError, input_state: &mut InputState, ) { if !self.has_acquisition_attempt() { @@ -329,8 +331,14 @@ impl FrozenState { &mut self, input_state: &mut InputState, ) { - debug_assert!(self.image().is_some()); - debug_assert!(input_state.frozen_active()); + debug_assert!( + self.image().is_some(), + "install the frozen image before reporting the acquisition ready" + ); + debug_assert!( + input_state.frozen_active(), + "activate frozen mode before reporting the acquisition ready" + ); self.finish_acquisition( ScreenAcquisitionOutcome::Ready { installed_generation: self.image_generation(), @@ -347,7 +355,10 @@ impl FrozenState { let Some((id, owner)) = self.acquisition_attempt.take() else { return; }; - debug_assert!(self.acquisition_completion.is_none()); + debug_assert!( + self.acquisition_completion.is_none(), + "start_capture_for refuses a new attempt while a completion is unclaimed" + ); self.finish_attempt_resources(); if !matches!(outcome, ScreenAcquisitionOutcome::Ready { .. }) { input_state.set_frozen_active(false); @@ -462,6 +473,8 @@ fn next_capture_backend_after( #[cfg(test)] mod tests { + use crate::backend::wayland::portal_task::PortalTask; + use super::*; use crate::backend::wayland::acquisition::{ ScreenAcquisitionCompletion, ScreenAcquisitionOutcome, ScreenAcquisitionOwner, @@ -1000,9 +1013,9 @@ mod tests { fn cancel_clears_an_in_flight_portal_capture() { let mut state = FrozenState::new(None); let mut input_state = make_test_input_state(); - state.portal.start( - crate::backend::wayland::portal_task::PortalTask::disconnected_for_test(Instant::now()), - ); + state + .portal + .start(PortalTask::disconnected_for_test(Instant::now())); state.cancel(&mut input_state); @@ -1364,8 +1377,10 @@ mod tests { .map_err(anyhow::Error::msg) .unwrap_err(); - state - .finish_preflight_failure(CapturePreflightError::from_backend(error), &mut input_state); + state.finish_preflight_failure( + &CapturePreflightError::from_backend(error), + &mut input_state, + ); assert_eq!( state @@ -1384,7 +1399,7 @@ mod tests { state.take_preflight_pending(); assert!(state.queue_portal_layout_retry(Some(1), true)); - state.finish_preflight_failure(CapturePreflightError::LostSuppression, &mut input_state); + state.finish_preflight_failure(&CapturePreflightError::LostSuppression, &mut input_state); assert!(!state.has_layout_retry()); assert!(!state.is_in_progress()); diff --git a/src/backend/wayland/frozen/state/activation.rs b/src/backend/wayland/frozen/state/activation.rs index a6333492d..76aa8b5ea 100644 --- a/src/backend/wayland/frozen/state/activation.rs +++ b/src/backend/wayland/frozen/state/activation.rs @@ -1,7 +1,13 @@ //! Validate and install completed output images; portal pixels are processed on the worker. -use super::*; +use super::{FrozenCaptureSource, FrozenState, PendingFrozenImage}; +use crate::backend::wayland::acquisition::ScreenAcquisitionOutcome; +use crate::backend::wayland::frozen::{FrozenImage, ScreenImageProvenance}; +use crate::backend::wayland::frozen_geometry::OutputGeometry; use crate::backend::wayland::portal_capture::layout_token_matches; +use crate::input::InputState; use log::info; +use std::sync::Arc; +use wayland_client::protocol::wl_output; impl FrozenState { pub fn set_pending_output_image( diff --git a/src/backend/wayland/frozen/state/admission.rs b/src/backend/wayland/frozen/state/admission.rs index 25b31950d..5cacb7b08 100644 --- a/src/backend/wayland/frozen/state/admission.rs +++ b/src/backend/wayland/frozen/state/admission.rs @@ -1,6 +1,8 @@ //! Backend selection and admission of the retained Freeze request. -use super::*; -use crate::backend::wayland::capture_preflight::{CaptureBackend, CaptureLayoutScope}; +use super::{FrozenCaptureBackend, FrozenState}; +use crate::backend::wayland::capture_preflight::{ + CaptureBackend, CaptureLayoutScope, CapturePreflightError, +}; use crate::backend::wayland::state::OverlaySuppression; impl CaptureBackend for FrozenCaptureBackend { diff --git a/src/backend/wayland/handlers/activation.rs b/src/backend/wayland/handlers/activation.rs index 44b7cbcd8..de5b8223d 100644 --- a/src/backend/wayland/handlers/activation.rs +++ b/src/backend/wayland/handlers/activation.rs @@ -1,9 +1,7 @@ // Handles xdg-activation events so GNOME will focus the fallback xdg overlay window. use log::debug; -use smithay_client_toolkit::{ - activation::{ActivationHandler, RequestData}, - globals::GlobalData, -}; +use smithay_client_toolkit::activation::{ActivationHandler, RequestData}; +use smithay_client_toolkit::globals::GlobalData; use wayland_client::{Connection, Dispatch, QueueHandle}; use wayland_protocols::xdg::activation::v1::client::{xdg_activation_token_v1, xdg_activation_v1}; diff --git a/src/backend/wayland/handlers/buffer.rs b/src/backend/wayland/handlers/buffer.rs index f3f844115..43f7f08b1 100644 --- a/src/backend/wayland/handlers/buffer.rs +++ b/src/backend/wayland/handlers/buffer.rs @@ -1,6 +1,7 @@ // Listens for wl_buffer release events so SlotPool buffers can re-enter circulation. use log::debug; -use wayland_client::{Connection, Dispatch, QueueHandle, protocol::wl_buffer}; +use wayland_client::protocol::wl_buffer; +use wayland_client::{Connection, Dispatch, QueueHandle}; use super::super::state::WaylandState; diff --git a/src/backend/wayland/handlers/compositor.rs b/src/backend/wayland/handlers/compositor.rs index 169a3be69..79cbca960 100644 --- a/src/backend/wayland/handlers/compositor.rs +++ b/src/backend/wayland/handlers/compositor.rs @@ -2,10 +2,8 @@ // can throttle rendering; invoked by smithay through the delegate in `mod.rs`. use log::{debug, info}; use smithay_client_toolkit::compositor::CompositorHandler; -use wayland_client::{ - Connection, Dispatch, QueueHandle, - protocol::{wl_callback, wl_output, wl_surface}, -}; +use wayland_client::protocol::{wl_callback, wl_output, wl_surface}; +use wayland_client::{Connection, Dispatch, QueueHandle}; use super::super::state::{FullDamageReason, WaylandState}; use super::super::surface::MainSurfaceFrameCallback; @@ -154,13 +152,15 @@ impl CompositorHandler for WaylandState { #[cfg(test)] mod tests { + use crate::config::Config; + use super::*; use crate::backend::wayland::frozen::FrozenImage; use crate::backend::wayland::handlers::test_support::HandlerFixture; #[test] fn reenter_and_neighbour_leave_preserve_installed_freeze_and_zoom() { - let mut fixture = HandlerFixture::with_outputs(crate::config::Config::default(), 2); + let mut fixture = HandlerFixture::with_outputs(Config::default(), 2); let outputs: Vec<_> = fixture.state.protocol.output().outputs().collect(); let qh = fixture.queue.handle(); let surface = fixture.state.surface.wl_surface().unwrap().clone(); diff --git a/src/backend/wayland/handlers/ext_image_copy.rs b/src/backend/wayland/handlers/ext_image_copy.rs index 3014d11d7..8a05732c9 100644 --- a/src/backend/wayland/handlers/ext_image_copy.rs +++ b/src/backend/wayland/handlers/ext_image_copy.rs @@ -1,19 +1,11 @@ // Dispatch handlers for ext-image-copy-capture objects used by frozen mode. use log::debug; use wayland_client::{Connection, Dispatch, QueueHandle}; -use wayland_protocols::ext::{ - image_capture_source::v1::client::{ - ext_image_capture_source_v1::{Event as SourceEvent, ExtImageCaptureSourceV1}, - ext_output_image_capture_source_manager_v1::{ - Event as OutputSourceManagerEvent, ExtOutputImageCaptureSourceManagerV1, - }, - }, - image_copy_capture::v1::client::{ - ext_image_copy_capture_frame_v1::{Event as FrameEvent, ExtImageCopyCaptureFrameV1}, - ext_image_copy_capture_manager_v1::{Event as ManagerEvent, ExtImageCopyCaptureManagerV1}, - ext_image_copy_capture_session_v1::{Event as SessionEvent, ExtImageCopyCaptureSessionV1}, - }, -}; +use wayland_protocols::ext::image_capture_source::v1::client::ext_image_capture_source_v1::{Event as SourceEvent, ExtImageCaptureSourceV1}; +use wayland_protocols::ext::image_capture_source::v1::client::ext_output_image_capture_source_manager_v1::{Event as OutputSourceManagerEvent, ExtOutputImageCaptureSourceManagerV1}; +use wayland_protocols::ext::image_copy_capture::v1::client::ext_image_copy_capture_frame_v1::{Event as FrameEvent, ExtImageCopyCaptureFrameV1}; +use wayland_protocols::ext::image_copy_capture::v1::client::ext_image_copy_capture_manager_v1::{Event as ManagerEvent, ExtImageCopyCaptureManagerV1}; +use wayland_protocols::ext::image_copy_capture::v1::client::ext_image_copy_capture_session_v1::{Event as SessionEvent, ExtImageCopyCaptureSessionV1}; use super::super::frozen::FrozenCaptureBackend; use super::super::state::WaylandState; @@ -66,7 +58,7 @@ impl Dispatch for WaylandState { _conn: &Connection, qh: &QueueHandle, ) { - let should_fallback = state.frozen.handle_ext_session_event(event, qh); + let should_fallback = state.frozen.handle_ext_session_event(&event, qh); if should_fallback { state.continue_frozen_capture_after_failure(FrozenCaptureBackend::ExtImageCopy, qh); } @@ -84,7 +76,7 @@ impl Dispatch for WaylandState { ) { if state .frozen - .handle_ext_frame_event(event, qh, &mut state.input_state) + .handle_ext_frame_event(&event, qh, &mut state.input_state) { state.continue_frozen_capture_after_failure(FrozenCaptureBackend::ExtImageCopy, qh); } diff --git a/src/backend/wayland/handlers/keyboard/mod.rs b/src/backend/wayland/handlers/keyboard/mod.rs index 50ffb33ae..16fc508fd 100644 --- a/src/backend/wayland/handlers/keyboard/mod.rs +++ b/src/backend/wayland/handlers/keyboard/mod.rs @@ -4,15 +4,18 @@ mod translate; use log::{debug, trace, warn}; use smithay_client_toolkit::seat::keyboard::{ - KeyEvent, KeyboardHandler, Modifiers, RawModifiers, RepeatInfo, + KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers, RepeatInfo, }; use std::time::{Duration, Instant}; -use wayland_client::{ - Connection, QueueHandle, - protocol::{wl_keyboard, wl_surface}, -}; +use wayland_client::protocol::{wl_keyboard, wl_surface}; +use wayland_client::{Connection, QueueHandle}; -use crate::{config::Action, input::Key, notification}; +use crate::backend::wayland::state::WindowSnapDirection; +use crate::config::{Action, ChordModifiers}; +use crate::input::Key; +use crate::input::state::RegionPurposeTag; +use crate::notification; +use crate::ui::RegionAction; use super::super::state::{KeyRepeatTiming, KeyboardId, WaylandState}; pub(in crate::backend::wayland) use press::{ForwardedKey, KeyPressSource}; @@ -27,19 +30,25 @@ pub(in crate::backend::wayland) enum XdgFocusLeaveAction { Exit, } +/// What the overlay knows when keyboard focus leaves its surface. +#[derive(Debug, Clone, Copy)] +pub(in crate::backend::wayland) struct XdgFocusLeaveContext { + pub(in crate::backend::wayland) is_xdg_window: bool, + pub(in crate::backend::wayland) desktop_open_in_progress: bool, + pub(in crate::backend::wayland) focus_exit_suppressed: bool, + pub(in crate::backend::wayland) focus_loss_exits_overlay: bool, +} + pub(in crate::backend::wayland) fn xdg_focus_leave_action( - is_xdg_window: bool, - desktop_open_in_progress: bool, - focus_exit_suppressed: bool, - focus_loss_exits_overlay: bool, + context: XdgFocusLeaveContext, ) -> XdgFocusLeaveAction { - if !is_xdg_window { + if !context.is_xdg_window { XdgFocusLeaveAction::Ignore - } else if desktop_open_in_progress { + } else if context.desktop_open_in_progress { XdgFocusLeaveAction::AwaitDesktopOpen - } else if focus_exit_suppressed { + } else if context.focus_exit_suppressed { XdgFocusLeaveAction::RestoreClipboardFocus - } else if focus_loss_exits_overlay { + } else if context.focus_loss_exits_overlay { XdgFocusLeaveAction::Exit } else { XdgFocusLeaveAction::StayOpen @@ -55,7 +64,7 @@ impl KeyboardHandler for WaylandState { surface: &wl_surface::WlSurface, serial: u32, _raw: &[u32], - _keysyms: &[smithay_client_toolkit::seat::keyboard::Keysym], + _keysyms: &[Keysym], ) { debug!("Keyboard focus entered"); self.focus.keyboard_entered(); @@ -94,12 +103,12 @@ impl KeyboardHandler for WaylandState { debug!("Keyboard focus left"); self.teardown_keyboard_focus(); - match xdg_focus_leave_action( - self.surface.is_xdg_window(), - self.desktop_open_in_progress(), - self.focus.exit_suppressed(Instant::now()), - self.xdg_focus_loss_exits_overlay(), - ) { + match xdg_focus_leave_action(XdgFocusLeaveContext { + is_xdg_window: self.surface.is_xdg_window(), + desktop_open_in_progress: self.desktop_open_in_progress(), + focus_exit_suppressed: self.focus.exit_suppressed(Instant::now()), + focus_loss_exits_overlay: self.xdg_focus_loss_exits_overlay(), + }) { XdgFocusLeaveAction::Ignore => {} XdgFocusLeaveAction::AwaitDesktopOpen => { // The opener deliberately transfers focus. Overlay exit waits @@ -196,12 +205,12 @@ impl KeyboardHandler for WaylandState { ); // Trust compositor-reported modifier state to reconcile any missed key release // events and avoid "stuck" modifiers. - self.input_state.sync_modifiers( - modifiers.shift, - modifiers.ctrl, - modifiers.alt, - modifiers.logo, - ); + self.input_state.sync_modifiers(ChordModifiers { + ctrl: modifiers.ctrl, + shift: modifiers.shift, + alt: modifiers.alt, + logo: modifiers.logo, + }); self.sync_region_square_modifier(modifiers.shift); } @@ -339,8 +348,7 @@ impl WaylandState { return true; } if action == Action::CopyTextFromScreen - && self.input_state.region_state().purpose() - == Some(crate::input::state::RegionPurposeTag::Ocr) + && self.input_state.region_state().purpose() == Some(RegionPurposeTag::Ocr) { self.cancel_ocr(); return true; @@ -510,7 +518,7 @@ fn screen_modal_swallows_key_release( /// to submit. fn region_select_all_pressed( region_active: bool, - purpose: Option, + purpose: Option, ctrl: bool, key: Key, ) -> bool { @@ -522,10 +530,7 @@ fn region_select_all_pressed( && matches!(key, Key::Char('a' | 'A')) } -fn region_window_snap_direction( - key: Key, - logo: bool, -) -> Option { +fn region_window_snap_direction(key: Key, logo: bool) -> Option { use crate::backend::wayland::state::WindowSnapDirection; if !logo { @@ -543,7 +548,7 @@ fn region_window_snap_direction( #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum RegionReviewKeyAction { Nudge(i64, i64), - Submit(crate::ui::RegionAction), + Submit(RegionAction), } fn region_review_key_action(key: Key, ctrl: bool, shift: bool) -> Option { @@ -553,42 +558,36 @@ fn region_review_key_action(key: Key, ctrl: bool, shift: bool) -> Option Some(RegionReviewKeyAction::Nudge(step, 0)), Key::Up => Some(RegionReviewKeyAction::Nudge(0, -step)), Key::Down => Some(RegionReviewKeyAction::Nudge(0, step)), - Key::Char('c' | 'C') if ctrl => { - Some(RegionReviewKeyAction::Submit(crate::ui::RegionAction::Copy)) - } - Key::Char('s' | 'S') if ctrl => { - Some(RegionReviewKeyAction::Submit(crate::ui::RegionAction::Save)) - } - Key::Return => Some(RegionReviewKeyAction::Submit(crate::ui::RegionAction::Both)), - Key::Char('b' | 'B') if !ctrl => Some(RegionReviewKeyAction::Submit( - crate::ui::RegionAction::Board, - )), + Key::Char('c' | 'C') if ctrl => Some(RegionReviewKeyAction::Submit(RegionAction::Copy)), + Key::Char('s' | 'S') if ctrl => Some(RegionReviewKeyAction::Submit(RegionAction::Save)), + Key::Return => Some(RegionReviewKeyAction::Submit(RegionAction::Both)), + Key::Char('b' | 'B') if !ctrl => Some(RegionReviewKeyAction::Submit(RegionAction::Board)), Key::Char('d' | 'D') if !ctrl => Some(RegionReviewKeyAction::Submit( - crate::ui::RegionAction::ToggleIncludeDrawings, - )), - Key::Char('x' | 'X') if !ctrl => Some(RegionReviewKeyAction::Submit( - crate::ui::RegionAction::CutBand, - )), - Key::Char('z' | 'Z') if ctrl && shift => Some(RegionReviewKeyAction::Submit( - crate::ui::RegionAction::RedoCut, - )), - Key::Char('z' | 'Z') if ctrl => Some(RegionReviewKeyAction::Submit( - crate::ui::RegionAction::UndoCut, - )), - Key::Char('y' | 'Y') if ctrl && !shift => Some(RegionReviewKeyAction::Submit( - crate::ui::RegionAction::RedoCut, + RegionAction::ToggleIncludeDrawings, )), + Key::Char('x' | 'X') if !ctrl => Some(RegionReviewKeyAction::Submit(RegionAction::CutBand)), + Key::Char('z' | 'Z') if ctrl && shift => { + Some(RegionReviewKeyAction::Submit(RegionAction::RedoCut)) + } + Key::Char('z' | 'Z') if ctrl => Some(RegionReviewKeyAction::Submit(RegionAction::UndoCut)), + Key::Char('y' | 'Y') if ctrl && !shift => { + Some(RegionReviewKeyAction::Submit(RegionAction::RedoCut)) + } _ => None, } } #[cfg(test)] mod tests { + use std::num::NonZeroU32; + + use crate::input::state::{RegionPurposeTag, ScreenCaptureSource, test_support}; + use super::*; fn repeat(rate: u32, delay: u32) -> RepeatInfo { RepeatInfo::Repeat { - rate: std::num::NonZeroU32::new(rate).expect("non-zero rate"), + rate: NonZeroU32::new(rate).expect("non-zero rate"), delay, } } @@ -646,13 +645,12 @@ mod tests { assert!(screen_modal_swallows_key_release(false, true)); assert!(!screen_modal_swallows_key_release(false, false)); - let mut input = crate::input::state::test_support::make_test_input_state(); - input.sync_modifiers(true, false, false, false); - input.set_region_pending_capture( - crate::input::state::RegionPurposeTag::Ocr, - 1, - crate::input::state::ScreenCaptureSource::Frozen, - ); + let mut input = test_support::make_test_input_state(); + input.sync_modifiers(ChordModifiers { + shift: true, + ..ChordModifiers::default() + }); + input.set_region_pending_capture(RegionPurposeTag::Ocr, 1, ScreenCaptureSource::Frozen); if !screen_modal_swallows_key_release(input.region_is_engaged(), false) { input.on_key_release(Key::Shift); } @@ -660,7 +658,7 @@ mod tests { input.modifiers.shift, "key release must not outrun modifier sync" ); - input.sync_modifiers(false, false, false, false); + input.sync_modifiers(ChordModifiers::default()); assert!( !input.modifiers.shift, "compositor modifier sync is authoritative" @@ -740,54 +738,44 @@ mod tests { ); assert_eq!( region_review_key_action(Key::Char('c'), true, false), - Some(RegionReviewKeyAction::Submit(crate::ui::RegionAction::Copy)) + Some(RegionReviewKeyAction::Submit(RegionAction::Copy)) ); assert_eq!( region_review_key_action(Key::Char('s'), true, false), - Some(RegionReviewKeyAction::Submit(crate::ui::RegionAction::Save)) + Some(RegionReviewKeyAction::Submit(RegionAction::Save)) ); assert_eq!( region_review_key_action(Key::Return, false, false), - Some(RegionReviewKeyAction::Submit(crate::ui::RegionAction::Both)) + Some(RegionReviewKeyAction::Submit(RegionAction::Both)) ); assert_eq!( region_review_key_action(Key::Char('b'), false, false), - Some(RegionReviewKeyAction::Submit( - crate::ui::RegionAction::Board - )) + Some(RegionReviewKeyAction::Submit(RegionAction::Board)) ); assert_eq!(region_review_key_action(Key::Char('b'), true, false), None); assert_eq!( region_review_key_action(Key::Char('d'), false, false), Some(RegionReviewKeyAction::Submit( - crate::ui::RegionAction::ToggleIncludeDrawings + RegionAction::ToggleIncludeDrawings )) ); assert_eq!(region_review_key_action(Key::Char('d'), true, false), None); assert_eq!( region_review_key_action(Key::Char('x'), false, false), - Some(RegionReviewKeyAction::Submit( - crate::ui::RegionAction::CutBand - )) + Some(RegionReviewKeyAction::Submit(RegionAction::CutBand)) ); assert_eq!(region_review_key_action(Key::Char('x'), true, false), None); assert_eq!( region_review_key_action(Key::Char('z'), true, false), - Some(RegionReviewKeyAction::Submit( - crate::ui::RegionAction::UndoCut - )) + Some(RegionReviewKeyAction::Submit(RegionAction::UndoCut)) ); assert_eq!( region_review_key_action(Key::Char('z'), true, true), - Some(RegionReviewKeyAction::Submit( - crate::ui::RegionAction::RedoCut - )) + Some(RegionReviewKeyAction::Submit(RegionAction::RedoCut)) ); assert_eq!( region_review_key_action(Key::Char('y'), true, false), - Some(RegionReviewKeyAction::Submit( - crate::ui::RegionAction::RedoCut - )) + Some(RegionReviewKeyAction::Submit(RegionAction::RedoCut)) ); } @@ -827,12 +815,12 @@ mod tests { for focus_exit_suppressed in [false, true] { for focus_loss_exits_overlay in [false, true] { assert_eq!( - xdg_focus_leave_action( - true, - true, + xdg_focus_leave_action(XdgFocusLeaveContext { + is_xdg_window: true, + desktop_open_in_progress: true, focus_exit_suppressed, focus_loss_exits_overlay, - ), + }), XdgFocusLeaveAction::AwaitDesktopOpen, ); } @@ -841,20 +829,38 @@ mod tests { #[test] fn focus_leave_routing_preserves_non_desktop_open_policies() { + let idle_xdg = XdgFocusLeaveContext { + is_xdg_window: true, + desktop_open_in_progress: false, + focus_exit_suppressed: false, + focus_loss_exits_overlay: false, + }; assert_eq!( - xdg_focus_leave_action(false, true, true, true), + xdg_focus_leave_action(XdgFocusLeaveContext { + is_xdg_window: false, + desktop_open_in_progress: true, + focus_exit_suppressed: true, + focus_loss_exits_overlay: true, + }), XdgFocusLeaveAction::Ignore, ); assert_eq!( - xdg_focus_leave_action(true, false, true, true), + xdg_focus_leave_action(XdgFocusLeaveContext { + focus_exit_suppressed: true, + focus_loss_exits_overlay: true, + ..idle_xdg + }), XdgFocusLeaveAction::RestoreClipboardFocus, ); assert_eq!( - xdg_focus_leave_action(true, false, false, false), + xdg_focus_leave_action(idle_xdg), XdgFocusLeaveAction::StayOpen, ); assert_eq!( - xdg_focus_leave_action(true, false, false, true), + xdg_focus_leave_action(XdgFocusLeaveContext { + focus_loss_exits_overlay: true, + ..idle_xdg + }), XdgFocusLeaveAction::Exit, ); } diff --git a/src/backend/wayland/handlers/keyboard/press.rs b/src/backend/wayland/handlers/keyboard/press.rs index 49a21cdfa..9c803af58 100644 --- a/src/backend/wayland/handlers/keyboard/press.rs +++ b/src/backend/wayland/handlers/keyboard/press.rs @@ -8,6 +8,7 @@ use wayland_client::{Connection, QueueHandle}; use super::super::super::state::{KeyboardId, WaylandState}; use super::{is_repeatable_key, key_for_log, keysym_to_key, should_try_toolbar_key}; +use crate::config::ChordModifiers; use crate::input::Key; /// Where a key press came from. @@ -42,6 +43,16 @@ pub(in crate::backend::wayland) struct ForwardedKey { } impl ForwardedKey { + /// The modifier state GTK reported with this press. + pub(in crate::backend::wayland) fn modifiers(self) -> ChordModifiers { + ChordModifiers { + ctrl: self.ctrl, + shift: self.shift, + alt: self.alt, + logo: self.logo, + } + } + /// The overlay key this press stands for, or `None` for a bare modifier: /// its state already rides on the next forwarded key, and forwarding the /// press itself would latch a modifier whose release never arrives. @@ -74,17 +85,11 @@ impl WaylandState { }; let saved = self.input_state.modifiers; - self.input_state.sync_modifiers( - forwarded.shift, - forwarded.ctrl, - forwarded.alt, - forwarded.logo, - ); + self.input_state.sync_modifiers(forwarded.modifiers()); self.dispatch_key_press(key, KeyPressSource::GtkToolbar, conn, qh); self.input_state.modifiers.tab = saved.tab; - self.input_state - .sync_modifiers(saved.shift, saved.ctrl, saved.alt, saved.logo); + self.input_state.sync_modifiers(saved.chord_modifiers()); } /// Route one key press through the overlay's shortcut handling. diff --git a/src/backend/wayland/handlers/layer.rs b/src/backend/wayland/handlers/layer.rs index e1abbc773..29472a0a7 100644 --- a/src/backend/wayland/handlers/layer.rs +++ b/src/backend/wayland/handlers/layer.rs @@ -1,8 +1,8 @@ // Responds to layer-shell configure/close events, keeping dimensions in sync with the compositor. use log::{debug, info}; -use smithay_client_toolkit::shell::{ - WaylandSurface, - wlr_layer::{LayerShellHandler, LayerSurface, LayerSurfaceConfigure}, +use smithay_client_toolkit::shell::WaylandSurface; +use smithay_client_toolkit::shell::wlr_layer::{ + LayerShellHandler, LayerSurface, LayerSurfaceConfigure, }; use wayland_client::{Connection, Proxy, QueueHandle}; diff --git a/src/backend/wayland/handlers/output.rs b/src/backend/wayland/handlers/output.rs index 77164faba..0de8f5a1d 100644 --- a/src/backend/wayland/handlers/output.rs +++ b/src/backend/wayland/handlers/output.rs @@ -1,11 +1,13 @@ // Tracks monitor hotplug/updates so `WaylandState` can respond to geometry changes. use log::{Level, log, log_enabled, trace}; use smithay_client_toolkit::output::{OutputHandler, OutputState}; -use wayland_client::{Connection, Proxy, QueueHandle, protocol::wl_output}; +use wayland_client::protocol::wl_output; +use wayland_client::{Connection, Proxy, QueueHandle}; use super::super::capture_preflight::{CAPTURE_LOG_TARGET, LogField}; use super::super::state::WaylandState; +#[derive(Clone, Copy)] pub(super) enum CaptureOutputEvent { New, Updated, diff --git a/src/backend/wayland/handlers/pointer/axis.rs b/src/backend/wayland/handlers/pointer/axis.rs index e76f09913..9ad39023d 100644 --- a/src/backend/wayland/handlers/pointer/axis.rs +++ b/src/backend/wayland/handlers/pointer/axis.rs @@ -1,9 +1,16 @@ +use std::cmp::Ordering; +use std::time::Duration; + use log::debug; use smithay_client_toolkit::seat::pointer::{AxisScroll, PointerEvent}; use std::time::Instant; use wayland_client::protocol::wl_pointer; -use super::*; +use crate::draw::TextMeasurer; +use crate::input::DrawingState; + +use super::super::route::{InputSurface, RoutedInput}; +use crate::backend::wayland::state::WaylandState; use crate::input::Tool; use crate::input::state::{InputState, SpotlightWheelClaim, SpotlightWheelOutcome}; @@ -11,7 +18,7 @@ use crate::input::state::{InputState, SpotlightWheelClaim, SpotlightWheelOutcome /// /// Long enough that a pause mid-scroll does not split one adjustment in two, /// short enough that a later visit is separately undoable. -pub(super) const SPOTLIGHT_WHEEL_IDLE: std::time::Duration = std::time::Duration::from_millis(600); +pub(super) const SPOTLIGHT_WHEEL_IDLE: Duration = Duration::from_millis(600); fn scroll_direction(vertical: AxisScroll) -> i32 { if vertical.value120 != 0 { @@ -70,7 +77,7 @@ fn finalize_spotlight_wheel_if_axis_stopped( /// routing: SCTK may aggregate a final movement and stop in one frame. fn try_handle_spotlight_axis( input_state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, spotlight_wheel_idle_deadline: &mut Option, canvas_position: (i32, i32), vertical: AxisScroll, @@ -98,7 +105,11 @@ fn try_handle_spotlight_axis( if steps != 0 { let outcome = input_state .nudge_spotlight_magnification_at_with(measurer, canvas_x, canvas_y, steps); - debug_assert_ne!(outcome, SpotlightWheelOutcome::NotOverLoupe); + debug_assert_ne!( + outcome, + SpotlightWheelOutcome::NotOverLoupe, + "the wheel claim already found an adjustable loupe at this point" + ); debug!("Spotlight wheel at ({canvas_x}, {canvas_y}): {outcome:?}"); } } @@ -275,25 +286,25 @@ impl WaylandState { } match scroll_direction.cmp(&0) { - std::cmp::Ordering::Greater if self.input_state.modifiers.shift => { + Ordering::Greater if self.input_state.modifiers.shift => { self.input_state.adjust_font_size(-2.0); debug!( "Font size decreased: {:.1}px", self.input_state.style.current_font_size ); } - std::cmp::Ordering::Less if self.input_state.modifiers.shift => { + Ordering::Less if self.input_state.modifiers.shift => { self.input_state.adjust_font_size(2.0); debug!( "Font size increased: {:.1}px", self.input_state.style.current_font_size ); } - std::cmp::Ordering::Greater | std::cmp::Ordering::Less => { + Ordering::Greater | Ordering::Less => { let delta = if scroll_direction > 0 { -1.0 } else { 1.0 }; self.adjust_active_tool_thickness(delta, false); } - std::cmp::Ordering::Equal => {} + Ordering::Equal => {} } } @@ -306,9 +317,7 @@ impl WaylandState { return false; } - if scroll_direction != 0 - && matches!(self.input_state.state, crate::input::DrawingState::Idle) - { + if scroll_direction != 0 && matches!(self.input_state.state, DrawingState::Idle) { self.handle_zoom_scroll(scroll_direction < 0, position.0, position.1); } true @@ -427,7 +436,7 @@ fn try_handle_board_picker_page_panel_axis( /// the tick. fn try_handle_properties_panel_axis( input_state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, position: (f64, f64), scroll_direction: i32, ) -> bool { @@ -441,10 +450,13 @@ fn try_handle_properties_panel_axis( #[cfg(test)] mod tests { + use crate::ui_text::UiTextEngine; + use super::*; use crate::config::Action; use crate::draw::{Frame, Shape}; - use crate::input::state::{BoardPickerFocus, test_support::make_test_input_state}; + use crate::input::state::BoardPickerFocus; + use crate::input::state::test_support::make_test_input_state; use std::time::Duration; #[test] @@ -482,7 +494,7 @@ mod tests { #[test] fn board_picker_page_panel_axis_consumes_before_thickness_changes() { let mut input_state = make_test_input_state(); - input_state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input_state.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input_state .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -513,7 +525,7 @@ mod tests { // both board-picker wheel routes before the registry swallows the tick. let mut input_state = make_test_input_state(); input_state.switch_board_force("whiteboard"); - input_state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input_state.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input_state .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -523,12 +535,10 @@ mod tests { let page_panel = (layout.page_viewport_x + 1.0, layout.page_viewport_y + 1.0); input_state.board_picker_set_focus(BoardPickerFocus::PagePanel); - input_state - .board_picker_edit_color_selected_with_measurer(&crate::draw::TextMeasurer::default()); + input_state.board_picker_edit_color_selected_with_measurer(&TextMeasurer::default()); assert!( - input_state.open_color_picker_popup_for_board_paper_with_measurer( - &crate::draw::TextMeasurer::default() - ) + input_state + .open_color_picker_popup_for_board_paper_with_measurer(&TextMeasurer::default()) ); assert!(input_state.is_board_picker_open()); let frame = input_state.board_appearance_frame().expect("sheet frame"); @@ -573,7 +583,7 @@ mod tests { #[test] fn an_active_screen_modal_prevents_the_toolbar_scroll_route() { let mut input_state = make_test_input_state(); - input_state.activate_eyedropper_with(&crate::draw::TextMeasurer::default(), None); + input_state.activate_eyedropper_with(&TextMeasurer::default(), None); assert_eq!( axis_surface_route(&input_state, true, true), @@ -594,7 +604,7 @@ mod tests { // over the board picker, which makes it the one that can have the list // scrolled out from under it. let mut input_state = make_test_input_state(); - input_state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input_state.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input_state .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -652,8 +662,8 @@ mod tests { #[test] fn a_final_axis_delta_and_stop_complete_one_spotlight_gesture() { - let pointer_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let pointer_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &pointer_measurer, ui_engine: &test_ui_engine, @@ -727,8 +737,8 @@ mod tests { #[test] fn a_coalesced_value120_frame_applies_every_logical_step() { - let pointer_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let pointer_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &pointer_measurer, ui_engine: &test_ui_engine, @@ -788,8 +798,8 @@ mod tests { #[test] fn partial_value120_frames_accumulate_before_applying_a_logical_step() { - let pointer_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let pointer_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &pointer_measurer, ui_engine: &test_ui_engine, @@ -873,8 +883,8 @@ mod tests { #[test] fn a_finger_axis_pause_longer_than_the_wheel_timeout_stays_one_gesture() { - let pointer_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let pointer_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &pointer_measurer, ui_engine: &test_ui_engine, diff --git a/src/backend/wayland/handlers/pointer/cursor.rs b/src/backend/wayland/handlers/pointer/cursor.rs index a3440dfa4..4344191d2 100644 --- a/src/backend/wayland/handlers/pointer/cursor.rs +++ b/src/backend/wayland/handlers/pointer/cursor.rs @@ -1,7 +1,8 @@ use smithay_client_toolkit::seat::pointer::CursorIcon; use wayland_client::Connection; -use super::*; +use super::super::route::{InputSurface, RoutedInput}; +use crate::backend::wayland::state::WaylandState; use crate::backend::wayland::toolbar::ToolbarCursorHint; use crate::input::{ BoardPickerCursorHint, ColorPickerCursorHint, CommandPaletteCursorHint, ContextMenuCursorHint, @@ -373,16 +374,18 @@ impl WaylandState { #[cfg(test)] mod tests { + use crate::input::state::SelectionGrab; + use super::{CursorHint, ScreenModalCursorContext, drawing_state_cursor, screen_modal_cursor}; - use crate::{ - backend::wayland::toolbar::ToolbarCursorHint, - draw::{Shape, color::BLACK, frame::ShapeSnapshot}, - input::{ - BoardPickerCursorHint, ColorPickerCursorHint, CommandPaletteCursorHint, - ContextMenuCursorHint, DrawingState, HelpOverlayCursorHint, SelectionHandle, Tool, - }, - util::Rect, + use crate::backend::wayland::toolbar::ToolbarCursorHint; + use crate::draw::Shape; + use crate::draw::color::BLACK; + use crate::draw::frame::ShapeSnapshot; + use crate::input::{ + BoardPickerCursorHint, ColorPickerCursorHint, CommandPaletteCursorHint, + ContextMenuCursorHint, DrawingState, HelpOverlayCursorHint, SelectionHandle, Tool, }; + use crate::util::Rect; use smithay_client_toolkit::seat::pointer::CursorIcon; use std::sync::Arc; @@ -470,7 +473,7 @@ mod tests { ), ( DrawingState::MovingSelection { - grab: crate::input::state::SelectionGrab::new(0, 0, 0), + grab: SelectionGrab::new(0, 0, 0), last_x: 0, last_y: 0, snapshots: Vec::new(), diff --git a/src/backend/wayland/handlers/pointer/enter_leave.rs b/src/backend/wayland/handlers/pointer/enter_leave.rs index 0a868b80c..1f7afd102 100644 --- a/src/backend/wayland/handlers/pointer/enter_leave.rs +++ b/src/backend/wayland/handlers/pointer/enter_leave.rs @@ -4,7 +4,9 @@ use wayland_client::Connection; use crate::input::{EraserMode, Tool}; -use super::*; +use super::super::route::{InputSurface, RoutedInput}; +use crate::backend::wayland::state::WaylandState; +use crate::input::state::RegionInputSource; impl WaylandState { pub(super) fn handle_pointer_enter( diff --git a/src/backend/wayland/handlers/pointer/mod.rs b/src/backend/wayland/handlers/pointer/mod.rs index 6a7b92059..0b93617ab 100644 --- a/src/backend/wayland/handlers/pointer/mod.rs +++ b/src/backend/wayland/handlers/pointer/mod.rs @@ -1,13 +1,13 @@ // Feeds pointer events (motion/buttons/scroll) into the drawing state to keep the canvas reactive. use log::debug; use smithay_client_toolkit::seat::pointer::{PointerEvent, PointerEventKind, PointerHandler}; -use wayland_client::{Connection, QueueHandle, protocol::wl_pointer}; +use wayland_client::protocol::wl_pointer; +use wayland_client::{Connection, QueueHandle}; use crate::backend::wayland::state::{debug_toolbar_drag_logging_enabled, surface_id}; -use crate::input::state::RegionInputSource; use super::super::state::WaylandState; -use super::route::{InputSurface, RoutedInput}; +use super::route::InputSurface; mod axis; mod cursor; diff --git a/src/backend/wayland/handlers/pointer/motion.rs b/src/backend/wayland/handlers/pointer/motion.rs index cd05c6b31..b84943a71 100644 --- a/src/backend/wayland/handlers/pointer/motion.rs +++ b/src/backend/wayland/handlers/pointer/motion.rs @@ -4,9 +4,11 @@ use wayland_client::Connection; use crate::backend::wayland::state::{PerfInputSource, drag_log}; use crate::backend::wayland::toolbar_intent::intent_to_event; +use crate::input::DrawingState; -use super::*; -use crate::backend::wayland::state::ContactMotion; +use super::super::route::{InputSurface, RoutedInput}; +use crate::backend::wayland::state::{ContactMotion, WaylandState}; +use crate::input::state::RegionInputSource; impl WaylandState { pub(super) fn handle_pointer_motion( @@ -251,7 +253,7 @@ impl WaylandState { return; } let (wx, wy) = self.zoomed_world_coords(sx, sy); - let idle = matches!(self.input_state.state, crate::input::DrawingState::Idle); + let idle = matches!(self.input_state.state, DrawingState::Idle); self.update_onboarding_card_hover(idle.then_some((sx, sy))); // Hovering the onboarding card is chrome motion, not canvas motion. A // stroke already in progress keeps its motion as it crosses the card. diff --git a/src/backend/wayland/handlers/pointer/press.rs b/src/backend/wayland/handlers/pointer/press.rs index a06a6b2d4..bb6baedba 100644 --- a/src/backend/wayland/handlers/pointer/press.rs +++ b/src/backend/wayland/handlers/pointer/press.rs @@ -2,17 +2,21 @@ use log::debug; use smithay_client_toolkit::seat::pointer::{BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, PointerEvent}; use wayland_client::QueueHandle; -use crate::backend::wayland::state::{ContactOwner, RegionReviewPress, drag_log}; -use crate::backend::wayland::toolbar_intent::intent_to_event; +use crate::backend::wayland::state::{ContactOwner, RegionReviewPress}; +use crate::config::keybindings::linux; +use crate::config::{ChordModifiers, PointerButton, PointerTrigger, ShortcutTrigger, keybindings}; use crate::i18n::{tr, trf}; use crate::input::MouseButton; use crate::input::state::HelpOverlayPressSource; -use crate::ui::toolbar::ToolbarEvent; +#[cfg(test)] +use crate::ui::RegionAction; -use super::*; +use super::super::route::{InputSurface, RoutedInput}; +use crate::backend::wayland::state::WaylandState; +use crate::input::state::RegionInputSource; #[cfg(test)] -fn review_action_suppresses_next_release(action: crate::ui::RegionAction) -> bool { +fn review_action_suppresses_next_release(action: RegionAction) -> bool { action.is_terminal() } @@ -317,7 +321,7 @@ impl WaylandState { } fn try_dispatch_pointer_shortcut(&mut self, button: u32) -> bool { - let Some(pointer) = crate::config::keybindings::linux::pointer_button(button) else { + let Some(pointer) = linux::pointer_button(button) else { return false; }; if self.dispatch_pointer_shortcut(pointer) { @@ -331,34 +335,29 @@ impl WaylandState { pub(in crate::backend::wayland) fn try_dispatch_gdk_pointer_shortcut( &mut self, button: u32, - ctrl: bool, - shift: bool, - alt: bool, - logo: bool, + modifiers: ChordModifiers, ) -> bool { - let Some(pointer) = crate::config::keybindings::gdk::pointer_button(button) else { + let Some(pointer) = keybindings::gdk::pointer_button(button) else { return false; }; - self.dispatch_pointer_trigger(crate::config::PointerTrigger { + self.dispatch_pointer_trigger(PointerTrigger { button: pointer, - ctrl, - shift, - alt, - logo, + ctrl: modifiers.ctrl, + shift: modifiers.shift, + alt: modifiers.alt, + logo: modifiers.logo, }) } - fn dispatch_pointer_shortcut(&mut self, pointer: crate::config::PointerButton) -> bool { + fn dispatch_pointer_shortcut(&mut self, pointer: PointerButton) -> bool { match self.input_state.pointer_trigger(pointer) { - crate::config::ShortcutTrigger::Pointer(trigger) => { - self.dispatch_pointer_trigger(trigger) - } + ShortcutTrigger::Pointer(trigger) => self.dispatch_pointer_trigger(trigger), _ => false, } } - fn dispatch_pointer_trigger(&mut self, trigger: crate::config::PointerTrigger) -> bool { - let shortcut = crate::config::ShortcutTrigger::Pointer(trigger); + fn dispatch_pointer_trigger(&mut self, trigger: PointerTrigger) -> bool { + let shortcut = ShortcutTrigger::Pointer(trigger); let Some(action) = self.input_state.find_trigger_action(&shortcut) else { return false; }; @@ -377,7 +376,7 @@ fn input_hud_button_label(button: u32) -> String { BTN_LEFT => tr("Click").to_string(), BTN_RIGHT => tr("Right Click").to_string(), BTN_MIDDLE => tr("Middle Click").to_string(), - other => crate::config::keybindings::linux::pointer_button(other) + other => linux::pointer_button(other) .map(|button| button.name()) .unwrap_or_else(|| trf("Button {}", &[&other])), } diff --git a/src/backend/wayland/handlers/pointer/press/toolbar.rs b/src/backend/wayland/handlers/pointer/press/toolbar.rs index d197a4ff4..e99cf992b 100644 --- a/src/backend/wayland/handlers/pointer/press/toolbar.rs +++ b/src/backend/wayland/handlers/pointer/press/toolbar.rs @@ -1,5 +1,12 @@ //! Presses taken by the inline strip, toolbar surfaces, and toolbar menus. -use super::*; +use crate::backend::wayland::state::{ + ContactOwner, WaylandState, debug_toolbar_drag_logging_enabled, drag_log, surface_id, +}; +use crate::backend::wayland::toolbar_intent::intent_to_event; +use crate::ui::toolbar::ToolbarEvent; +use log::debug; +use smithay_client_toolkit::seat::pointer::{BTN_LEFT, BTN_RIGHT, PointerEvent}; +use wayland_client::QueueHandle; impl WaylandState { /// The inline strip, a toolbar surface, and open toolbar menus take a press diff --git a/src/backend/wayland/handlers/pointer/release.rs b/src/backend/wayland/handlers/pointer/release.rs index 011afdb7f..a0f846ae5 100644 --- a/src/backend/wayland/handlers/pointer/release.rs +++ b/src/backend/wayland/handlers/pointer/release.rs @@ -4,11 +4,13 @@ use smithay_client_toolkit::seat::pointer::{BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Poi use wayland_client::Connection; use crate::backend::wayland::state::drag_log; -use crate::input::state::HelpOverlayPressSource; +use crate::input::state::{HelpOverlayPressSource, ZoomAnchor}; use crate::input::{HelpOverlayReleaseOutcome, MouseButton}; use crate::ui::ZoomChipPress; -use super::*; +use super::super::route::{InputSurface, RoutedInput}; +use crate::backend::wayland::state::{WaylandState, debug_toolbar_drag_logging_enabled}; +use crate::input::state::RegionInputSource; impl WaylandState { pub(super) fn handle_pointer_release( @@ -43,7 +45,7 @@ impl WaylandState { // Swallow releases after modal clicks (e.g., palette dismiss) if self .pointer - .take_suppressed_release(crate::input::state::RegionInputSource::Pointer) + .take_suppressed_release(RegionInputSource::Pointer) { self.pointer.clear_chrome_press(); return; @@ -254,10 +256,7 @@ impl WaylandState { .input_state .check_zoom_chip_click(kind, screen_x, screen_y); if let Some(action) = action { - self.dispatch_input_action_anchored( - action, - crate::input::state::ZoomAnchor::ScreenCenter, - ); + self.dispatch_input_action_anchored(action, ZoomAnchor::ScreenCenter); } } pressed.is_pending() diff --git a/src/backend/wayland/handlers/pointer/tests.rs b/src/backend/wayland/handlers/pointer/tests.rs index 5b4c5714a..71ac9460e 100644 --- a/src/backend/wayland/handlers/pointer/tests.rs +++ b/src/backend/wayland/handlers/pointer/tests.rs @@ -1,27 +1,37 @@ +use std::fs; +use std::time::Instant; + +use crate::backend::wayland::session::driver; +use crate::config::Config; +use crate::draw::Shape; +use crate::input::MouseButton; +use crate::session::SessionOptions; +use crate::{session, test_temp}; + use super::*; use crate::backend::wayland::handlers::test_support::HandlerFixture; use crate::input::DrawingState; use smithay_client_toolkit::seat::pointer::BTN_LEFT; -use wayland_client::{Proxy, protocol::wl_pointer::WlPointer}; +use wayland_client::Proxy; +use wayland_client::protocol::wl_pointer::WlPointer; #[test] fn unresolved_dirty_source_has_one_backoff_instead_of_an_idle_redraw_loop() { use crate::backend::wayland::session::SessionState; - let temp = crate::test_temp::tempdir().unwrap(); - let mut options = - crate::session::SessionOptions::new(temp.path().to_path_buf(), "failed-launch"); + let temp = test_temp::tempdir().unwrap(); + let mut options = SessionOptions::new(temp.path().to_path_buf(), "failed-launch"); options.persist_transparent = true; options.set_named_file_target(temp.path().join("failed.wayscriber-session")); - std::fs::write(options.session_file_path(), b"unreadable").unwrap(); - std::fs::write( - crate::session::append_path_suffix( + fs::write(options.session_file_path(), b"unreadable").unwrap(); + fs::write( + session::append_path_suffix( &options.session_file_path(), ".corrupt-18446744073709551615", ), b"occupied", ) .unwrap(); - let mut fixture = HandlerFixture::new(crate::config::Config::default()); + let mut fixture = HandlerFixture::new(Config::default()); fixture.state.session = SessionState::new(Some(options)); fixture .state @@ -30,12 +40,12 @@ fn unresolved_dirty_source_has_one_backoff_instead_of_an_idle_redraw_loop() { fixture .state .input_state - .on_mouse_press(crate::input::MouseButton::Left, 600, 400); + .on_mouse_press(MouseButton::Left, 600, 400); fixture.state.input_state.on_mouse_motion(620, 420); fixture .state .input_state - .on_mouse_release(crate::input::MouseButton::Left, 620, 420); + .on_mouse_release(MouseButton::Left, 620, 420); fixture .state .session @@ -56,20 +66,20 @@ fn unresolved_dirty_source_has_one_backoff_instead_of_an_idle_redraw_loop() { .pending_output_transition() .unwrap() .retry_at - > std::time::Instant::now() + > Instant::now() ); } -fn draw_line(fixture: &mut HandlerFixture) -> Vec { +fn draw_line(fixture: &mut HandlerFixture) -> Vec { fixture .state .input_state - .on_mouse_press(crate::input::MouseButton::Left, 600, 400); + .on_mouse_press(MouseButton::Left, 600, 400); fixture.state.input_state.on_mouse_motion(620, 420); fixture .state .input_state - .on_mouse_release(crate::input::MouseButton::Left, 620, 420); + .on_mouse_release(MouseButton::Left, 620, 420); let shapes = fixture .state .input_state @@ -86,11 +96,11 @@ fn draw_line(fixture: &mut HandlerFixture) -> Vec { #[test] fn leaving_the_per_output_placeholder_keeps_early_ink_and_finishes_before_input() { use crate::backend::wayland::session::SessionState; - let temp = crate::test_temp::tempdir().unwrap(); - let mut placeholder = crate::session::SessionOptions::new(temp.path().to_path_buf(), "startup"); + let temp = test_temp::tempdir().unwrap(); + let mut placeholder = SessionOptions::new(temp.path().to_path_buf(), "startup"); placeholder.persist_transparent = true; placeholder.per_output = true; - let mut fixture = HandlerFixture::new(crate::config::Config::default()); + let mut fixture = HandlerFixture::new(Config::default()); fixture.state.session = SessionState::new(Some(placeholder.clone())); fixture .state @@ -110,11 +120,11 @@ fn leaving_the_per_output_placeholder_keeps_early_ink_and_finishes_before_input( assert!(!fixture.state.session.is_dirty()); // The early ink went to the placeholder's own file, as on main, rather // than being held unsaveable on screen. - let saved = crate::session::load_snapshot(&placeholder) + let saved = session::load_snapshot(&placeholder) .unwrap() .expect("early ink saved"); - let points = |shape: &crate::draw::Shape| match shape { - crate::draw::Shape::Freehand { points, .. } => points.clone(), + let points = |shape: &Shape| match shape { + Shape::Freehand { points, .. } => points.clone(), other => panic!("expected the early stroke, got {other:?}"), }; let saved_points = saved.boards[0].pages.pages[0] @@ -128,7 +138,7 @@ fn leaving_the_per_output_placeholder_keeps_early_ink_and_finishes_before_input( fixture .state .session - .autosave_timeout(std::time::Instant::now(), &active), + .autosave_timeout(Instant::now(), &active), None ); } @@ -136,18 +146,17 @@ fn leaving_the_per_output_placeholder_keeps_early_ink_and_finishes_before_input( #[test] fn early_ink_on_an_unloaded_per_output_session_is_kept_and_its_retry_backs_off() { use crate::backend::wayland::session::SessionState; - let temp = crate::test_temp::tempdir().unwrap(); - let mut options = crate::session::SessionOptions::new(temp.path().to_path_buf(), "startup"); + let temp = test_temp::tempdir().unwrap(); + let mut options = SessionOptions::new(temp.path().to_path_buf(), "startup"); options.persist_transparent = true; options.per_output = true; - let mut fixture = HandlerFixture::new(crate::config::Config::default()); + let mut fixture = HandlerFixture::new(Config::default()); fixture.state.session = SessionState::new(Some(options.clone())); let shapes = draw_line(&mut fixture); fixture .state .begin_session_output_transition(Some("monitor-a".to_string()), "first monitor"); - crate::backend::wayland::session::driver::finish_pending_session_command(&mut fixture.state) - .unwrap(); + driver::finish_pending_session_command(&mut fixture.state).unwrap(); assert_eq!( fixture .state @@ -179,12 +188,12 @@ fn early_ink_on_an_unloaded_per_output_session_is_kept_and_its_retry_backs_off() assert!(!fixture.state.input_state.needs_redraw); } let retry = fixture.state.session.pending_output_transition().unwrap(); - assert!(retry.retry_at > std::time::Instant::now()); + assert!(retry.retry_at > Instant::now()); } #[test] fn alt_text_drag_ends_through_the_real_pointer_release_handler() { - let mut config = crate::config::Config::default(); + let mut config = Config::default(); config.ui.show_onboarding_hints = false; let mut fixture = HandlerFixture::new(config); fixture.state.input_state.state = DrawingState::text_input(100, 100, "hello".to_string()); diff --git a/src/backend/wayland/handlers/pointer_constraints.rs b/src/backend/wayland/handlers/pointer_constraints.rs index 74d2216ec..b076b5855 100644 --- a/src/backend/wayland/handlers/pointer_constraints.rs +++ b/src/backend/wayland/handlers/pointer_constraints.rs @@ -1,11 +1,8 @@ use smithay_client_toolkit::seat::pointer_constraints::PointerConstraintsHandler; -use wayland_client::{ - Connection, QueueHandle, - protocol::{wl_pointer, wl_surface}, -}; -use wayland_protocols::wp::pointer_constraints::zv1::client::{ - zwp_confined_pointer_v1::ZwpConfinedPointerV1, zwp_locked_pointer_v1::ZwpLockedPointerV1, -}; +use wayland_client::protocol::{wl_pointer, wl_surface}; +use wayland_client::{Connection, QueueHandle}; +use wayland_protocols::wp::pointer_constraints::zv1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; +use wayland_protocols::wp::pointer_constraints::zv1::client::zwp_locked_pointer_v1::ZwpLockedPointerV1; use super::super::state::WaylandState; diff --git a/src/backend/wayland/handlers/registry.rs b/src/backend/wayland/handlers/registry.rs index 560713de7..1cb014c1d 100644 --- a/src/backend/wayland/handlers/registry.rs +++ b/src/backend/wayland/handlers/registry.rs @@ -1,10 +1,8 @@ // Hooks up smithay's registry bookkeeping so other handler modules can request globals. -use smithay_client_toolkit::{ - output::OutputState, - registry::{ProvidesRegistryState, RegistryState}, - registry_handlers, - seat::SeatState, -}; +use smithay_client_toolkit::output::OutputState; +use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState}; +use smithay_client_toolkit::registry_handlers; +use smithay_client_toolkit::seat::SeatState; use super::super::state::WaylandState; diff --git a/src/backend/wayland/handlers/relative_pointer.rs b/src/backend/wayland/handlers/relative_pointer.rs index 57cbbf46e..388c6dca8 100644 --- a/src/backend/wayland/handlers/relative_pointer.rs +++ b/src/backend/wayland/handlers/relative_pointer.rs @@ -1,9 +1,9 @@ use smithay_client_toolkit::seat::relative_pointer::{RelativeMotionEvent, RelativePointerHandler}; -use wayland_client::{Connection, QueueHandle, protocol::wl_pointer}; +use wayland_client::protocol::wl_pointer; +use wayland_client::{Connection, QueueHandle}; use wayland_protocols::wp::relative_pointer::zv1::client::zwp_relative_pointer_v1::ZwpRelativePointerV1; -use super::super::state::WaylandState; -use super::super::state::drag_log; +use super::super::state::{WaylandState, drag_log}; impl RelativePointerHandler for WaylandState { fn relative_pointer_motion( diff --git a/src/backend/wayland/handlers/screencopy.rs b/src/backend/wayland/handlers/screencopy.rs index ba4fd8279..42f8349fb 100644 --- a/src/backend/wayland/handlers/screencopy.rs +++ b/src/backend/wayland/handlers/screencopy.rs @@ -1,9 +1,11 @@ // Dispatch handlers for wlr-screencopy objects used by frozen mode. use log::debug; use wayland_client::{Connection, Dispatch, QueueHandle}; -use wayland_protocols_wlr::screencopy::v1::client::{ - zwlr_screencopy_frame_v1::{Event as FrameEvent, ZwlrScreencopyFrameV1}, - zwlr_screencopy_manager_v1::{Event as ManagerEvent, ZwlrScreencopyManagerV1}, +use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::{ + Event as FrameEvent, ZwlrScreencopyFrameV1, +}; +use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::{ + Event as ManagerEvent, ZwlrScreencopyManagerV1, }; use super::super::frozen::FrozenCaptureBackend; @@ -32,10 +34,12 @@ impl Dispatch for WaylandState { qh: &QueueHandle, ) { if state.zoom.is_in_progress() { - state.zoom.handle_frame_event(event, &mut state.input_state); + state + .zoom + .handle_frame_event(&event, &mut state.input_state); } else if state .frozen - .handle_frame_event(event, &mut state.input_state) + .handle_frame_event(&event, &mut state.input_state) { state.continue_frozen_capture_after_failure(FrozenCaptureBackend::WlrScreencopy, qh); } diff --git a/src/backend/wayland/handlers/seat.rs b/src/backend/wayland/handlers/seat.rs index e611d5fc5..2faf9f437 100644 --- a/src/backend/wayland/handlers/seat.rs +++ b/src/backend/wayland/handlers/seat.rs @@ -1,10 +1,9 @@ // Manages seat capabilities (keyboard/pointer availability) and requests the matching devices. use log::{debug, info, warn}; -use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState, pointer::ThemeSpec}; -use wayland_client::{ - Connection, Proxy, QueueHandle, - protocol::{wl_keyboard, wl_seat, wl_touch}, -}; +use smithay_client_toolkit::seat::pointer::ThemeSpec; +use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState}; +use wayland_client::protocol::{wl_keyboard, wl_seat, wl_touch}; +use wayland_client::{Connection, Proxy, QueueHandle}; use super::super::state::{KeyboardId, WaylandState}; use crate::input::RegionInputSource; @@ -119,13 +118,13 @@ fn seat_device_has_release(version: u32) -> bool { version >= 3 } -fn release_keyboard(keyboard: wl_keyboard::WlKeyboard) { +fn release_keyboard(keyboard: &wl_keyboard::WlKeyboard) { if seat_device_has_release(keyboard.version()) { keyboard.release(); } } -fn release_touch(touch: wl_touch::WlTouch) { +fn release_touch(touch: &wl_touch::WlTouch) { if seat_device_has_release(touch.version()) { touch.release(); } @@ -139,7 +138,7 @@ impl WaylandState { debug!("Keyboard initialized"); if let Some(replaced) = self.focus.attach_keyboard(seat.clone(), keyboard) { self.key_repeat.forget_keyboard(KeyboardId::of(&replaced)); - release_keyboard(replaced); + release_keyboard(&replaced); } } Err(err) => warn!("Keyboard initialization failed: {}", err), @@ -149,7 +148,7 @@ impl WaylandState { fn release_seat_keyboard(&mut self, seat: &wl_seat::WlSeat) { if let Some(keyboard) = self.focus.detach_keyboard(seat) { self.key_repeat.forget_keyboard(KeyboardId::of(&keyboard)); - release_keyboard(keyboard); + release_keyboard(&keyboard); } } @@ -159,7 +158,7 @@ impl WaylandState { Ok(touch) => { debug!("Touch initialized"); if let Some(replaced) = self.pointer.attach_touch(seat.clone(), touch) { - release_touch(replaced); + release_touch(&replaced); } } Err(err) => warn!("Touch initialization failed: {}", err), @@ -172,7 +171,7 @@ impl WaylandState { return false; }; - release_touch(touch); + release_touch(&touch); true } diff --git a/src/backend/wayland/handlers/tablet/frame.rs b/src/backend/wayland/handlers/tablet/frame.rs index d05d3a0eb..08aed2400 100644 --- a/src/backend/wayland/handlers/tablet/frame.rs +++ b/src/backend/wayland/handlers/tablet/frame.rs @@ -1,21 +1,24 @@ +use std::mem; + use log::{debug, info}; use crate::backend::wayland::state::{ ContactOwner, PerfInputSource, StylusDownAdmission, WaylandState, }; use crate::config::keybindings::{StylusButton, linux}; -use crate::input::state::HelpOverlayPressSource; -use crate::input::{DrawingState, MouseButton}; +use crate::config::{Action, TabletInputConfig}; +use crate::input::state::{HelpOverlayPressSource, RegionInputSource}; +use crate::input::{DrawingState, InputState, MouseButton}; -fn modal_blocks_stylus_barrel_actions(input_state: &crate::input::InputState) -> bool { +fn modal_blocks_stylus_barrel_actions(input_state: &InputState) -> bool { input_state.modal_owns_pointer_shortcuts() } fn stylus_barrel_action( - input_state: &crate::input::InputState, + input_state: &InputState, button: u32, - tablet: &crate::config::TabletInputConfig, -) -> Option { + tablet: &TabletInputConfig, +) -> Option { let stylus = linux::stylus_button(button)?; let trigger = input_state.stylus_trigger(stylus); if let Some(action) = input_state.find_trigger_action(&trigger) { @@ -58,7 +61,7 @@ impl WaylandState { /// The real press router admits a contact before pressure mutates it. The /// first drawing sample is then replaced with this frame's pressure. pub(super) fn commit_pending_stylus_frame(&mut self) { - let pending = std::mem::take(&mut self.tablet.pending_frame); + let pending = mem::take(&mut self.tablet.pending_frame); if pending.is_empty() { return; } @@ -179,7 +182,7 @@ impl WaylandState { if self.input_state.region_is_active() { let (x, y) = self.current_stylus_position(); - self.begin_region_selection(crate::input::state::RegionInputSource::Stylus, x, y); + self.begin_region_selection(RegionInputSource::Stylus, x, y); return false; } diff --git a/src/backend/wayland/handlers/tablet/frame/tests.rs b/src/backend/wayland/handlers/tablet/frame/tests.rs index 3ac842530..609fd6a49 100644 --- a/src/backend/wayland/handlers/tablet/frame/tests.rs +++ b/src/backend/wayland/handlers/tablet/frame/tests.rs @@ -1,3 +1,12 @@ +use crate::backend::wayland::handlers::test_support::HandlerFixture; +use crate::config::{Config, TabletInputConfig}; +use crate::draw::TextMeasurer; +use crate::input::state::{Toast, ToastPriority, test_support}; +use crate::ui::OnboardingCardLayout; +use crate::ui::toolbar::ToolbarEvent; + +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::{Event, ZwpTabletToolV2}; + use super::{DrawingState, WaylandState, modal_blocks_stylus_barrel_actions, stylus_barrel_action}; use crate::input::state::test_support::make_test_input_state; @@ -7,7 +16,7 @@ fn hard_popup_taps_through_the_pen_frame_handler_do_not_resize_the_next_stroke() use wayland_client::{Dispatch, Proxy}; use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::{Event, ZwpTabletToolV2}; for chrome in ["card", "popover", "color button", "color slider", "radial"] { - let mut config = crate::config::Config::default(); + let mut config = Config::default(); config.ui.show_onboarding_hints = chrome == "card"; config.tablet.enabled = true; config.tablet.pressure_enabled = true; @@ -21,21 +30,19 @@ fn hard_popup_taps_through_the_pen_frame_handler_do_not_resize_the_next_stroke() let initial = state.input_state.style.current_thickness; let position = match chrome { "card" => { - state - .onboarding_card - .set_layout(Some(crate::ui::OnboardingCardLayout { - x: 100.0, - y: 50.0, - width: 200.0, - height: 120.0, - buttons: Vec::new(), - })); + state.onboarding_card.set_layout(Some(OnboardingCardLayout { + x: 100.0, + y: 50.0, + width: 200.0, + height: 120.0, + buttons: Vec::new(), + })); (110.0, 60.0) } "popover" => { - state.input_state.apply_toolbar_event( - crate::ui::toolbar::ToolbarEvent::ToggleSessionPopover(true), - ); + state + .input_state + .apply_toolbar_event(ToolbarEvent::ToggleSessionPopover(true)); (600.0, 400.0) } "radial" => { @@ -121,34 +128,24 @@ fn hard_popup_taps_through_the_pen_frame_handler_do_not_resize_the_next_stroke() } } -fn pen_fixture( - pressure_enabled: bool, -) -> ( - crate::backend::wayland::handlers::test_support::HandlerFixture, - wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::ZwpTabletToolV2, -) { +fn pen_fixture(pressure_enabled: bool) -> (HandlerFixture, ZwpTabletToolV2) { use wayland_client::Proxy; - let mut config = crate::config::Config::default(); + let mut config = Config::default(); config.ui.show_onboarding_hints = false; config.tablet.enabled = true; config.tablet.pressure_enabled = pressure_enabled; config.tablet.min_thickness = 1.0; config.tablet.max_thickness = 20.0; - let mut fixture = crate::backend::wayland::handlers::test_support::HandlerFixture::new(config); + let mut fixture = HandlerFixture::new(config); fixture.state.tablet.on_overlay = true; - let tool = - wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::ZwpTabletToolV2::inert( - fixture.conn.backend().downgrade(), - ); + let tool = ZwpTabletToolV2::inert(fixture.conn.backend().downgrade()); (fixture, tool) } fn send_pen( - fixture: &mut crate::backend::wayland::handlers::test_support::HandlerFixture, - tool: &wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::ZwpTabletToolV2, - events: impl IntoIterator< - Item = wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::Event, - >, + fixture: &mut HandlerFixture, + tool: &ZwpTabletToolV2, + events: impl IntoIterator, ) { use wayland_client::Dispatch; use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::ZwpTabletToolV2; @@ -223,11 +220,10 @@ fn a_pen_tap_on_a_toast_acts_on_the_toast_instead_of_drawing() { use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::Event; let (mut fixture, tool) = pen_fixture(true); let initial = fixture.state.input_state.style.current_thickness; - fixture.state.input_state.push_toast( - crate::input::state::ToastPriority::Info, - "test", - crate::input::state::Toast::info("Saved"), - ); + fixture + .state + .input_state + .push_toast(ToastPriority::Info, "test", Toast::info("Saved")); fixture .state .input_state @@ -280,7 +276,7 @@ fn a_pen_tap_that_closes_a_menu_keeps_a_thickness_set_since_the_last_stroke() { ); assert!(fixture.state.input_state.style.current_thickness < 3.0); // A mouse or keyboard change after the stroke, which the pen never saw. - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let _ = fixture .state .input_state @@ -288,7 +284,7 @@ fn a_pen_tap_that_closes_a_menu_keeps_a_thickness_set_since_the_last_stroke() { fixture .state .input_state - .apply_toolbar_event(crate::ui::toolbar::ToolbarEvent::ToggleSessionPopover(true)); + .apply_toolbar_event(ToolbarEvent::ToggleSessionPopover(true)); send_pen( &mut fixture, &tool, @@ -325,18 +321,14 @@ fn screen_region_modals_block_stylus_barrel_actions() { state.set_region_pending_capture(RegionPurposeTag::Ocr, 1, ScreenCaptureSource::Frozen); assert!(modal_blocks_stylus_barrel_actions(&state)); - state.activate_region_with( - &crate::draw::TextMeasurer::default(), - RegionPurposeTag::Ocr, - 1, - ); + state.activate_region_with(&TextMeasurer::default(), RegionPurposeTag::Ocr, 1); assert!(modal_blocks_stylus_barrel_actions(&state)); state.cancel_region_ui_only(); assert!(!modal_blocks_stylus_barrel_actions(&state)); state.set_eyedropper_pending_capture(EyedropperCaptureSource::Frozen); assert!(modal_blocks_stylus_barrel_actions(&state)); - state.activate_eyedropper_with(&crate::draw::TextMeasurer::default(), Some(1)); + state.activate_eyedropper_with(&TextMeasurer::default(), Some(1)); assert!(modal_blocks_stylus_barrel_actions(&state)); state.cancel_eyedropper(); assert!(!modal_blocks_stylus_barrel_actions(&state)); @@ -351,10 +343,10 @@ fn canonical_stylus_shortcut_wins_over_legacy_tablet_binding() { keybindings.core.undo = vec!["StylusPrimary".to_string()]; let action_map = keybindings.build_action_map().expect("map"); let action_bindings = keybindings.build_action_bindings().expect("bindings"); - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.set_keybinding_maps(action_map, action_bindings); - let mut tablet = crate::config::TabletInputConfig::default(); + let mut tablet = TabletInputConfig::default(); tablet.stylus_button.action = Some(Action::ToggleRadialMenu); assert_eq!( @@ -373,8 +365,8 @@ fn unbound_stylus_falls_back_to_legacy_tablet_action() { use crate::config::Action; use crate::config::keybindings::linux; - let state = crate::input::state::test_support::make_test_input_state(); - let mut tablet = crate::config::TabletInputConfig::default(); + let state = test_support::make_test_input_state(); + let mut tablet = TabletInputConfig::default(); tablet.stylus_button.action = Some(Action::ToggleRadialMenu); tablet.stylus_button2.action = None; diff --git a/src/backend/wayland/handlers/tablet/pad.rs b/src/backend/wayland/handlers/tablet/pad.rs index b0549445d..31e1d3433 100644 --- a/src/backend/wayland/handlers/tablet/pad.rs +++ b/src/backend/wayland/handlers/tablet/pad.rs @@ -1,9 +1,9 @@ use log::{debug, info, warn}; use std::sync::Arc; +use wayland_client::backend::ObjectData; use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; -use wayland_protocols::wp::tablet::zv2::client::{ - zwp_tablet_pad_group_v2::ZwpTabletPadGroupV2, zwp_tablet_pad_v2::ZwpTabletPadV2, -}; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_group_v2::ZwpTabletPadGroupV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_v2::ZwpTabletPadV2; use super::IgnoredObjectData; use crate::backend::wayland::state::WaylandState; @@ -72,10 +72,7 @@ impl Dispatch for WaylandState { } } - fn event_created_child( - opcode: u16, - qhandle: &QueueHandle, - ) -> Arc { + fn event_created_child(opcode: u16, qhandle: &QueueHandle) -> Arc { use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_v2::EVT_GROUP_OPCODE; match opcode { EVT_GROUP_OPCODE => qhandle.make_data::(()), diff --git a/src/backend/wayland/handlers/tablet/pad_group.rs b/src/backend/wayland/handlers/tablet/pad_group.rs index d4e70d5f0..addfb93b8 100644 --- a/src/backend/wayland/handlers/tablet/pad_group.rs +++ b/src/backend/wayland/handlers/tablet/pad_group.rs @@ -1,10 +1,10 @@ use log::{debug, warn}; use std::sync::Arc; +use wayland_client::backend::ObjectData; use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; -use wayland_protocols::wp::tablet::zv2::client::{ - zwp_tablet_pad_group_v2::ZwpTabletPadGroupV2, zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2, - zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2, -}; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_group_v2::ZwpTabletPadGroupV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2; use super::IgnoredObjectData; use crate::backend::wayland::state::WaylandState; @@ -47,10 +47,7 @@ impl Dispatch for WaylandState { } } - fn event_created_child( - opcode: u16, - qhandle: &QueueHandle, - ) -> Arc { + fn event_created_child(opcode: u16, qhandle: &QueueHandle) -> Arc { use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_group_v2::{ EVT_RING_OPCODE, EVT_STRIP_OPCODE, }; diff --git a/src/backend/wayland/handlers/tablet/seat.rs b/src/backend/wayland/handlers/tablet/seat.rs index 182381eae..130dade45 100644 --- a/src/backend/wayland/handlers/tablet/seat.rs +++ b/src/backend/wayland/handlers/tablet/seat.rs @@ -1,10 +1,11 @@ use log::{info, warn}; use std::sync::Arc; +use wayland_client::backend::ObjectData; use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; -use wayland_protocols::wp::tablet::zv2::client::{ - zwp_tablet_pad_v2::ZwpTabletPadV2, zwp_tablet_seat_v2::ZwpTabletSeatV2, - zwp_tablet_tool_v2::ZwpTabletToolV2, zwp_tablet_v2::ZwpTabletV2, -}; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_v2::ZwpTabletPadV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_seat_v2::ZwpTabletSeatV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::ZwpTabletToolV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_v2::ZwpTabletV2; use super::IgnoredObjectData; use crate::backend::wayland::state::WaylandState; @@ -47,10 +48,7 @@ impl Dispatch for WaylandState { } } - fn event_created_child( - opcode: u16, - qhandle: &QueueHandle, - ) -> Arc { + fn event_created_child(opcode: u16, qhandle: &QueueHandle) -> Arc { use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_seat_v2::{ EVT_PAD_ADDED_OPCODE, EVT_TABLET_ADDED_OPCODE, EVT_TOOL_ADDED_OPCODE, }; diff --git a/src/backend/wayland/handlers/tablet/tool.rs b/src/backend/wayland/handlers/tablet/tool.rs index 1b910f1b5..5cdc3dfd2 100644 --- a/src/backend/wayland/handlers/tablet/tool.rs +++ b/src/backend/wayland/handlers/tablet/tool.rs @@ -1,17 +1,18 @@ use log::{debug, info}; +use wayland_client::protocol::wl_surface::WlSurface; use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::ZwpTabletToolV2; use crate::backend::wayland::toolbar_intent::intent_to_event; -use crate::{ - input::{DrawingState, EraserMode, Tool}, - util::Rect, -}; +use crate::input::{DrawingState, EraserMode, Tool}; +use crate::util::Rect; -use crate::backend::wayland::{ - handlers::route::InputSurface, - state::{ContactOwner, RegionReviewPress, ReleaseRoute, StylusMotionRoute, WaylandState}, +use crate::backend::wayland::TabletToolType; +use crate::backend::wayland::handlers::route::InputSurface; +use crate::backend::wayland::state::{ + ContactOwner, RegionReviewPress, ReleaseRoute, StylusMotionRoute, WaylandState, }; +use crate::input::InputState; use crate::input::state::RegionInputSource; const STYLUS_CURSOR_DAMAGE_RADIUS: i32 = 64; @@ -33,11 +34,7 @@ const STYLUS_CURSOR_DAMAGE_RADIUS: i32 = 64; /// of the tip and motion guards: a stroke begun while a capture is still pending /// is a real stroke and keeps its pressure. The retired term is what stops that /// allowance from also readmitting the contact the modal already took over. -fn drop_stylus_pressure( - on_overlay: bool, - contact_retired: bool, - input_state: &crate::input::InputState, -) -> bool { +fn drop_stylus_pressure(on_overlay: bool, contact_retired: bool, input_state: &InputState) -> bool { !on_overlay || contact_retired || input_state.screen_modal_is_active() } impl WaylandState { @@ -123,11 +120,7 @@ fn stylus_cursor_damage_rect(pos: (f64, f64), width: i32, height: i32) -> Option } impl WaylandState { - fn handle_stylus_proximity_in( - &mut self, - proxy: &ZwpTabletToolV2, - surface: wayland_client::protocol::wl_surface::WlSurface, - ) { + fn handle_stylus_proximity_in(&mut self, proxy: &ZwpTabletToolV2, surface: WlSurface) { let tool_id = proxy.id(); let tool_type = self.tablet.tool_types.get(&tool_id).copied(); debug!( @@ -158,10 +151,7 @@ impl WaylandState { } } - fn auto_switch_physical_eraser( - &mut self, - tool_type: Option, - ) { + fn auto_switch_physical_eraser(&mut self, tool_type: Option) { if !self.config.tablet.auto_eraser_switch || !tool_type.is_some_and(|tool_type| tool_type.is_eraser()) || self.input_state.active_tool() == Tool::Eraser diff --git a/src/backend/wayland/handlers/tablet/tool/tests.rs b/src/backend/wayland/handlers/tablet/tool/tests.rs index 376e489dd..57538a612 100644 --- a/src/backend/wayland/handlers/tablet/tool/tests.rs +++ b/src/backend/wayland/handlers/tablet/tool/tests.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::*; /// The modal term stops exactly where the tip and motion guards stop: once @@ -19,11 +21,7 @@ fn stylus_pressure_stops_at_the_selector_not_at_the_request() { !fresh_contact(&state), "a stroke drawn while the capture is pending is still a real stroke" ); - state.activate_region_with( - &crate::draw::TextMeasurer::default(), - RegionPurposeTag::Ocr, - 1, - ); + state.activate_region_with(&TextMeasurer::default(), RegionPurposeTag::Ocr, 1); assert!(fresh_contact(&state)); state.start_region_selection(RegionInputSource::Stylus, (10.0, 10.0)); assert!(fresh_contact(&state)); @@ -32,7 +30,7 @@ fn stylus_pressure_stops_at_the_selector_not_at_the_request() { state.set_eyedropper_pending_capture(EyedropperCaptureSource::Frozen); assert!(!fresh_contact(&state)); - state.activate_eyedropper_with(&crate::draw::TextMeasurer::default(), Some(1)); + state.activate_eyedropper_with(&TextMeasurer::default(), Some(1)); assert!(fresh_contact(&state)); state.cancel_eyedropper(); assert!(!fresh_contact(&state)); diff --git a/src/backend/wayland/handlers/test_support.rs b/src/backend/wayland/handlers/test_support.rs index 111569c83..0c970ea05 100644 --- a/src/backend/wayland/handlers/test_support.rs +++ b/src/backend/wayland/handlers/test_support.rs @@ -1,29 +1,43 @@ //! Private protocol peer for exercising real handlers without a desktop connection. use std::io::{Read, Write}; use std::os::unix::net::UnixStream; +use std::thread; +use std::time::Duration; -use smithay_client_toolkit::{ - compositor::CompositorState, - output::OutputState, - registry::RegistryState, - seat::{ - SeatState, pointer_constraints::PointerConstraintsState, - relative_pointer::RelativePointerState, - }, - shell::xdg::{XdgShell, XdgSurface, window::WindowDecorations}, - shm::Shm, -}; -use wayland_client::{ - Connection, EventQueue, Proxy, globals::registry_queue_init, protocol::wl_output, -}; +use smithay_client_toolkit::compositor::CompositorState; +use smithay_client_toolkit::output::OutputState; +use smithay_client_toolkit::registry::RegistryState; +use smithay_client_toolkit::seat::SeatState; +use smithay_client_toolkit::seat::pointer_constraints::PointerConstraintsState; +use smithay_client_toolkit::seat::relative_pointer::RelativePointerState; +use smithay_client_toolkit::shell::xdg::window::WindowDecorations; +use smithay_client_toolkit::shell::xdg::{XdgShell, XdgSurface}; +use smithay_client_toolkit::shm::Shm; +use tokio::runtime::{Builder, Runtime}; +use wayland_client::globals::registry_queue_init; +use wayland_client::protocol::wl_output; +use wayland_client::{Connection, EventQueue, Proxy}; use wayland_protocols::ext::image_copy_capture::v1::client::ext_image_copy_capture_manager_v1::ExtImageCopyCaptureManagerV1; use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1; -use crate::backend::wayland::{ - backend::runtime_wake::RuntimeWakeSource, - session::{HomeSession, PersistenceController, SessionHome, SessionLaunch, session_target}, - state::{ProtocolGlobals, ProtocolGlobalsSeed, WaylandState, WaylandStateInit}, +use crate::backend::ExitAfterCaptureMode; +use crate::backend::wayland::backend::runtime_wake::RuntimeWakeSource; +use crate::backend::wayland::frozen::ExtImageCopyManagers; +use crate::backend::wayland::session::{ + HomeSession, PersistenceController, SessionHome, SessionLaunch, session_target, }; +use crate::backend::wayland::state::{ + ProtocolGlobals, ProtocolGlobalsSeed, WaylandState, WaylandStateInit, +}; +use crate::capture::CaptureManager; +use crate::config::Config; +use crate::draw::TextMeasurer; +use crate::input::InputState; +use crate::onboarding::OnboardingStore; +use crate::palette_recents::{PaletteRecentsStore, PaletteRecentsWriter}; +use crate::test_temp; +use crate::test_temp::TempDir; +use crate::ui_text::UiTextEngine; pub(in crate::backend::wayland) struct HandlerFixture { pub state: WaylandState, @@ -32,8 +46,8 @@ pub(in crate::backend::wayland) struct HandlerFixture { peer: UnixStream, direct_capture_manager: Option, _wake: RuntimeWakeSource, - _runtime: tokio::runtime::Runtime, - _data: crate::test_temp::TempDir, + _runtime: Runtime, + _data: TempDir, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -45,30 +59,22 @@ pub(in crate::backend::wayland) enum CaptureFixtureBackend { } impl HandlerFixture { - pub fn new(config: crate::config::Config) -> Self { + pub fn new(config: Config) -> Self { Self::new_inner(config, 0, CaptureFixtureBackend::Unavailable) } - pub fn with_outputs(config: crate::config::Config, output_count: u32) -> Self { + pub fn with_outputs(config: Config, output_count: u32) -> Self { Self::new_inner(config, output_count, CaptureFixtureBackend::Unavailable) } - pub fn with_capture_output( - config: crate::config::Config, - backend: CaptureFixtureBackend, - ) -> Self { + pub fn with_capture_output(config: Config, backend: CaptureFixtureBackend) -> Self { Self::new_inner(config, 1, backend) } - fn new_inner( - config: crate::config::Config, - output_count: u32, - backend: CaptureFixtureBackend, - ) -> Self { + fn new_inner(config: Config, output_count: u32, backend: CaptureFixtureBackend) -> Self { let (client, mut peer) = UnixStream::pair().unwrap(); - peer.set_read_timeout(Some(std::time::Duration::from_secs(5))) - .unwrap(); - let bootstrap = std::thread::spawn(move || { + peer.set_read_timeout(Some(Duration::from_secs(5))).unwrap(); + let bootstrap = thread::spawn(move || { loop { let request = read_request(&mut peer); assert_eq!(request.object, 1); @@ -125,19 +131,14 @@ impl HandlerFixture { output: OutputState::new(&globals, &qh), seat: SeatState::new(&globals, &qh), }); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - let wake = RuntimeWakeSource::new().unwrap(); + let runtime = Builder::new_current_thread().enable_all().build().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); // Private store files keep tests away from the developer's own data // folder without changing the environment other tests read. - let data = crate::test_temp::tempdir().unwrap(); - let onboarding = - crate::onboarding::OnboardingStore::load_from_path(data.path().join("onboarding.toml")); - let palette_recents = crate::palette_recents::PaletteRecentsStore::load_from_path( - data.path().join("palette_recents.toml"), - ); + let data = test_temp::tempdir().unwrap(); + let onboarding = OnboardingStore::load_from_path(data.path().join("onboarding.toml")); + let palette_recents = + PaletteRecentsStore::load_from_path(data.path().join("palette_recents.toml")); let screencopy_manager: Option = (backend == CaptureFixtureBackend::WlrScreencopy) @@ -156,21 +157,21 @@ impl HandlerFixture { let ext_image_copy_managers = ext_capture_manager.map(|capture| { let source = globals.bind(&qh, 1..=1, ()).unwrap(); - crate::backend::wayland::frozen::ExtImageCopyManagers::new(capture, source) + ExtImageCopyManagers::new(capture, source) }); // Placement follows the configured preference, as at startup without env overrides. let xdg_fullscreen = config.ui.xdg_fullscreen; let mut state = WaylandState::new(WaylandStateInit { - ui_text: crate::ui_text::UiTextEngine::default(), - text_measurer: crate::draw::TextMeasurer::default(), + ui_text: UiTextEngine::default(), + text_measurer: TextMeasurer::default(), globals: protocol, - input_state: crate::input::InputState::from_config(&config), + input_state: InputState::from_config(&config), config, startup_activation_token: None, onboarding, - palette_recents: crate::palette_recents::PaletteRecentsWriter::new(palette_recents), - capture_manager: crate::capture::CaptureManager::new(runtime.handle()), + palette_recents: PaletteRecentsWriter::new(palette_recents), + capture_manager: CaptureManager::new(runtime.handle()), session_options: None, session_home: SessionHome::new( SessionLaunch { @@ -187,7 +188,7 @@ impl HandlerFixture { runtime_ui_unavailable: None, runtime_wake: wake.handle(), tokio_handle: runtime.handle().clone(), - exit_after_capture_mode: crate::backend::ExitAfterCaptureMode::Never, + exit_after_capture_mode: ExitAfterCaptureMode::Never, frozen_enabled: backend != CaptureFixtureBackend::Unavailable, preferred_output_identity: None, xdg_fullscreen, diff --git a/src/backend/wayland/handlers/text_input.rs b/src/backend/wayland/handlers/text_input.rs index 1cd0891de..6aee4a920 100644 --- a/src/backend/wayland/handlers/text_input.rs +++ b/src/backend/wayland/handlers/text_input.rs @@ -18,12 +18,13 @@ //! keyboard driving fcitx5/ibus-style input. use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; -use wayland_protocols::wp::text_input::zv3::client::{ - zwp_text_input_manager_v3::ZwpTextInputManagerV3, - zwp_text_input_v3::{self, ChangeCause, ContentHint, ContentPurpose, ZwpTextInputV3}, +use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; +use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ + self, ChangeCause, ContentHint, ContentPurpose, ZwpTextInputV3, }; use crate::backend::wayland::state::WaylandState; +use crate::draw::TextMeasurer; impl Dispatch for WaylandState { fn event( @@ -198,7 +199,7 @@ impl WaylandState { &self, ti: &ZwpTextInputV3, external_change: bool, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ) -> bool { if external_change { ti.set_text_change_cause(ChangeCause::Other); diff --git a/src/backend/wayland/handlers/touch.rs b/src/backend/wayland/handlers/touch.rs index 85a331259..f534dd322 100644 --- a/src/backend/wayland/handlers/touch.rs +++ b/src/backend/wayland/handlers/touch.rs @@ -1,9 +1,7 @@ use log::debug; use smithay_client_toolkit::seat::touch::TouchHandler; -use wayland_client::{ - Connection, QueueHandle, - protocol::{wl_surface, wl_touch}, -}; +use wayland_client::protocol::{wl_surface, wl_touch}; +use wayland_client::{Connection, QueueHandle}; use crate::backend::wayland::state::{ PerfInputSource, RegionReviewPress, TouchTarget, WaylandState, diff --git a/src/backend/wayland/handlers/xdg.rs b/src/backend/wayland/handlers/xdg.rs index 6c5ccbf13..eb22acaa2 100644 --- a/src/backend/wayland/handlers/xdg.rs +++ b/src/backend/wayland/handlers/xdg.rs @@ -151,10 +151,11 @@ mod tests { use smithay_client_toolkit::compositor::CompositorHandler; use crate::backend::wayland::handlers::test_support::HandlerFixture; + use crate::config::Config; #[test] fn configure_output_guess_yields_to_the_compositor_enter() { - let mut fixture = HandlerFixture::with_outputs(crate::config::Config::default(), 2); + let mut fixture = HandlerFixture::with_outputs(Config::default(), 2); let outputs: Vec<_> = fixture.state.protocol.output().outputs().collect(); for (output, name) in outputs.iter().zip(["DP-1", "DP-2"]) { fixture.complete_output_metadata_for(output.clone(), name); diff --git a/src/backend/wayland/input_monitor/monitor.rs b/src/backend/wayland/input_monitor/monitor.rs index b9ddd80fa..40ec23147 100644 --- a/src/backend/wayland/input_monitor/monitor.rs +++ b/src/backend/wayland/input_monitor/monitor.rs @@ -15,29 +15,13 @@ //! into render-only state; failures are reported as typed `Failed` events so a //! device or keymap problem can never panic across the libinput FFI edge. -use std::collections::{HashMap, HashSet}; -use std::fs::{File, OpenOptions}; use std::io; -use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; -use std::os::unix::fs::OpenOptionsExt; -use std::path::Path; -use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel}; +use std::os::fd::{AsRawFd, OwnedFd}; +use std::sync::mpsc::{Receiver, TryRecvError, channel}; use std::thread::{self, JoinHandle}; use std::time::{Duration, Instant}; -use input::event::device::DeviceEvent; -use input::event::keyboard::{KeyState, KeyboardEventTrait}; -use input::event::pointer::{Axis, ButtonState, PointerScrollEvent}; -use input::event::tablet_tool::{TabletToolEvent, TipState}; -use input::event::{Event, EventTrait, KeyboardEvent, PointerEvent}; -use input::{DeviceCapability, Libinput, LibinputInterface}; -use xkbcommon::xkb; - use super::super::RuntimeWakeHandle; -use super::super::handlers::keyboard::{KEY_REPEAT_INITIAL_DELAY, KEY_REPEAT_INTERVAL}; -use super::translate::{ - EVDEV_KEYCODE_OFFSET, button_label, key_label, modifiers_from_xkb, pen_label, scroll_label, -}; mod worker; @@ -161,7 +145,7 @@ impl InputMonitor { let (sender, events) = channel(); let join = thread::Builder::new() .name("wayscriber-input-monitor".to_string()) - .spawn(move || run(stop_read, sender, wake))?; + .spawn(move || run(&stop_read, &sender, &wake))?; Ok(Self { join: Some(join), stop: stop_write, diff --git a/src/backend/wayland/input_monitor/monitor/worker.rs b/src/backend/wayland/input_monitor/monitor/worker.rs index 0c0a0345a..d15741a0a 100644 --- a/src/backend/wayland/input_monitor/monitor/worker.rs +++ b/src/backend/wayland/input_monitor/monitor/worker.rs @@ -1,8 +1,30 @@ -use super::*; +use super::super::translate::{ + EVDEV_KEYCODE_OFFSET, button_label, key_label, modifiers_from_xkb, pen_label, scroll_label, +}; +use super::{SystemInputEvent, SystemInputFailure}; +use crate::backend::wayland::RuntimeWakeHandle; +#[cfg(test)] +use crate::backend::wayland::handlers::keyboard::KEY_REPEAT_INITIAL_DELAY; +#[cfg(test)] +use crate::backend::wayland::handlers::keyboard::KEY_REPEAT_INTERVAL; +use input::event::device::DeviceEvent; +use input::event::keyboard::{KeyState, KeyboardEventTrait}; +use input::event::pointer::{Axis, ButtonState, PointerScrollEvent}; +use input::event::tablet_tool::{TabletToolEvent, TipState}; +use input::event::{Event, EventTrait, KeyboardEvent, PointerEvent}; +use input::{DeviceCapability, Libinput, LibinputInterface}; +use std::fs::{File, OpenOptions}; +use std::io; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; +use std::os::unix::fs::OpenOptionsExt; +use std::path::Path; +use std::sync::mpsc::Sender; +use std::time::{Duration, Instant}; +use xkbcommon::xkb; mod repeat; -use repeat::*; +use repeat::{KeyboardKeys, RepeatPress, RepeatState}; /// Non-blocking, close-on-exec pipe used only to interrupt the reader's poll. pub(super) fn stop_pipe() -> io::Result<(OwnedFd, OwnedFd)> { @@ -42,11 +64,15 @@ impl LibinputInterface for DirectInputDevices { /// Reader thread body. Every exit path is a typed event or a clean return; a /// panic must never cross the libinput FFI boundary. -pub(super) fn run(stop_read: OwnedFd, sender: Sender, wake: RuntimeWakeHandle) { +pub(super) fn run( + stop_read: &OwnedFd, + sender: &Sender, + wake: &RuntimeWakeHandle, +) { let seat = super::super::probe::current_seat(); let mut libinput = Libinput::new_with_udev(DirectInputDevices); if libinput.udev_assign_seat(&seat).is_err() { - report_failure(&sender, &wake, SystemInputFailure::SeatUnavailable { seat }); + report_failure(sender, wake, SystemInputFailure::SeatUnavailable { seat }); return; } // `udev_assign_seat` succeeds for a seat with no devices, and libinput @@ -71,14 +97,14 @@ pub(super) fn run(stop_read: OwnedFd, sender: Sender, wake: Ru SystemInputFailure::Unavailable { seat } } }; - report_failure(&sender, &wake, failure); + report_failure(sender, wake, failure); return; } Ok(_) => {} Err(err) => { report_failure( - &sender, - &wake, + sender, + wake, SystemInputFailure::ReadFailed(format!("libinput dispatch failed: {err}")), ); return; @@ -92,7 +118,7 @@ pub(super) fn run(stop_read: OwnedFd, sender: Sender, wake: Ru let Some(keymap) = xkb::Keymap::new_from_names(&context, "", "", "", "", None, xkb::KEYMAP_COMPILE_NO_FLAGS) else { - report_failure(&sender, &wake, SystemInputFailure::KeymapUnavailable); + report_failure(sender, wake, SystemInputFailure::KeymapUnavailable); return; }; let mut xkb_state = xkb::State::new(&keymap); @@ -110,7 +136,7 @@ pub(super) fn run(stop_read: OwnedFd, sender: Sender, wake: Ru } loop { - match wait_for_input(&libinput, &stop_read, repeat.timeout(Instant::now())) { + match wait_for_input(&libinput, stop_read, repeat.timeout(Instant::now())) { Ok(PollOutcome::Stop) => return, Ok(PollOutcome::Ready) => {} Ok(PollOutcome::Timeout) => { @@ -128,8 +154,8 @@ pub(super) fn run(stop_read: OwnedFd, sender: Sender, wake: Ru } Err(err) => { report_failure( - &sender, - &wake, + sender, + wake, SystemInputFailure::ReadFailed(format!("input poll failed: {err}")), ); return; @@ -138,8 +164,8 @@ pub(super) fn run(stop_read: OwnedFd, sender: Sender, wake: Ru if let Err(err) = libinput.dispatch() { report_failure( - &sender, - &wake, + sender, + wake, SystemInputFailure::ReadFailed(format!("libinput dispatch failed: {err}")), ); return; @@ -305,7 +331,7 @@ fn translate_event( } else { RepeatPress::NonRepeating }; - repeat.on_press(raw_keycode, &device, press, Instant::now()); + repeat.on_press(raw_keycode, &device, &press, Instant::now()); let (label, bare_modifier) = label_pair?; Some(SystemInputEvent::Key { label, diff --git a/src/backend/wayland/input_monitor/monitor/worker/repeat.rs b/src/backend/wayland/input_monitor/monitor/worker/repeat.rs index 77c4e0ffc..f4e935813 100644 --- a/src/backend/wayland/input_monitor/monitor/worker/repeat.rs +++ b/src/backend/wayland/input_monitor/monitor/worker/repeat.rs @@ -1,4 +1,9 @@ -use super::*; +use super::super::SystemInputEvent; +use crate::backend::wayland::handlers::keyboard::{KEY_REPEAT_INITIAL_DELAY, KEY_REPEAT_INTERVAL}; +use crate::backend::wayland::input_monitor::translate::EVDEV_KEYCODE_OFFSET; +use std::collections::{HashMap, HashSet}; +use std::time::{Duration, Instant}; +use xkbcommon::xkb; pub(super) struct RepeatState { held: Option, @@ -88,12 +93,12 @@ impl KeyboardKeys { /// What a press means for the repeat timer: the keymap's repeat flag, plus /// the chip label when the HUD has one. -pub(super) enum RepeatPress<'a> { +pub(super) enum RepeatPress<'label> { /// The keymap marks this key as non-repeating (modifiers, and anything /// else the layout excludes from typematic). NonRepeating, /// The keymap repeats this key; `None` when the HUD has no label for it. - Repeating(Option<&'a str>), + Repeating(Option<&'label str>), } impl RepeatState { @@ -115,10 +120,10 @@ impl RepeatState { &mut self, keycode: u32, device: &str, - press: RepeatPress<'_>, + press: &RepeatPress<'_>, now: Instant, ) { - match press { + match *press { RepeatPress::NonRepeating => {} RepeatPress::Repeating(label) => { self.held = label.map(|label| HeldKey { diff --git a/src/backend/wayland/input_monitor/monitor/worker/tests.rs b/src/backend/wayland/input_monitor/monitor/worker/tests.rs index fd7652604..7c0c3dfe7 100644 --- a/src/backend/wayland/input_monitor/monitor/worker/tests.rs +++ b/src/backend/wayland/input_monitor/monitor/worker/tests.rs @@ -4,12 +4,12 @@ const KEYBOARD: &str = "event0"; /// A keymap-repeating press, labeled as the HUD would label it. fn press(repeat: &mut RepeatState, keycode: u32, label: Option<&str>, now: Instant) { - repeat.on_press(keycode, KEYBOARD, RepeatPress::Repeating(label), now); + repeat.on_press(keycode, KEYBOARD, &RepeatPress::Repeating(label), now); } /// A press the keymap marks as non-repeating (modifiers and friends). fn press_non_repeating(repeat: &mut RepeatState, keycode: u32, now: Instant) { - repeat.on_press(keycode, KEYBOARD, RepeatPress::NonRepeating, now); + repeat.on_press(keycode, KEYBOARD, &RepeatPress::NonRepeating, now); } #[test] diff --git a/src/backend/wayland/input_monitor/probe.rs b/src/backend/wayland/input_monitor/probe.rs index b281b363f..e5314ea88 100644 --- a/src/backend/wayland/input_monitor/probe.rs +++ b/src/backend/wayland/input_monitor/probe.rs @@ -9,6 +9,11 @@ use std::ffi::CString; use std::os::unix::ffi::OsStrExt; use std::path::Path; +use crate::env_vars; +use std::ffi::OsString; +use std::io::ErrorKind; +use std::{env, fs, io}; + const DEV_INPUT_DIR: &str = "/dev/input"; /// Seat to follow when the session does not name one. libinput's udev backend /// groups devices by seat and `seat0` is the single-seat default. @@ -23,7 +28,7 @@ const DEFAULT_SEAT: &str = "seat0"; /// callers need it, so it is dead code without the feature. #[cfg_attr(not(feature = "input-monitor"), allow(dead_code))] pub(in crate::backend::wayland) fn current_seat() -> String { - seat_name(std::env::var_os(crate::env_vars::XDG_SEAT_ENV)) + seat_name(env::var_os(env_vars::XDG_SEAT_ENV)) } /// Resolve a seat name from the environment value, falling back to the @@ -31,7 +36,7 @@ pub(in crate::backend::wayland) fn current_seat() -> String { /// seat (or hold no devices at all), so the session's value wins when it is /// set to something usable. #[cfg_attr(not(feature = "input-monitor"), allow(dead_code))] -fn seat_name(configured: Option) -> String { +fn seat_name(configured: Option) -> String { configured .and_then(|seat| seat.into_string().ok()) .map(|seat| seat.trim().to_string()) @@ -127,15 +132,15 @@ pub(in crate::backend::wayland) fn event_node_access() -> EventNodeAccess { /// Flat scan of `/dev/input`, used only when udev cannot attribute nodes to a /// seat. Cannot distinguish seats, so it is never the primary classifier. fn classify_event_nodes(dir: &Path) -> EventNodeAccess { - let entries = match std::fs::read_dir(dir) { + let entries = match fs::read_dir(dir) { Ok(entries) => entries, // A directory this process may not list is itself the permission // case; reporting it as "no devices" would send the user looking for // hardware instead of at their group membership. - Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => { + Err(err) if err.kind() == ErrorKind::PermissionDenied => { return EventNodeAccess::Unreadable; } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return EventNodeAccess::None, + Err(err) if err.kind() == ErrorKind::NotFound => return EventNodeAccess::None, Err(_) => return EventNodeAccess::Unknown, }; let mut access = EventNodeAccess::None; @@ -217,7 +222,7 @@ fn node_path_access(path: &Path) -> NodePathAccess { if unsafe { libc::access(c_path.as_ptr(), libc::R_OK) } == 0 { return NodePathAccess::Readable; } - match std::io::Error::last_os_error().raw_os_error() { + match io::Error::last_os_error().raw_os_error() { Some(libc::EACCES | libc::EPERM) => NodePathAccess::PermissionDenied, Some(libc::ENOENT | libc::ENOTDIR | libc::ENODEV) => NodePathAccess::Absent, _ => NodePathAccess::Unknown, @@ -226,6 +231,10 @@ fn node_path_access(path: &Path) -> NodePathAccess { #[cfg(test)] mod tests { + use std::fs::Permissions; + + use crate::test_temp; + use super::*; #[test] @@ -240,11 +249,11 @@ mod tests { /// so a seat with only legacy nodes reads as unavailable. #[test] fn only_event_nodes_count_as_capture_devices() { - let temp = crate::test_temp::tempdir().expect("tempdir should succeed"); - std::fs::write(temp.path().join("mice"), b"").expect("fixture node"); + let temp = test_temp::tempdir().expect("tempdir should succeed"); + fs::write(temp.path().join("mice"), b"").expect("fixture node"); assert_eq!(classify_event_nodes(temp.path()), EventNodeAccess::None); - std::fs::write(temp.path().join("event0"), b"").expect("fixture node"); + fs::write(temp.path().join("event0"), b"").expect("fixture node"); assert_eq!(classify_event_nodes(temp.path()), EventNodeAccess::Readable); } @@ -252,23 +261,22 @@ mod tests { /// different fixes, so the classifier keeps them apart. #[test] fn node_access_separates_an_empty_directory_from_an_unreadable_one() { - let temp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let temp = test_temp::tempdir().expect("tempdir should succeed"); assert_eq!(classify_event_nodes(temp.path()), EventNodeAccess::None); // Only non-event nodes: still nothing to capture from. - std::fs::write(temp.path().join("mice"), b"").expect("fixture node"); + fs::write(temp.path().join("mice"), b"").expect("fixture node"); assert_eq!(classify_event_nodes(temp.path()), EventNodeAccess::None); let node = temp.path().join("event0"); - std::fs::write(&node, b"").expect("fixture node"); + fs::write(&node, b"").expect("fixture node"); assert_eq!(classify_event_nodes(temp.path()), EventNodeAccess::Readable); // A node this process cannot read reports the permission case. Running // as root defeats the mode bits, so skip there rather than assert a // condition the environment cannot produce. use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&node, std::fs::Permissions::from_mode(0o000)) - .expect("fixture permissions"); + fs::set_permissions(&node, Permissions::from_mode(0o000)).expect("fixture permissions"); if node_path_access(&node) == NodePathAccess::Readable { eprintln!("running with privileges that bypass file permissions; skipping"); return; @@ -290,27 +298,23 @@ mod tests { fn an_unlistable_directory_reports_the_permission_case() { use std::os::unix::fs::PermissionsExt; - let temp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let temp = test_temp::tempdir().expect("tempdir should succeed"); let dir = temp.path().join("input"); - std::fs::create_dir(&dir).expect("fixture directory"); - std::fs::write(dir.join("event0"), b"").expect("fixture node"); + fs::create_dir(&dir).expect("fixture directory"); + fs::write(dir.join("event0"), b"").expect("fixture node"); - std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o000)) - .expect("fixture permissions"); - let listable = std::fs::read_dir(&dir).is_ok(); + fs::set_permissions(&dir, Permissions::from_mode(0o000)).expect("fixture permissions"); + let listable = fs::read_dir(&dir).is_ok(); // Restore before any assertion so the tempdir can always be cleaned up. - std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)) - .expect("restore permissions"); + fs::set_permissions(&dir, Permissions::from_mode(0o755)).expect("restore permissions"); if listable { eprintln!("running with privileges that bypass directory permissions; skipping"); return; } - std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o000)) - .expect("fixture permissions"); + fs::set_permissions(&dir, Permissions::from_mode(0o000)).expect("fixture permissions"); let access = classify_event_nodes(&dir); - std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)) - .expect("restore permissions"); + fs::set_permissions(&dir, Permissions::from_mode(0o755)).expect("restore permissions"); assert_eq!(access, EventNodeAccess::Unreadable); } diff --git a/src/backend/wayland/input_monitor/translate.rs b/src/backend/wayland/input_monitor/translate.rs index 22ea7b535..10289af29 100644 --- a/src/backend/wayland/input_monitor/translate.rs +++ b/src/backend/wayland/input_monitor/translate.rs @@ -5,6 +5,8 @@ use xkbcommon::xkb; +use crate::input::state; + use super::super::handlers::keyboard::keysym_to_key; use crate::i18n::{tr, trf}; use crate::input::Modifiers; @@ -36,7 +38,7 @@ pub(super) fn modifiers_from_xkb(state: &xkb::State) -> Modifiers { pub(super) fn key_label(keysym: xkb::Keysym, modifiers: Modifiers) -> Option<(String, bool)> { let key = keysym_to_key(keysym); let label = input_hud_key_label(key, modifiers)?; - Some((label, crate::input::state::is_bare_modifier(key))) + Some((label, state::is_bare_modifier(key))) } /// Chip label for a pointer button press. diff --git a/src/backend/wayland/portal_capture.rs b/src/backend/wayland/portal_capture.rs index 8b6952a8e..87fb21120 100644 --- a/src/backend/wayland/portal_capture.rs +++ b/src/backend/wayland/portal_capture.rs @@ -1,25 +1,27 @@ #[cfg(feature = "portal")] use std::time::Duration; +use tokio::runtime::Runtime; +#[cfg(feature = "portal")] +use tokio::time; + +#[cfg(feature = "portal")] +use crate::capture::portal; +use crate::capture::types::CaptureError; + #[cfg(feature = "portal")] const PORTAL_STARTUP_PROBE_TIMEOUT: Duration = Duration::from_millis(500); /// Whether freeze may fall back to the screenshot portal. #[cfg(feature = "portal")] -pub(crate) fn portal_freeze_fallback( - runtime: &tokio::runtime::Runtime, - direct_capture_supported: bool, -) -> bool { +pub(crate) fn portal_freeze_fallback(runtime: &Runtime, direct_capture_supported: bool) -> bool { portal_fallback_available(direct_capture_supported, || { screenshot_portal_available(runtime) }) } #[cfg(not(feature = "portal"))] -pub(crate) fn portal_freeze_fallback( - _runtime: &tokio::runtime::Runtime, - _direct_capture_supported: bool, -) -> bool { +pub(crate) fn portal_freeze_fallback(_runtime: &Runtime, _direct_capture_supported: bool) -> bool { false } @@ -33,20 +35,16 @@ fn portal_fallback_available(direct_capture_supported: bool, probe: impl FnOnce( } #[cfg(feature = "portal")] -fn screenshot_portal_available(runtime: &tokio::runtime::Runtime) -> bool { +fn screenshot_portal_available(runtime: &Runtime) -> bool { runtime.block_on(async { - tokio::time::timeout( - PORTAL_STARTUP_PROBE_TIMEOUT, - crate::capture::portal::is_portal_available(), - ) - .await - .unwrap_or(false) + time::timeout(PORTAL_STARTUP_PROBE_TIMEOUT, portal::is_portal_available()) + .await + .unwrap_or(false) }) } #[cfg(feature = "portal")] -pub(crate) async fn capture_via_portal_fullscreen_bytes() --> Result, crate::capture::types::CaptureError> { +pub(crate) async fn capture_via_portal_fullscreen_bytes() -> Result, CaptureError> { use crate::capture::sources::portal::capture_via_portal_bytes; use crate::capture::types::CaptureType; @@ -54,9 +52,8 @@ pub(crate) async fn capture_via_portal_fullscreen_bytes() } #[cfg(not(feature = "portal"))] -pub(crate) async fn capture_via_portal_fullscreen_bytes() --> Result, crate::capture::types::CaptureError> { - Err(crate::capture::types::CaptureError::PortalUnavailable) +pub(crate) async fn capture_via_portal_fullscreen_bytes() -> Result, CaptureError> { + Err(CaptureError::PortalUnavailable) } pub(crate) const fn portal_output_matches(target: Option, current: Option) -> bool { diff --git a/src/backend/wayland/portal_raster.rs b/src/backend/wayland/portal_raster.rs index 172e208cd..6c0c30ed6 100644 --- a/src/backend/wayland/portal_raster.rs +++ b/src/backend/wayland/portal_raster.rs @@ -108,7 +108,7 @@ impl LogicalDesktop { } pub(super) fn crop_portal_raster( - data: Vec, + data: &[u8], width: u32, height: u32, geometry: &OutputGeometry, @@ -172,16 +172,8 @@ pub(super) fn crop_portal_raster( return Err(error("Portal capture does not contain the active output")); } - let (_, _, data) = crop_argb( - &data, - width, - height, - crop.x, - crop.y, - crop.width, - crop.height, - ) - .ok_or_else(|| error("Portal capture crop is invalid"))?; + let (_, _, data) = crop_argb(data, width, height, crop.x, crop.y, crop.width, crop.height) + .ok_or_else(|| error("Portal capture crop is invalid"))?; resample(data, (crop.width, crop.height), target) } @@ -276,6 +268,8 @@ fn resample( #[cfg(test)] mod tests { + use crate::capture::DesktopBackdropGeometry; + use super::*; use wayland_client::protocol::wl_output; @@ -308,9 +302,7 @@ mod tests { Some((active.physical_width, active.physical_height)), ) .unwrap() - .with_desktop_backdrop_geometry(crate::capture::DesktopBackdropGeometry::from_outputs( - active, outputs, - )) + .with_desktop_backdrop_geometry(DesktopBackdropGeometry::from_outputs(active, outputs)) .with_portal_outputs(Some(outputs.to_vec())) .with_known_output_count(Some(outputs.len() as u32)) } @@ -341,7 +333,7 @@ mod tests { } } - let image = crop_portal_raster(data, 16, 20, &geo).unwrap(); + let image = crop_portal_raster(&data, 16, 20, &geo).unwrap(); assert_eq!(image.data, 0xff123456_u32.to_ne_bytes().repeat(4 * 8)); } @@ -364,7 +356,7 @@ mod tests { } } - let image = crop_portal_raster(data, 18, 8, &geo).unwrap(); + let image = crop_portal_raster(&data, 18, 8, &geo).unwrap(); assert_eq!((image.width, image.height), (2, 6)); assert_eq!(image.data, expected); @@ -387,7 +379,7 @@ mod tests { } } - let image = crop_portal_raster(pixels, 12, 4, &geo).unwrap(); + let image = crop_portal_raster(&pixels, 12, 4, &geo).unwrap(); assert_eq!((image.width, image.height, image.stride), (8, 4, 32)); assert_eq!(image.data, expected); @@ -411,7 +403,7 @@ mod tests { assert!( crop_portal_raster( - pixels.clone(), + &pixels, 2, 1, &geometry.clone().with_known_output_count(Some(2)) @@ -419,13 +411,8 @@ mod tests { .is_err() ); - let image = crop_portal_raster( - pixels.clone(), - 2, - 1, - &geometry.with_known_output_count(Some(1)), - ) - .unwrap(); + let image = + crop_portal_raster(&pixels, 2, 1, &geometry.with_known_output_count(Some(1))).unwrap(); assert_eq!(image.data, pixels); } @@ -462,7 +449,7 @@ mod tests { let outputs = [output(0, 0, (8, 4), (16, 8)), output(2, 1, (4, 2), (4, 2))]; let geo = geometry(outputs[1], &outputs); assert_eq!(geo.screenshot_size, Some((16, 8))); - assert!(crop_portal_raster(vec![0; 16 * 8 * 4], 16, 8, &geo).is_err()); + assert!(crop_portal_raster(&vec![0; 16 * 8 * 4], 16, 8, &geo).is_err()); } #[test] @@ -484,8 +471,8 @@ mod tests { output(1280, 0, (640, 360), (1280, 720)), ]; let geo = geometry(outputs[0], &outputs); - assert!(crop_portal_raster(vec![0; 16], 3840, 1440, &geo).is_err()); - assert!(crop_portal_raster(vec![0; 1920 * 720 * 4], 1920, 720, &geo).is_err()); + assert!(crop_portal_raster(&[0; 16], 3840, 1440, &geo).is_err()); + assert!(crop_portal_raster(&vec![0; 1920 * 720 * 4], 1920, 720, &geo).is_err()); assert!( LogicalDesktop::from_outputs(&[ output(i32::MIN, 0, (1, 1), (1, 1)), diff --git a/src/backend/wayland/portal_task.rs b/src/backend/wayland/portal_task.rs index 738fea3a3..60349965c 100644 --- a/src/backend/wayland/portal_task.rs +++ b/src/backend/wayland/portal_task.rs @@ -1,11 +1,14 @@ //! Owned capacity-one task used by frozen and zoom portal fallbacks. use std::future::Future; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, TrySendError}; +use std::sync::{Arc, mpsc}; use std::time::{Duration, Instant}; +use tokio::runtime::Handle; +use tokio::task::{JoinHandle, JoinSet}; + use super::RuntimeWakeHandle; pub(super) const PORTAL_CAPTURE_TIMEOUT: Duration = Duration::from_secs(10); @@ -60,7 +63,7 @@ impl PortalOperation { pub(super) struct PortalTask { receiver: Receiver>, - worker: Option>, + worker: Option>, expected_cancel: Arc, started_at: Instant, } @@ -70,7 +73,7 @@ where T: Send + 'static, { pub(super) fn spawn( - runtime: &tokio::runtime::Handle, + runtime: &Handle, runtime_wake: RuntimeWakeHandle, future: impl Future + Send + 'static, ) -> Self { @@ -78,17 +81,17 @@ where } fn spawn_at( - runtime: &tokio::runtime::Handle, + runtime: &Handle, runtime_wake: RuntimeWakeHandle, started_at: Instant, future: impl Future + Send + 'static, ) -> Self { - let (sender, receiver) = std::sync::mpsc::sync_channel(1); + let (sender, receiver) = mpsc::sync_channel(1); let expected_cancel = Arc::new(AtomicBool::new(false)); let task_cancel = Arc::clone(&expected_cancel); let worker = runtime.spawn(async move { let guard = PortalTaskExitGuard::new(sender, runtime_wake, task_cancel); - let mut tasks = tokio::task::JoinSet::new(); + let mut tasks = JoinSet::new(); tasks.spawn(future); let message = match tasks.join_next().await { Some(Ok(value)) => PortalMessage::Ready(value), @@ -107,7 +110,7 @@ where #[cfg(test)] pub(super) fn spawn_at_for_test( - runtime: &tokio::runtime::Handle, + runtime: &Handle, runtime_wake: RuntimeWakeHandle, started_at: Instant, future: impl Future + Send + 'static, @@ -117,7 +120,7 @@ where #[cfg(test)] pub(super) fn disconnected_for_test(started_at: Instant) -> Self { - let (sender, receiver) = std::sync::mpsc::sync_channel(1); + let (sender, receiver) = mpsc::sync_channel(1); drop(sender); Self { receiver, @@ -213,8 +216,11 @@ impl Drop for PortalTaskExitGuard { #[cfg(test)] mod tests { + use std::future; use std::os::fd::AsRawFd; + use tokio::task; + use super::*; use crate::backend::wayland::RuntimeWakeSource; @@ -230,22 +236,18 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn publishes_before_waking() { - let wake = RuntimeWakeSource::new().unwrap(); - let mut task = - PortalTask::spawn(&tokio::runtime::Handle::current(), wake.handle(), async { - 17 - }); + let wake = RuntimeWakeSource::try_new().unwrap(); + let mut task = PortalTask::spawn(&Handle::current(), wake.handle(), async { 17 }); wait_for_wake(&wake); assert!(matches!(task.poll(), PortalPoll::Ready(17))); } #[tokio::test(flavor = "multi_thread")] async fn panic_is_an_explicit_failure_before_wake() { - let wake = RuntimeWakeSource::new().unwrap(); - let mut task = - PortalTask::::spawn(&tokio::runtime::Handle::current(), wake.handle(), async { - panic!("expected portal panic") - }); + let wake = RuntimeWakeSource::try_new().unwrap(); + let mut task = PortalTask::::spawn(&Handle::current(), wake.handle(), async { + panic!("expected portal panic") + }); wait_for_wake(&wake); assert!(matches!( task.poll(), @@ -255,35 +257,32 @@ mod tests { #[tokio::test] async fn expected_cancel_aborts_without_waking_failure() { - let wake = RuntimeWakeSource::new().unwrap(); - let task = PortalTask::spawn( - &tokio::runtime::Handle::current(), - wake.handle(), - async move { - std::future::pending::<()>().await; - 1 - }, - ); + let wake = RuntimeWakeSource::try_new().unwrap(); + let task = PortalTask::spawn(&Handle::current(), wake.handle(), async move { + future::pending::<()>().await; + 1 + }); drop(task); - tokio::task::yield_now().await; + task::yield_now().await; let mut pollfd = libc::pollfd { fd: wake.poll_fd().as_raw_fd(), events: libc::POLLIN, revents: 0, }; // SAFETY: the descriptor and pollfd are valid for this non-blocking poll. - assert_eq!(unsafe { libc::poll(&mut pollfd, 1, 0) }, 0); + let ready = unsafe { libc::poll(&mut pollfd, 1, 0) }; + assert_eq!(ready, 0); } #[tokio::test] async fn deadline_uses_injected_start_instant() { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let start = Instant::now(); let task = PortalTask::spawn_at( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), start, - std::future::pending::<()>(), + future::pending::<()>(), ); assert_eq!(task.timeout(start), PORTAL_CAPTURE_TIMEOUT); assert!(task.timed_out(start + PORTAL_CAPTURE_TIMEOUT)); diff --git a/src/backend/wayland/runtime_operation.rs b/src/backend/wayland/runtime_operation.rs index 1869affd3..cfbf486b9 100644 --- a/src/backend/wayland/runtime_operation.rs +++ b/src/backend/wayland/runtime_operation.rs @@ -1,9 +1,11 @@ //! Identified capacity-one completion transport for event-loop runtime operations. -use std::fmt; +use std::any::Any; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, TrySendError}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, mpsc}; +use std::thread::Builder; +use std::{fmt, io}; use crate::backend::wayland::RuntimeWakeHandle; @@ -157,18 +159,22 @@ where operation: impl FnOnce() -> T + Send + 'static, ) -> Result> { self.try_submit_with_spawner(context, operation, |job| { - std::thread::Builder::new() + Builder::new() .name(thread_name.to_string()) .spawn(job) .map(drop) }) } + #[expect( + clippy::disallowed_methods, + reason = "a panicking operation must still publish a terminal failure carrying its panic message; the exit guard only reports a bare disconnect" + )] fn try_submit_with_spawner( &mut self, context: C, operation: impl FnOnce() -> T + Send + 'static, - spawn: impl FnOnce(Box) -> std::io::Result<()>, + spawn: impl FnOnce(Box) -> io::Result<()>, ) -> Result> { if !self.healthy { return Err(RuntimeOperationSubmitFailure { @@ -193,7 +199,7 @@ where return Err(RuntimeOperationSubmitFailure { error, context }); } }; - let (sender, receiver) = std::sync::mpsc::sync_channel(1); + let (sender, receiver) = mpsc::sync_channel(1); let runtime_wake = self.runtime_wake.clone(); let job = Box::new(move || { let guard = RuntimeOperationExitGuard::new(id, sender, runtime_wake); @@ -201,7 +207,7 @@ where Ok(outcome) => ProducerMessage::Ready { id, outcome }, Err(payload) => ProducerMessage::Failed { id, - reason: panic_reason(payload), + reason: panic_reason(&*payload), }, }; guard.publish(message); @@ -227,7 +233,7 @@ where &mut self, context: C, operation: impl FnOnce() -> T + Send + 'static, - spawn: impl FnOnce(Box) -> std::io::Result<()>, + spawn: impl FnOnce(Box) -> io::Result<()>, ) -> Result> { self.try_submit_with_spawner(context, operation, spawn) } @@ -299,7 +305,7 @@ where } } -fn panic_reason(payload: Box) -> String { +fn panic_reason(payload: &(dyn Any + Send)) -> String { if let Some(message) = payload.downcast_ref::<&'static str>() { (*message).to_string() } else if let Some(message) = payload.downcast_ref::() { @@ -377,12 +383,13 @@ mod tests { use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::mpsc; use std::time::{Duration, Instant}; + use std::{fs, thread}; use super::*; use crate::backend::wayland::RuntimeWakeSource; fn controller() -> (RuntimeWakeSource, RuntimeOperationController) { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let controller = RuntimeOperationController::new(RuntimeOperationIdSource::new(), wake.handle()); (wake, controller) @@ -403,7 +410,7 @@ mod tests { let stat_path = format!("/proc/self/task/{tid}/stat"); let deadline = Instant::now() + Duration::from_secs(1); loop { - let state = std::fs::read_to_string(&stat_path).ok().and_then(|stat| { + let state = fs::read_to_string(&stat_path).ok().and_then(|stat| { stat.rsplit_once(") ") .and_then(|(_, suffix)| suffix.chars().next()) }); @@ -411,13 +418,13 @@ mod tests { return; } assert!(Instant::now() < deadline, "poller did not block: {state:?}"); - std::thread::yield_now(); + thread::yield_now(); } } #[test] fn shared_source_allocates_distinct_ids_across_controllers() { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let ids = RuntimeOperationIdSource::new(); let mut publish = RuntimeOperationController::new(ids.clone(), wake.handle()); let mut paste = RuntimeOperationController::new(ids, wake.handle()); @@ -508,7 +515,7 @@ mod tests { }) .unwrap(); let (tid_tx, tid_rx) = mpsc::channel(); - let poller = std::thread::spawn(move || { + let poller = thread::spawn(move || { // SAFETY: gettid has no preconditions and is used only to observe this test thread. let tid = unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t }; tid_tx.send(tid).unwrap(); @@ -547,11 +554,11 @@ mod tests { #[test] fn exit_guard_disconnects_before_waking() { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let ids = RuntimeOperationIdSource::new(); let mut controller = RuntimeOperationController::::new(ids, wake.handle()); let id = RuntimeOperationId(9); - let (sender, receiver) = std::sync::mpsc::sync_channel(1); + let (sender, receiver) = mpsc::sync_channel(1); controller.active = Some(ActiveOperation { id, context: 31, @@ -572,7 +579,7 @@ mod tests { .try_submit_with_spawner( 41, || 1, - |_job| Err(std::io::Error::other("injected spawn failure")), + |_job| Err(io::Error::other("injected spawn failure")), ) .unwrap_err(); assert_eq!( @@ -604,7 +611,7 @@ mod tests { fn identity_mismatch_restores_active_context_and_disables_controller() { let (_wake, mut controller) = controller::(); let active_id = RuntimeOperationId(3); - let (sender, receiver) = std::sync::mpsc::sync_channel(1); + let (sender, receiver) = mpsc::sync_channel(1); controller.active = Some(ActiveOperation { id: active_id, context: 43, diff --git a/src/backend/wayland/runtime_ui_state.rs b/src/backend/wayland/runtime_ui_state.rs index 8440554fb..6dd6ef63c 100644 --- a/src/backend/wayland/runtime_ui_state.rs +++ b/src/backend/wayland/runtime_ui_state.rs @@ -7,15 +7,27 @@ use std::collections::BTreeMap; use std::path::PathBuf; -use anyhow::{Context, Result}; - -use crate::backend::wayland::state::MoveDragKind; -use crate::config::{ - Config, ToolbarItemOrderGroup, ToolbarSectionVisibility, TopDisplayMode, - fold_legacy_section_flags, item_visibility_setting, resettable_individual_toolbar_item_ids, -}; +use crate::backend::wayland::state::{MoveDragKind, ToolbarChrome}; +use crate::config::ToolbarItemOrderGroup; +#[cfg(test)] +use crate::config::{Config, TopDisplayMode, resettable_individual_toolbar_item_ids}; use crate::input::InputState; -use crate::runtime_ui_state::*; +use crate::input::boards::BoardIdentityGeneration; +#[cfg(test)] +use crate::runtime_ui_state::{ + BeginPersistenceRecoveryResult, CheckoutPersistenceRecoveryHandleResult, InteractionSeedTarget, + InteractionSeedValue, PersistedTopDisplayMode, PersistenceIncidentId, + PersistenceRecoveryAction, PersistenceRecoveryRequest, PersistenceRecoveryResult, + PreviewFinishResult, RequestResetResult, RuntimePreviewFinishIntent, + RuntimeStateFailurePathEffect, RuntimeStateIoError, RuntimeStateObservedPathEffect, + RuntimeStateRecoveryArtifact, RuntimeStateSourceObservation, RuntimeStateWriterCompletion, + RuntimeUiMutationScope, RuntimeUiMutationValues, RuntimeUiStateStore, SourceMutationResult, + SubmitSourceMutationResult, +}; +use crate::runtime_ui_state::{ + PreviewRollbackSnapshot, RuntimeStateWriterCommand, RuntimeUiPreviewSession, + RuntimeUiStateController, RuntimeUiStateWriter, +}; use crate::ui::toolbar::model::ToolbarRuntimeUiPersistenceTarget; mod board; @@ -33,18 +45,18 @@ pub(in crate::backend::wayland) use seed_refresh::{ RuntimeUiSeedRefresh, SeedRefreshContext, refresh_runtime_ui_config_seeds, }; -use live_state::{ - apply_live_board_state, apply_live_toolbar_positions, apply_live_toolbar_state, - apply_persisted_top_display_mode, runtime_preview_authority, top_display_mode_values, -}; +#[cfg(test)] +use live_state::top_display_mode_values; +#[cfg(test)] pub(in crate::backend::wayland) use live_state::{user_click_highlight_enabled, user_tool_preview}; -use positions::{ - position_rollback, position_seed_targets, position_values, rejected_source_mutation, -}; +use positions::position_rollback; +#[cfg(test)] pub(in crate::backend::wayland) use rollback::apply_toolbar_runtime_rollback; +use seeds::toolbar_values; +#[cfg(test)] use seeds::{ board_pin_seeds_from_input, retain_stored_board_pin_seeds_for_session_restore, - runtime_seeds_from_config, toolbar_values, + runtime_seeds_from_config, }; pub(in crate::backend::wayland) use seeds::{click_highlight_values, single_bool_seed_target}; @@ -56,7 +68,7 @@ pub(in crate::backend::wayland) struct ToolbarPositionSnapshot { } impl ToolbarPositionSnapshot { - fn from_chrome(chrome: &crate::backend::wayland::state::ToolbarChrome) -> Self { + fn from_chrome(chrome: &ToolbarChrome) -> Self { Self { top: chrome.top_offset(), } @@ -106,7 +118,7 @@ pub(in crate::backend::wayland) struct PreparedBoardPinMutation { #[derive(Debug)] struct DeferredBoardPinRestore { board_id: String, - board_identity_generation: crate::input::boards::BoardIdentityGeneration, + board_identity_generation: BoardIdentityGeneration, pin_seed: bool, pinned: bool, authority_epoch: u64, diff --git a/src/backend/wayland/runtime_ui_state/board.rs b/src/backend/wayland/runtime_ui_state/board.rs index 87ea86d71..72ac0c52d 100644 --- a/src/backend/wayland/runtime_ui_state/board.rs +++ b/src/backend/wayland/runtime_ui_state/board.rs @@ -1,4 +1,15 @@ -use super::*; +use std::mem; + +use super::seeds::runtime_seeds_from_config; +use super::{ + DeferredBoardPinRestore, PreparedBoardPinMutation, ToolbarRuntimeFinish, ToolbarRuntimeState, +}; +use crate::config::Config; +use crate::input::InputState; +use crate::runtime_ui_state::{ + InteractionSeedTarget, InteractionSeedValue, PreviewRollbackSnapshot, + RuntimePreviewFinishIntent, RuntimeUiMutationScope, RuntimeUiMutationValues, UpdateSeedsResult, +}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum BoardSeedUpdate { @@ -16,7 +27,7 @@ impl ToolbarRuntimeState { return BoardSeedUpdate::Rejected; } }; - let outcome = match self.controller.update_seeds(seeds) { + let outcome = match self.controller.update_seeds(&seeds) { UpdateSeedsResult::Applied { .. } => BoardSeedUpdate::Applied, UpdateSeedsResult::StagedBehindBarrier { barrier, .. } => { log::debug!("Board runtime seed update staged behind barrier {barrier:?}"); @@ -104,17 +115,17 @@ impl ToolbarRuntimeState { input.apply_board_pinned_runtime(&board_id, pin_seed); return None; } - self.commit_board_pin_value(input, board_id, pin_seed, pinned) + self.commit_board_pin_value(input, &board_id, pin_seed, pinned) } fn commit_board_pin_value( &mut self, input: &mut InputState, - board_id: String, + board_id: &str, current: bool, desired: bool, ) -> Option { - let target = InteractionSeedTarget::BoardPin(board_id.clone()); + let target = InteractionSeedTarget::BoardPin(board_id.to_string()); let rollback = PreviewRollbackSnapshot { values: [(target.clone(), InteractionSeedValue::Bool(current))] .into_iter() @@ -128,11 +139,11 @@ impl ToolbarRuntimeState { Ok(session) => session, Err(error) => { log::warn!("Restored board pin runtime mutation blocked: {error:?}"); - input.apply_board_pinned_runtime(&board_id, current); + input.apply_board_pinned_runtime(board_id, current); return None; } }; - input.apply_board_pinned_runtime(&board_id, desired); + input.apply_board_pinned_runtime(board_id, desired); let values = RuntimeUiMutationValues::one(target, InteractionSeedValue::Bool(desired)) .expect("board pin value matches its runtime target"); let result = self @@ -151,7 +162,7 @@ impl ToolbarRuntimeState { let current_generation = input.boards.board_identity_generation(); let current_epoch = self.controller.authority_epoch(); - let deferred = std::mem::take(&mut self.deferred_board_pin_restores); + let deferred = mem::take(&mut self.deferred_board_pin_restores); let mut finishes = Vec::new(); for restore in deferred.into_values() { if restore.board_identity_generation != current_generation @@ -174,7 +185,7 @@ impl ToolbarRuntimeState { continue; } if let Some(finish) = - self.commit_board_pin_value(input, restore.board_id, current, restore.pinned) + self.commit_board_pin_value(input, &restore.board_id, current, restore.pinned) { finishes.push(finish); } diff --git a/src/backend/wayland/runtime_ui_state/coordinator.rs b/src/backend/wayland/runtime_ui_state/coordinator.rs index c7b9e81f5..039090a86 100644 --- a/src/backend/wayland/runtime_ui_state/coordinator.rs +++ b/src/backend/wayland/runtime_ui_state/coordinator.rs @@ -1,21 +1,58 @@ use std::path::Path; use std::sync::mpsc::TryRecvError; +use std::{fs, mem}; use anyhow::{Context, Result}; -use super::*; +use crate::backend::wayland::RuntimeWakeHandle; +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + +use super::lifecycle::RuntimeUiLifecycleState; +use super::live_state::{ + apply_live_board_state, apply_live_toolbar_positions, apply_live_toolbar_state, + runtime_preview_authority, +}; +use super::positions::{ + position_rollback, position_seed_targets, position_values, rejected_source_mutation, +}; +use super::rollback::apply_toolbar_runtime_rollback; +use super::seeds::{ + board_pin_seeds_from_input, retain_stored_board_pin_seeds_for_session_restore, + runtime_seeds_from_config, toolbar_values, +}; +use super::{ + ActiveItemDrag, ActivePositionDrag, PreparedToolbarMutation, ToolbarPositionSnapshot, + ToolbarRuntimeDrain, ToolbarRuntimeFinish, ToolbarRuntimeState, ToolbarSeedRefresh, +}; +use crate::backend::wayland::state::MoveDragKind; +use crate::config::{Config, ToolbarItemOrderGroup}; +use crate::input::InputState; +use crate::runtime_ui_state::{ + AbandonedPreviewResolutionReason, ControllerBarrierId, ExternalAuthorityInstallError, + InteractionSeedTarget, PreviewFinishResult, PreviewRollbackSnapshot, RecoveryIoCompletion, + RecoveryIoOperation, RecoveryIoResult, RuntimePreviewFinishIntent, + RuntimeStateFailurePathEffect, RuntimeStateInspectionError, RuntimeStateIoError, + RuntimeStateObservedPathEffect, RuntimeStateSourceObservation, RuntimeStateWriterCommand, + RuntimeStateWriterCompletion, RuntimeStateWriterSubmitError, RuntimeUiFileStatus, + RuntimeUiMutationScope, RuntimeUiMutationValues, RuntimeUiStateStore, RuntimeUiStateWriter, + RuntimeUiWireState, SourceMutationResult, SubmitSourceMutationResult, UpdateSeedsResult, + decode_runtime_ui_file, runtime_preview_rollback, +}; +use crate::ui::toolbar::model::ToolbarRuntimeUiPersistenceTarget; +use std::collections::BTreeMap; impl ToolbarRuntimeState { pub(in crate::backend::wayland) fn start( config: &Config, input: &InputState, path: &Path, - runtime_wake: crate::backend::wayland::RuntimeWakeHandle, + runtime_wake: RuntimeWakeHandle, ) -> Result { let parent = path .parent() .context("runtime UI state path has no parent directory")?; - std::fs::create_dir_all(parent).with_context(|| { + fs::create_dir_all(parent).with_context(|| { format!( "failed to create runtime UI state directory {}", parent.display() @@ -61,8 +98,8 @@ impl ToolbarRuntimeState { pub(in crate::backend::wayland) fn apply_startup_state( &self, - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, input: &mut InputState, ) { apply_live_toolbar_state( @@ -90,8 +127,8 @@ impl ToolbarRuntimeState { pub(super) fn apply_live_state( &self, - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, input: &mut InputState, positions: &mut ToolbarPositionSnapshot, ) { @@ -122,7 +159,7 @@ impl ToolbarRuntimeState { input: &InputState, ) -> Option { let values = toolbar_values(target, input).ok()?; - self.begin_toolbar_mutation_with_rollback(target, values) + self.begin_toolbar_mutation_with_rollback(target, &values) } /// Begin a toolbar mutation whose pre-change values are already known. @@ -132,7 +169,7 @@ impl ToolbarRuntimeState { pub(in crate::backend::wayland) fn begin_toolbar_mutation_with_rollback( &self, target: ToolbarRuntimeUiPersistenceTarget, - rollback_values: RuntimeUiMutationValues, + rollback_values: &RuntimeUiMutationValues, ) -> Option { let rollback = PreviewRollbackSnapshot { values: rollback_values.values().clone(), @@ -287,8 +324,8 @@ impl ToolbarRuntimeState { pub(in crate::backend::wayland) fn refresh_config_seeds( &mut self, - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, config: &Config, input: &mut InputState, positions: &mut ToolbarPositionSnapshot, @@ -301,7 +338,7 @@ impl ToolbarRuntimeState { return ToolbarSeedRefresh::default(); } }; - let changed = match self.controller.update_seeds(seeds) { + let changed = match self.controller.update_seeds(&seeds) { UpdateSeedsResult::Applied { changed_targets, .. } => changed_targets, @@ -368,7 +405,7 @@ impl ToolbarRuntimeState { pub(super) fn drain_writer_completions(&mut self) -> ToolbarRuntimeDrain { let lifecycle_before = self.persistence_snapshot(); let mut drain = ToolbarRuntimeDrain { - rebuild_live: std::mem::take(&mut self.live_rebuild_pending), + rebuild_live: mem::take(&mut self.live_rebuild_pending), ..ToolbarRuntimeDrain::default() }; self.collect_preview_resolutions(&mut drain); @@ -384,7 +421,7 @@ impl ToolbarRuntimeState { }; self.integrate_writer_completion(completion); self.poll_recovery_completion(); - drain.rebuild_live |= std::mem::take(&mut self.live_rebuild_pending); + drain.rebuild_live |= mem::take(&mut self.live_rebuild_pending); self.collect_preview_resolutions(&mut drain); self.dispatch_writer_command(); } diff --git a/src/backend/wayland/runtime_ui_state/lifecycle.rs b/src/backend/wayland/runtime_ui_state/lifecycle.rs index 8fe8a568f..3eb7b4b41 100644 --- a/src/backend/wayland/runtime_ui_state/lifecycle.rs +++ b/src/backend/wayland/runtime_ui_state/lifecycle.rs @@ -1,6 +1,15 @@ use std::path::PathBuf; -use super::*; +use super::ToolbarRuntimeState; +use crate::runtime_ui_state::{ + BeginPersistenceRecoveryResult, CancelPersistenceRecoveryResult, + CancelUnsupportedResetConfirmationResult, CheckoutPersistenceRecoveryHandleResult, + ConfirmUnsupportedResetResult, ControllerBarrierPhase, InvalidStateResetConfirmation, + PersistenceIncidentId, PersistenceRecoveryAction, PersistenceRecoveryEvidence, + PersistenceRecoveryHandle, PersistenceRecoveryRequest, PersistenceRecoveryResult, + RecoveryCancellation, RecoveryCompletionReceiver, RequestResetResult, RuntimeStateIoError, + RuntimeStateRecoveryArtifact, RuntimeUiFileStatus, UnsupportedResetConfirmation, +}; use crate::ui::toolbar::{RuntimeUiPersistenceMode, RuntimeUiPersistenceSnapshot, ToolbarEvent}; #[derive(Debug)] @@ -221,7 +230,7 @@ impl ToolbarRuntimeState { self.lifecycle.detail = Some( match self .controller - .cancel_unsupported_reset_confirmation(prompt.confirmation) + .cancel_unsupported_reset_confirmation(&prompt.confirmation) { CancelUnsupportedResetConfirmationResult::Cancelled => { "The unsupported runtime-state file was left unchanged.".to_string() diff --git a/src/backend/wayland/runtime_ui_state/live_state.rs b/src/backend/wayland/runtime_ui_state/live_state.rs index ffe71f029..69200e29e 100644 --- a/src/backend/wayland/runtime_ui_state/live_state.rs +++ b/src/backend/wayland/runtime_ui_state/live_state.rs @@ -1,4 +1,17 @@ -use super::*; +use crate::config::{StatusBarItem, ToolbarSectionFlag}; +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + +use super::ToolbarPositionSnapshot; +use crate::config::{ + ToolbarItemOrderGroup, TopDisplayMode, resettable_individual_toolbar_item_ids, +}; +use crate::input::InputState; +use crate::runtime_ui_state::{ + ControllerId, InteractionSeedTarget, InteractionSeedValue, MutationShapeError, + PersistedTopDisplayMode, RuntimeUiLiveState, RuntimeUiMutationValues, RuntimeUiPreviewSession, + SeedGuard, +}; /// Presenter mode can hold the tool preview hidden while it runs, so what /// persists is the value presenter will restore -- the user's own. @@ -29,7 +42,7 @@ fn persisted_top_display_mode( pub(super) fn top_display_mode_values( mode: TopDisplayMode, input: &InputState, -) -> std::result::Result { +) -> Result { let presenter_restore = input.presenter_restored_top_display_mode(); RuntimeUiMutationValues::one( InteractionSeedTarget::TopDisplayMode, @@ -43,8 +56,8 @@ pub(super) fn top_display_mode_values( /// belongs to what presenter will restore, not to the live strip; anywhere /// else it is the live strip's mode. pub(super) fn apply_persisted_top_display_mode( - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, input: &mut InputState, mode: PersistedTopDisplayMode, ) { @@ -58,8 +71,8 @@ pub(super) fn apply_persisted_top_display_mode( } pub(super) fn apply_live_toolbar_state( - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, input: &mut InputState, live: &RuntimeUiLiveState, include: impl Fn(&InteractionSeedTarget) -> bool, @@ -70,16 +83,16 @@ pub(super) fn apply_live_toolbar_state( apply_live_toolbar_structure(engine, measurer, input, live, &include); } -fn live_bool(live: &RuntimeUiLiveState, target: InteractionSeedTarget) -> Option { - match live.get(&target) { +fn live_bool(live: &RuntimeUiLiveState, target: &InteractionSeedTarget) -> Option { + match live.get(target) { Some(InteractionSeedValue::Bool(value)) => Some(*value), _ => None, } } fn apply_live_display_flags( - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, input: &mut InputState, live: &RuntimeUiLiveState, include: &impl Fn(&InteractionSeedTarget) -> bool, @@ -94,22 +107,22 @@ fn apply_live_display_flags( apply_persisted_top_display_mode(engine, measurer, input, *mode); } if include(&InteractionSeedTarget::StatusBar) - && let Some(value) = live_bool(live, InteractionSeedTarget::StatusBar) + && let Some(value) = live_bool(live, &InteractionSeedTarget::StatusBar) { input.ui_visibility.show_status_bar = value; } if include(&InteractionSeedTarget::StatusBoardBadge) - && let Some(value) = live_bool(live, InteractionSeedTarget::StatusBoardBadge) + && let Some(value) = live_bool(live, &InteractionSeedTarget::StatusBoardBadge) { input.ui_visibility.show_status_board_badge = value; } if include(&InteractionSeedTarget::StatusPageBadge) - && let Some(value) = live_bool(live, InteractionSeedTarget::StatusPageBadge) + && let Some(value) = live_bool(live, &InteractionSeedTarget::StatusPageBadge) { input.ui_visibility.show_status_page_badge = value; } if include(&InteractionSeedTarget::FloatingBadgeAlways) - && let Some(value) = live_bool(live, InteractionSeedTarget::FloatingBadgeAlways) + && let Some(value) = live_bool(live, &InteractionSeedTarget::FloatingBadgeAlways) { input.ui_visibility.show_floating_badge_always = value; } @@ -121,42 +134,42 @@ fn apply_live_toolbar_preferences( include: &impl Fn(&InteractionSeedTarget) -> bool, ) { if include(&InteractionSeedTarget::ToolbarIcons) - && let Some(value) = live_bool(live, InteractionSeedTarget::ToolbarIcons) + && let Some(value) = live_bool(live, &InteractionSeedTarget::ToolbarIcons) { input.set_toolbar_use_icons(value); } if include(&InteractionSeedTarget::ToolbarMoreColors) - && let Some(value) = live_bool(live, InteractionSeedTarget::ToolbarMoreColors) + && let Some(value) = live_bool(live, &InteractionSeedTarget::ToolbarMoreColors) { input.ui_visibility.show_more_colors = value; } if include(&InteractionSeedTarget::ToolbarContextAwareUi) - && let Some(value) = live_bool(live, InteractionSeedTarget::ToolbarContextAwareUi) + && let Some(value) = live_bool(live, &InteractionSeedTarget::ToolbarContextAwareUi) { input.ui_visibility.context_aware_ui = value; } if include(&InteractionSeedTarget::ToolbarPresetToasts) - && let Some(value) = live_bool(live, InteractionSeedTarget::ToolbarPresetToasts) + && let Some(value) = live_bool(live, &InteractionSeedTarget::ToolbarPresetToasts) { input.ui_visibility.show_preset_toasts = value; } if include(&InteractionSeedTarget::ToolbarIdleFade) - && let Some(value) = live_bool(live, InteractionSeedTarget::ToolbarIdleFade) + && let Some(value) = live_bool(live, &InteractionSeedTarget::ToolbarIdleFade) { input.ui_visibility.idle_fade = value; } if include(&InteractionSeedTarget::ToolbarToolPreview) - && let Some(value) = live_bool(live, InteractionSeedTarget::ToolbarToolPreview) + && let Some(value) = live_bool(live, &InteractionSeedTarget::ToolbarToolPreview) { input.ui_visibility.show_tool_preview = value; } if include(&InteractionSeedTarget::ToolbarDelaySliders) - && let Some(value) = live_bool(live, InteractionSeedTarget::ToolbarDelaySliders) + && let Some(value) = live_bool(live, &InteractionSeedTarget::ToolbarDelaySliders) { input.ui_visibility.show_delay_sliders = value; } if include(&InteractionSeedTarget::HistoryCustomSection) - && let Some(value) = live_bool(live, InteractionSeedTarget::HistoryCustomSection) + && let Some(value) = live_bool(live, &InteractionSeedTarget::HistoryCustomSection) { input.history_limits.set_custom_section_enabled(value); } @@ -168,35 +181,35 @@ fn apply_live_overlay_flags( include: &impl Fn(&InteractionSeedTarget) -> bool, ) { if include(&InteractionSeedTarget::InputHud) - && let Some(value) = live_bool(live, InteractionSeedTarget::InputHud) + && let Some(value) = live_bool(live, &InteractionSeedTarget::InputHud) { input.set_input_hud_enabled(value); } if include(&InteractionSeedTarget::FloatingBadge) - && let Some(value) = live_bool(live, InteractionSeedTarget::FloatingBadge) + && let Some(value) = live_bool(live, &InteractionSeedTarget::FloatingBadge) { input.ui_visibility.show_floating_badge = value; } if include(&InteractionSeedTarget::ZoomChip) - && let Some(value) = live_bool(live, InteractionSeedTarget::ZoomChip) + && let Some(value) = live_bool(live, &InteractionSeedTarget::ZoomChip) { input.ui_visibility.show_zoom_chip = value; } if include(&InteractionSeedTarget::ClickHighlight) - && let Some(value) = live_bool(live, InteractionSeedTarget::ClickHighlight) + && let Some(value) = live_bool(live, &InteractionSeedTarget::ClickHighlight) { input.set_click_highlight_enabled(value); } if include(&InteractionSeedTarget::ClickHighlightToolRing) - && let Some(value) = live_bool(live, InteractionSeedTarget::ClickHighlightToolRing) + && let Some(value) = live_bool(live, &InteractionSeedTarget::ClickHighlightToolRing) { input.set_highlight_tool_ring_enabled(value); } } fn apply_live_toolbar_structure( - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, input: &mut InputState, live: &RuntimeUiLiveState, include: &impl Fn(&InteractionSeedTarget) -> bool, @@ -207,7 +220,7 @@ fn apply_live_toolbar_structure( { input.apply_toolbar_layout_mode_runtime(*mode); } - for flag in crate::config::ToolbarSectionFlag::ALL { + for flag in ToolbarSectionFlag::ALL { let target = InteractionSeedTarget::SectionVisibility(flag); if include(&target) && let Some(InteractionSeedValue::Visibility(setting)) = live.get(&target) @@ -216,24 +229,24 @@ fn apply_live_toolbar_structure( } } if include(&InteractionSeedTarget::StatusBarInteractive) - && let Some(value) = live_bool(live, InteractionSeedTarget::StatusBarInteractive) + && let Some(value) = live_bool(live, &InteractionSeedTarget::StatusBarInteractive) { input.ui_visibility.status_bar_interactive = value; } - for item in crate::config::StatusBarItem::ALL { + for item in StatusBarItem::ALL { if include(&InteractionSeedTarget::StatusBarItem(item)) - && let Some(value) = live_bool(live, InteractionSeedTarget::StatusBarItem(item)) + && let Some(value) = live_bool(live, &InteractionSeedTarget::StatusBarItem(item)) { input.set_status_bar_item_visible_with_resources(engine, measurer, item, value); } } if include(&InteractionSeedTarget::TopPinned) - && let Some(value) = live_bool(live, InteractionSeedTarget::TopPinned) + && let Some(value) = live_bool(live, &InteractionSeedTarget::TopPinned) { input.set_toolbar_top_pinned(value); } if include(&InteractionSeedTarget::TopMinimized) - && let Some(value) = live_bool(live, InteractionSeedTarget::TopMinimized) + && let Some(value) = live_bool(live, &InteractionSeedTarget::TopMinimized) { input.apply_toolbar_set_top_minimized(value); } diff --git a/src/backend/wayland/runtime_ui_state/positions.rs b/src/backend/wayland/runtime_ui_state/positions.rs index 6b633f654..23f6774da 100644 --- a/src/backend/wayland/runtime_ui_state/positions.rs +++ b/src/backend/wayland/runtime_ui_state/positions.rs @@ -1,4 +1,12 @@ -use super::*; +use std::collections::BTreeMap; + +use super::ToolbarPositionSnapshot; +use crate::backend::wayland::state::MoveDragKind; +use crate::runtime_ui_state::{ + InteractionSeedTarget, InteractionSeedValue, PreviewRollbackSnapshot, + RuntimeStateFailurePathEffect, RuntimeStateIoError, RuntimeStateObservedPathEffect, + RuntimeUiMutationValues, SourceMutationId, SourceMutationResult, ToolbarPositionSeed, +}; /// The toolbar position a move drag can write. /// @@ -46,7 +54,7 @@ pub(super) fn position_rollback( kind: MoveDragKind, positions: ToolbarPositionSnapshot, ) -> PreviewRollbackSnapshot { - let mut values = std::collections::BTreeMap::new(); + let mut values = BTreeMap::new(); for target in position_drag_targets(kind) { let (x, y) = target.offsets(positions); if let Some(position) = ToolbarPositionSeed::new(x, y) { diff --git a/src/backend/wayland/runtime_ui_state/rollback.rs b/src/backend/wayland/runtime_ui_state/rollback.rs index d478946c1..d8022a7d2 100644 --- a/src/backend/wayland/runtime_ui_state/rollback.rs +++ b/src/backend/wayland/runtime_ui_state/rollback.rs @@ -1,8 +1,16 @@ -use super::*; +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + +use super::ToolbarPositionSnapshot; +use super::live_state::apply_persisted_top_display_mode; +use crate::input::InputState; +use crate::runtime_ui_state::{ + InteractionSeedTarget, InteractionSeedValue, PreviewRollbackSnapshot, +}; pub(in crate::backend::wayland) fn apply_toolbar_runtime_rollback( - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, input: &mut InputState, positions: &mut ToolbarPositionSnapshot, rollback: &PreviewRollbackSnapshot, diff --git a/src/backend/wayland/runtime_ui_state/seed_refresh.rs b/src/backend/wayland/runtime_ui_state/seed_refresh.rs index bd5695a33..9c0ed7caa 100644 --- a/src/backend/wayland/runtime_ui_state/seed_refresh.rs +++ b/src/backend/wayland/runtime_ui_state/seed_refresh.rs @@ -1,20 +1,21 @@ //! Shared seed reconciliation and publication; only protocol drag teardown is adapter-owned. use super::{ToolbarPositionSnapshot, ToolbarRuntimeState}; -use crate::backend::wayland::{ - state::{ToolbarChrome, ToolbarDrag}, - toolbar::ToolbarSurfaceManager, -}; -use crate::{config::Config, draw::TextMeasurer, input::InputState, ui_text::UiTextEngine}; +use crate::backend::wayland::state::{ToolbarChrome, ToolbarDrag}; +use crate::backend::wayland::toolbar::ToolbarSurfaceManager; +use crate::config::Config; +use crate::draw::TextMeasurer; +use crate::input::InputState; +use crate::ui_text::UiTextEngine; -pub(in crate::backend::wayland) struct SeedRefreshContext<'a> { - pub config: &'a Config, - pub input: &'a mut InputState, - pub engine: &'a UiTextEngine, - pub measurer: &'a TextMeasurer, - pub runtime: Option<&'a mut ToolbarRuntimeState>, - pub drag: &'a mut ToolbarDrag, - pub chrome: &'a mut ToolbarChrome, - pub toolbar: &'a mut ToolbarSurfaceManager, +pub(in crate::backend::wayland) struct SeedRefreshContext<'state> { + pub config: &'state Config, + pub input: &'state mut InputState, + pub engine: &'state UiTextEngine, + pub measurer: &'state TextMeasurer, + pub runtime: Option<&'state mut ToolbarRuntimeState>, + pub drag: &'state mut ToolbarDrag, + pub chrome: &'state mut ToolbarChrome, + pub toolbar: &'state mut ToolbarSurfaceManager, } pub(in crate::backend::wayland) trait RuntimeUiSeedRefresh { diff --git a/src/backend/wayland/runtime_ui_state/seeds.rs b/src/backend/wayland/runtime_ui_state/seeds.rs index a05cda6aa..93ea0cd75 100644 --- a/src/backend/wayland/runtime_ui_state/seeds.rs +++ b/src/backend/wayland/runtime_ui_state/seeds.rs @@ -1,4 +1,19 @@ -use super::*; +use crate::config::{ResolvedToolbarItems, StatusBarItem, ToolbarSectionFlag}; + +use super::live_state::{top_display_mode_values, user_click_highlight_enabled, user_tool_preview}; +use crate::config::{ + Config, ToolbarItemOrderGroup, ToolbarSectionVisibility, fold_legacy_section_flags, + item_visibility_setting, resettable_individual_toolbar_item_ids, +}; +use crate::input::InputState; +use crate::runtime_ui_state::{ + InteractionSeedTarget, InteractionSeedValue, MutationShapeError, PersistedTopDisplayMode, + RuntimeUiMutationValues, RuntimeUiStateInspection, ToolbarPositionSeed, + ValidatedInteractionSeeds, +}; +use crate::ui::toolbar::model::ToolbarRuntimeUiPersistenceTarget; +use anyhow::{Context, Result}; +use std::collections::BTreeMap; pub(super) fn runtime_seeds_from_config( config: &Config, @@ -75,7 +90,7 @@ pub(super) fn runtime_seeds_from_config( InteractionSeedTarget::StatusBarInteractive, InteractionSeedValue::Bool(config.ui.status_bar_interactive), )?; - for item in crate::config::StatusBarItem::ALL { + for item in StatusBarItem::ALL { insert( InteractionSeedTarget::StatusBarItem(item), InteractionSeedValue::Bool(config.ui.status_bar_item_visible(item)), @@ -102,7 +117,7 @@ pub(super) fn runtime_seeds_from_config( InteractionSeedTarget::ToolbarLayoutMode, InteractionSeedValue::LayoutMode(config.ui.toolbar.layout_mode), )?; - for flag in crate::config::ToolbarSectionFlag::ALL { + for flag in ToolbarSectionFlag::ALL { insert( InteractionSeedTarget::SectionVisibility(flag), InteractionSeedValue::Visibility(item_visibility_setting( @@ -172,7 +187,7 @@ pub(super) fn retain_stored_board_pin_seeds_for_session_restore( } } -fn resolved_toolbar_item_seeds(config: &Config) -> crate::config::ResolvedToolbarItems { +fn resolved_toolbar_item_seeds(config: &Config) -> ResolvedToolbarItems { let toolbar = &config.ui.toolbar; let mut items = toolbar.items.clone(); let mut legacy = ToolbarSectionVisibility { @@ -198,7 +213,7 @@ fn resolved_toolbar_item_seeds(config: &Config) -> crate::config::ResolvedToolba pub(super) fn toolbar_values( target: ToolbarRuntimeUiPersistenceTarget, input: &InputState, -) -> std::result::Result { +) -> Result { use ToolbarRuntimeUiPersistenceTarget as Target; match target { Target::TopPinned => RuntimeUiMutationValues::one( @@ -355,7 +370,7 @@ pub(in crate::backend::wayland) fn single_bool_seed_target( pub(in crate::backend::wayland) fn click_highlight_values( enabled: bool, tool_ring: bool, -) -> std::result::Result { +) -> Result { RuntimeUiMutationValues::batch([ ( InteractionSeedTarget::ClickHighlight, diff --git a/src/backend/wayland/runtime_ui_state/tests.rs b/src/backend/wayland/runtime_ui_state/tests.rs index db5a9e921..ee1c572d0 100644 --- a/src/backend/wayland/runtime_ui_state/tests.rs +++ b/src/backend/wayland/runtime_ui_state/tests.rs @@ -1,13 +1,18 @@ use super::*; -use std::fs; use std::path::Path; -use std::thread; use std::time::Duration; +use std::{fs, thread}; -use crate::config::{ToolbarItemsConfig, toolbar_item_ids as ids}; +use crate::config::{ToolbarItemsConfig, ToolbarSectionFlag, toolbar_item_ids as ids}; +use crate::draw::TextMeasurer; +use crate::input::boards::BoardManager; use crate::input::state::test_support::make_test_input_state; +use crate::runtime_ui_state::{ + CommitResult, ItemVisibilitySetting, RuntimeStateObservedEnvelope, RuntimeStateSourceRevision, +}; use crate::ui::toolbar::{RuntimeUiPersistenceMode, RuntimeUiPersistenceSnapshot, ToolbarEvent}; +use crate::ui_text::UiTextEngine; mod board_pin_resets; mod board_pins; @@ -21,7 +26,7 @@ mod visibility_recovery; fn input_from_config(config: &Config) -> InputState { let mut input = make_test_input_state(); - input.boards = crate::input::boards::BoardManager::from_config(config.resolved_boards()); + input.boards = BoardManager::from_config(config.resolved_boards()); input.init_toolbar_from_config(&config.ui.toolbar); input } @@ -100,12 +105,12 @@ fn settle_runtime(runtime: &mut ToolbarRuntimeState) -> ToolbarRuntimeDrain { fn wait_for_runtime_mode( runtime: &mut ToolbarRuntimeState, - expected: RuntimeUiPersistenceMode, + expected: &RuntimeUiPersistenceMode, ) -> RuntimeUiPersistenceSnapshot { for _ in 0..800 { runtime.drain_writer_completions(); let snapshot = runtime.persistence_snapshot(); - if snapshot.mode == expected { + if snapshot.mode == *expected { return snapshot; } thread::sleep(Duration::from_millis(5)); @@ -123,8 +128,8 @@ fn apply_finish( ) { if let ToolbarRuntimeFinish::Rollback(rollback) = finish { apply_toolbar_runtime_rollback( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), input, positions, &rollback, @@ -166,12 +171,12 @@ fn config_positions(config: &Config) -> ToolbarPositionSnapshot { fn stored_position( runtime: &ToolbarRuntimeState, - target: InteractionSeedTarget, + target: &InteractionSeedTarget, ) -> Option<(f64, f64)> { match runtime .controller .model() - .get(&target) + .get(target) .map(|entry| &entry.value) { Some(InteractionSeedValue::Position(position)) => { @@ -181,11 +186,11 @@ fn stored_position( } } -fn stored_bool(runtime: &ToolbarRuntimeState, target: InteractionSeedTarget) -> Option { +fn stored_bool(runtime: &ToolbarRuntimeState, target: &InteractionSeedTarget) -> Option { match runtime .controller .model() - .get(&target) + .get(target) .map(|entry| &entry.value) { Some(InteractionSeedValue::Bool(value)) => Some(*value), @@ -208,15 +213,15 @@ fn stored_display_mode(runtime: &ToolbarRuntimeState) -> Option ToolbarRuntimeFinish { let target = ToolbarRuntimeUiPersistenceTarget::TopDisplayMode; let prepared = runtime .begin_toolbar_mutation(target, input) .expect("display mode permit"); input.set_top_display_mode_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), mode, ); runtime.finish_toolbar_mutation(prepared, true, input) @@ -243,7 +248,7 @@ fn pins_rollback(top: bool) -> PreviewRollbackSnapshot { } } -fn section_toggle(flag: crate::config::ToolbarSectionFlag, show: bool) -> ToolbarEvent { +fn section_toggle(flag: ToolbarSectionFlag, show: bool) -> ToolbarEvent { use crate::config::ToolbarSectionFlag as Flag; match flag { Flag::Actions => ToolbarEvent::ToggleActionsSection(show), diff --git a/src/backend/wayland/runtime_ui_state/tests/board_pin_resets.rs b/src/backend/wayland/runtime_ui_state/tests/board_pin_resets.rs index d48b9578d..6438e5308 100644 --- a/src/backend/wayland/runtime_ui_state/tests/board_pin_resets.rs +++ b/src/backend/wayland/runtime_ui_state/tests/board_pin_resets.rs @@ -1,8 +1,10 @@ +use crate::test_temp; + use super::*; #[test] fn unrelated_board_pin_write_preserves_supported_unknown_fields() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); fs::write( &runtime_path, @@ -23,8 +25,8 @@ future_entry = [1, 2, 3] let mut input = input_from_config(&config); let mut runtime = test_runtime(&config, &runtime_path); runtime.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, ); assert!(board_pinned(&input, "whiteboard")); @@ -48,7 +50,7 @@ future_entry = [1, 2, 3] #[test] fn global_runtime_reset_clears_board_pin_override_and_live_value() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut input = input_from_config(&config); @@ -70,8 +72,8 @@ fn global_runtime_reset_clears_board_pin_override_and_live_value() { assert!(drain.rebuild_live); let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; runtime.apply_live_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, ); @@ -83,7 +85,7 @@ fn global_runtime_reset_clears_board_pin_override_and_live_value() { #[test] fn unsupported_runtime_file_keeps_toolbar_mutations_live_only_and_byte_exact() { const UNSUPPORTED: &[u8] = b"version = 22\nfuture = { keep = true }\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); fs::write(&runtime_path, UNSUPPORTED).unwrap(); let config = Config::default(); @@ -113,8 +115,8 @@ fn unsupported_runtime_file_keeps_toolbar_mutations_live_only_and_byte_exact() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); assert!(!restarted_input.toolbar_top_minimized()); @@ -126,11 +128,11 @@ fn unsupported_runtime_file_keeps_toolbar_mutations_live_only_and_byte_exact() { fn factory_visibility_reset_survives_restart_over_nondefault_authored_config() { const AUTHORED: &[u8] = b"# non-default authored toolbar seed\n[ui.toolbar.items]\nhidden = [\"top.tool.pen\"]\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); fs::write(&config_path, AUTHORED).unwrap(); - let config: Config = toml::from_str(std::str::from_utf8(AUTHORED).unwrap()).unwrap(); + let config: Config = toml::from_str(str::from_utf8(AUTHORED).unwrap()).unwrap(); let mut input = input_from_config(&config); assert!( input @@ -169,8 +171,8 @@ fn factory_visibility_reset_survives_restart_over_nondefault_authored_config() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); assert!( @@ -193,11 +195,11 @@ fn factory_visibility_reset_survives_restart_over_nondefault_authored_config() { #[test] fn factory_order_reset_survives_restart_over_nondefault_authored_config() { const AUTHORED: &[u8] = b"# preserve authored order exactly\n[ui.toolbar.items.order]\ntop_tools = [\"top.tool.pen\", \"top.tool.select\"]\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); fs::write(&config_path, AUTHORED).unwrap(); - let config: Config = toml::from_str(std::str::from_utf8(AUTHORED).unwrap()).unwrap(); + let config: Config = toml::from_str(str::from_utf8(AUTHORED).unwrap()).unwrap(); let mut input = input_from_config(&config); assert_eq!( input @@ -223,8 +225,8 @@ fn factory_order_reset_survives_restart_over_nondefault_authored_config() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); assert_eq!( diff --git a/src/backend/wayland/runtime_ui_state/tests/board_pins.rs b/src/backend/wayland/runtime_ui_state/tests/board_pins.rs index 78732a197..5d925d013 100644 --- a/src/backend/wayland/runtime_ui_state/tests/board_pins.rs +++ b/src/backend/wayland/runtime_ui_state/tests/board_pins.rs @@ -1,3 +1,5 @@ +use crate::test_temp; + use super::*; fn force_board_pin_test_persistence_failure( @@ -6,12 +8,12 @@ fn force_board_pin_test_persistence_failure( let target = InteractionSeedTarget::TopPinned; let permit = runtime .controller - .begin_mutation(RuntimeUiMutationScope::one(target.clone())) + .begin_mutation(&RuntimeUiMutationScope::one(target.clone())) .unwrap(); assert!(matches!( runtime.controller.commit( permit, - RuntimeUiMutationValues::one(target, InteractionSeedValue::Bool(false)).unwrap(), + &RuntimeUiMutationValues::one(target, InteractionSeedValue::Bool(false)).unwrap(), ), CommitResult::Accepted { .. } )); @@ -77,7 +79,7 @@ fn recover_board_pin_test_persistence( #[test] fn board_pin_is_runtime_owned_and_survives_restart_without_touching_config() { const AUTHORED: &[u8] = b"# authored config bytes stay exact\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); fs::write(&config_path, AUTHORED).unwrap(); @@ -108,8 +110,8 @@ fn board_pin_is_runtime_owned_and_survives_restart_without_touching_config() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); assert!(board_pinned(&restarted_input, "whiteboard")); @@ -119,7 +121,7 @@ fn board_pin_is_runtime_owned_and_survives_restart_without_touching_config() { #[test] fn session_only_board_pin_survives_startup_until_session_identity_is_known() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); fs::write( &runtime_path, @@ -142,8 +144,8 @@ value = true let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; let refresh = runtime.refresh_config_seeds( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &config, &mut input, &mut positions, @@ -160,7 +162,7 @@ value = true #[test] fn absent_provisional_board_pin_is_pruned_after_session_reconciliation() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); fs::write( &runtime_path, @@ -178,8 +180,8 @@ value = true let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; let refresh = runtime.refresh_config_seeds( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &config, &mut input, &mut positions, @@ -196,7 +198,7 @@ value = true #[test] fn newly_created_board_does_not_adopt_a_provisional_session_pin() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); fs::write( &runtime_path, @@ -222,8 +224,8 @@ value = true assert!(finish.is_none()); assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); runtime.apply_live_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut ToolbarPositionSnapshot { top: (0.0, 0.0) }, ); @@ -234,8 +236,8 @@ value = true assert!(restarted_input.create_board()); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); assert!(!board_pinned(&restarted_input, &board_id)); @@ -244,7 +246,7 @@ value = true #[test] fn restored_board_pin_is_replayed_after_same_authority_recovery() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut input = input_from_config(&config); @@ -268,8 +270,8 @@ fn restored_board_pin_is_replayed_after_same_authority_recovery() { let rebuild_live = recover_board_pin_test_persistence(&mut runtime, incident); assert!(rebuild_live); runtime.apply_live_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut ToolbarPositionSnapshot { top: (0.0, 0.0) }, ); @@ -291,7 +293,7 @@ fn restored_board_pin_is_replayed_after_same_authority_recovery() { #[test] fn deferred_board_pin_restore_is_discarded_when_reset_changes_authority() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut input = input_from_config(&config); @@ -319,8 +321,8 @@ fn deferred_board_pin_restore_is_discarded_when_reset_changes_authority() { assert!(runtime.controller.active_barrier().is_none()); assert_ne!(runtime.controller.authority_epoch(), original_epoch); runtime.apply_live_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut ToolbarPositionSnapshot { top: (0.0, 0.0) }, ); @@ -339,7 +341,7 @@ fn deferred_board_pin_restore_is_discarded_when_reset_changes_authority() { #[test] fn delayed_delete_and_same_id_reuse_cannot_resurrect_old_board_pin() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut input = input_from_config(&config); @@ -356,8 +358,8 @@ fn delayed_delete_and_same_id_reuse_cannot_resurrect_old_board_pin() { assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; runtime.apply_live_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, ); @@ -367,8 +369,8 @@ fn delayed_delete_and_same_id_reuse_cannot_resurrect_old_board_pin() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); assert!(!board_pinned(&restarted_input, "whiteboard")); @@ -377,7 +379,7 @@ fn delayed_delete_and_same_id_reuse_cannot_resurrect_old_board_pin() { #[test] fn stale_deferred_board_pin_is_rejected_after_authored_pin_reload() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config_a = Config::default(); let mut input = input_from_config(&config_a); @@ -400,8 +402,8 @@ fn stale_deferred_board_pin_is_rejected_after_authored_pin_reload() { .sync_pin_seeds_from_config(&config_b.resolved_boards()); let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; let refresh = runtime.refresh_config_seeds( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &config_b, &mut input, &mut positions, diff --git a/src/backend/wayland/runtime_ui_state/tests/drag_previews.rs b/src/backend/wayland/runtime_ui_state/tests/drag_previews.rs index dd1e68019..cfabf5535 100644 --- a/src/backend/wayland/runtime_ui_state/tests/drag_previews.rs +++ b/src/backend/wayland/runtime_ui_state/tests/drag_previews.rs @@ -1,8 +1,10 @@ +use crate::test_temp; + use super::*; #[test] fn item_drag_commit_accepts_one_revision_and_cancel_accepts_none() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut input = input_from_config(&config); @@ -109,7 +111,7 @@ fn unavailable_persistence_position_drag_cancel_restores_starting_offsets() { #[test] fn persistence_barrier_blocks_updates_without_consuming_untouched_drag_sessions() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut input = input_from_config(&config); @@ -148,7 +150,7 @@ fn persistence_barrier_blocks_updates_without_consuming_untouched_drag_sessions( #[test] fn relevant_reload_aborts_item_and_position_previews_without_restoring_old_seed() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config_a = Config::default(); let mut input = input_from_config(&config_a); @@ -164,7 +166,7 @@ fn relevant_reload_aborts_item_and_position_previews_without_restoring_old_seed( assert!(input.start_toolbar_item_drag(ToolbarItemOrderGroup::TopTools, ids::TOP_TOOL_PEN,)); assert!(input.drag_toolbar_item_over(ToolbarItemOrderGroup::TopTools, 5)); - let mut config_b = config_a.clone(); + let mut config_b = config_a; assert!(config_b.ui.toolbar.items.move_item_to_index( ToolbarItemOrderGroup::TopTools, ids::TOP_TOOL_PEN, @@ -179,8 +181,8 @@ fn relevant_reload_aborts_item_and_position_previews_without_restoring_old_seed( .ordered_ids(ToolbarItemOrderGroup::TopTools) .to_vec(); let refresh = runtime.refresh_config_seeds( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &config_b, &mut input, &mut positions, @@ -210,8 +212,8 @@ fn relevant_reload_aborts_item_and_position_previews_without_restoring_old_seed( config_c.ui.toolbar.top_offset = 100.0; config_c.ui.toolbar.top_offset_y = 101.0; let refresh = runtime.refresh_config_seeds( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &config_c, &mut input, &mut positions, @@ -227,7 +229,7 @@ fn relevant_reload_aborts_item_and_position_previews_without_restoring_old_seed( #[test] fn unrelated_position_reload_preserves_preview_and_cancel_only_restores_its_scope() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config_a = Config::default(); let original_top = ( @@ -244,8 +246,8 @@ fn unrelated_position_reload_preserves_preview_and_cancel_only_restores_its_scop config_b.ui.toolbar.top_minimized = !config_b.ui.toolbar.top_minimized; let refresh = runtime.refresh_config_seeds( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &config_b, &mut input, &mut positions, @@ -266,7 +268,7 @@ fn unrelated_position_reload_preserves_preview_and_cancel_only_restores_its_scop #[test] fn release_during_barrier_is_consumed_once_and_never_replayed() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut input = input_from_config(&config); @@ -302,7 +304,7 @@ fn release_during_barrier_is_consumed_once_and_never_replayed() { id: reset.id, error: RuntimeStateIoError::new("test reset failure"), active: Some(RuntimeStateSourceObservation::missing( - reset.expected_source.clone(), + reset.expected_source, )), recovery_artifacts: Vec::new(), path_effect: RuntimeStateFailurePathEffect::Known( @@ -312,8 +314,8 @@ fn release_during_barrier_is_consumed_once_and_never_replayed() { let drain = runtime.drain_writer_completions(); assert_eq!(drain.rollbacks.len(), 1); apply_toolbar_runtime_rollback( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, &drain.rollbacks[0], @@ -341,7 +343,7 @@ fn release_during_barrier_is_consumed_once_and_never_replayed() { #[test] fn external_source_conflict_rebuilds_live_toolbar_from_external_authority() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); assert!(config.ui.toolbar.top_pinned); @@ -396,8 +398,8 @@ fn external_source_conflict_rebuilds_live_toolbar_from_external_authority() { assert!(drain.rebuild_live); assert!(drain.rollbacks.is_empty()); runtime.apply_live_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, ); diff --git a/src/backend/wayland/runtime_ui_state/tests/layout_state.rs b/src/backend/wayland/runtime_ui_state/tests/layout_state.rs index 4a3bc16a7..03c9838af 100644 --- a/src/backend/wayland/runtime_ui_state/tests/layout_state.rs +++ b/src/backend/wayland/runtime_ui_state/tests/layout_state.rs @@ -1,9 +1,11 @@ +use crate::test_temp; + use super::*; #[test] fn committed_top_drag_writes_runtime_state_and_leaves_config_untouched() { const AUTHORED: &[u8] = b"# authored config bytes stay exact\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); fs::write(&config_path, AUTHORED).unwrap(); @@ -20,7 +22,7 @@ fn committed_top_drag_writes_runtime_state_and_leaves_config_untouched() { assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); assert_eq!( - stored_position(&runtime, InteractionSeedTarget::TopPosition), + stored_position(&runtime, &InteractionSeedTarget::TopPosition), Some((42.5, -7.25)) ); assert_eq!(fs::read(&config_path).unwrap(), AUTHORED); @@ -38,7 +40,7 @@ fn committed_top_drag_writes_runtime_state_and_leaves_config_untouched() { #[test] fn a_drag_back_to_the_authored_position_deletes_its_override() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut runtime = test_runtime(&config, &runtime_path); @@ -49,14 +51,14 @@ fn a_drag_back_to_the_authored_position_deletes_its_override() { positions.top = (10.0, 11.0); runtime.finish_position_drag(true, positions); assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); - assert!(stored_position(&runtime, InteractionSeedTarget::TopPosition).is_some()); + assert!(stored_position(&runtime, &InteractionSeedTarget::TopPosition).is_some()); assert!(runtime.begin_position_drag(MoveDragKind::Top, positions)); positions.top = seeded.top; runtime.finish_position_drag(true, positions); assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); assert_eq!( - stored_position(&runtime, InteractionSeedTarget::TopPosition), + stored_position(&runtime, &InteractionSeedTarget::TopPosition), None, "an override equal to its seed is deleted, not stored" ); @@ -65,7 +67,7 @@ fn a_drag_back_to_the_authored_position_deletes_its_override() { #[test] fn an_authored_position_edit_drops_the_stale_drag_override() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config_a = Config::default(); let mut input = input_from_config(&config_a); @@ -77,7 +79,7 @@ fn an_authored_position_edit_drops_the_stale_drag_override() { runtime.finish_position_drag(true, positions); assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); assert_eq!( - stored_position(&runtime, InteractionSeedTarget::TopPosition), + stored_position(&runtime, &InteractionSeedTarget::TopPosition), Some((55.0, 66.0)) ); @@ -85,8 +87,8 @@ fn an_authored_position_edit_drops_the_stale_drag_override() { config_b.ui.toolbar.top_offset = 200.0; config_b.ui.toolbar.top_offset_y = 201.0; let refresh = runtime.refresh_config_seeds( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &config_b, &mut input, &mut positions, @@ -94,7 +96,7 @@ fn an_authored_position_edit_drops_the_stale_drag_override() { assert!(refresh.applied); assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); assert_eq!( - stored_position(&runtime, InteractionSeedTarget::TopPosition), + stored_position(&runtime, &InteractionSeedTarget::TopPosition), None, "an explicit config edit wins over an older drag" ); @@ -105,7 +107,7 @@ fn an_authored_position_edit_drops_the_stale_drag_override() { #[test] fn a_drag_without_a_runtime_store_is_process_only_and_never_touches_config() { const AUTHORED: &[u8] = b"# authored config bytes stay exact\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); fs::write(&config_path, AUTHORED).unwrap(); let config = Config::default(); @@ -133,7 +135,7 @@ fn display_mode_cycle_is_runtime_owned_and_hidden_persists_as_full() { use crate::config::TopDisplayMode; const AUTHORED: &[u8] = b"# authored config bytes stay exact\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); fs::write(&config_path, AUTHORED).unwrap(); @@ -175,7 +177,7 @@ fn display_mode_cycle_is_runtime_owned_and_hidden_persists_as_full() { fn a_stored_display_mode_is_restored_at_startup_over_the_config_seed() { use crate::config::TopDisplayMode; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut input = input_from_config(&config); @@ -192,8 +194,8 @@ fn a_stored_display_mode_is_restored_at_startup_over_the_config_seed() { ); let restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); assert_eq!( @@ -203,15 +205,15 @@ fn a_stored_display_mode_is_restored_at_startup_over_the_config_seed() { } #[test] fn a_display_mode_change_during_presenter_mode_stores_the_pre_presenter_value() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, }; use crate::config::{PresenterToolbarMode, TopDisplayMode}; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut input = input_from_config(&config); @@ -262,7 +264,7 @@ fn a_display_mode_change_during_presenter_mode_stores_the_pre_presenter_value() fn an_authored_display_mode_edit_drops_the_stale_cycle_override() { use crate::config::TopDisplayMode; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config_a = Config::default(); let mut input = input_from_config(&config_a); @@ -279,8 +281,8 @@ fn an_authored_display_mode_edit_drops_the_stale_cycle_override() { let mut config_b = config_a; config_b.ui.toolbar.top_display_mode = TopDisplayMode::Micro; let refresh = runtime.refresh_config_seeds( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &config_b, &mut input, &mut positions, diff --git a/src/backend/wayland/runtime_ui_state/tests/preference_actions.rs b/src/backend/wayland/runtime_ui_state/tests/preference_actions.rs index f7ecf1e0d..b08545f24 100644 --- a/src/backend/wayland/runtime_ui_state/tests/preference_actions.rs +++ b/src/backend/wayland/runtime_ui_state/tests/preference_actions.rs @@ -1,3 +1,6 @@ +use crate::config::ToolbarItemVisibilitySetting; +use crate::{config, test_temp}; + use super::*; /// The click highlight and its tool ring persist together, from the toolbar @@ -7,7 +10,7 @@ use super::*; #[test] fn click_highlight_survives_restart_from_either_path() { const AUTHORED: &[u8] = b"# authored config bytes stay exact\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); fs::write(&config_path, AUTHORED).unwrap(); @@ -32,7 +35,7 @@ fn click_highlight_survives_restart_from_either_path() { assert!(input.set_click_highlight_enabled(enabled)); let rollback = click_highlight_values(previous_enabled, previous_ring).expect("valid rollback"); let prepared = runtime - .begin_toolbar_mutation_with_rollback(target, rollback) + .begin_toolbar_mutation_with_rollback(target, &rollback) .expect("keyboard permit"); let finish = runtime.finish_toolbar_mutation(prepared, true, &input); assert!(matches!(finish, ToolbarRuntimeFinish::KeepPreview)); @@ -42,8 +45,8 @@ fn click_highlight_survives_restart_from_either_path() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); @@ -60,7 +63,7 @@ fn click_highlight_survives_restart_from_either_path() { #[test] fn keyboard_only_chrome_toggles_survive_restart_without_touching_config() { const AUTHORED: &[u8] = b"# authored config bytes stay exact\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); fs::write(&config_path, AUTHORED).unwrap(); @@ -105,7 +108,7 @@ fn keyboard_only_chrome_toggles_survive_restart_without_touching_config() { let rollback = RuntimeUiMutationValues::one(seed, InteractionSeedValue::Bool(previous)) .expect("valid rollback"); let prepared = runtime - .begin_toolbar_mutation_with_rollback(target, rollback) + .begin_toolbar_mutation_with_rollback(target, &rollback) .unwrap_or_else(|| panic!("{target:?} permit")); let finish = runtime.finish_toolbar_mutation(prepared, true, &input); assert!( @@ -120,8 +123,8 @@ fn keyboard_only_chrome_toggles_survive_restart_without_touching_config() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); @@ -259,7 +262,7 @@ fn a_rollback_restores_every_durable_chrome_preference() { ), ]; - let mut values = std::collections::BTreeMap::new(); + let mut values = BTreeMap::new(); for (target, read, _) in &bools { values.insert(target.clone(), InteractionSeedValue::Bool(read(&input))); } @@ -270,7 +273,7 @@ fn a_rollback_restores_every_durable_chrome_preference() { ); values.insert( InteractionSeedTarget::SectionVisibility(ToolbarSectionFlag::Presets), - InteractionSeedValue::Visibility(crate::config::ToolbarItemVisibilitySetting::Default), + InteractionSeedValue::Visibility(ToolbarItemVisibilitySetting::Default), ); for (_, _, flip) in &bools { @@ -282,8 +285,8 @@ fn a_rollback_restores_every_durable_chrome_preference() { input.apply_toolbar_event(section_toggle(ToolbarSectionFlag::Presets, false)); apply_toolbar_runtime_rollback( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, &PreviewRollbackSnapshot { @@ -302,11 +305,11 @@ fn a_rollback_restores_every_durable_chrome_preference() { } assert_eq!(input.toolbar_layout_mode(), original_layout); assert_eq!( - crate::config::item_visibility_setting( + config::item_visibility_setting( input.resolved_toolbar_items(), ToolbarSectionFlag::Presets.item_id() ), - crate::config::ToolbarItemVisibilitySetting::Default, + ToolbarItemVisibilitySetting::Default, "the section override was not rolled back" ); } diff --git a/src/backend/wayland/runtime_ui_state/tests/preferences.rs b/src/backend/wayland/runtime_ui_state/tests/preferences.rs index ace8bf574..d2a720091 100644 --- a/src/backend/wayland/runtime_ui_state/tests/preferences.rs +++ b/src/backend/wayland/runtime_ui_state/tests/preferences.rs @@ -1,3 +1,5 @@ +use crate::test_temp; + use super::*; /// Status-bar content is chrome the user arranges from the overlay's settings @@ -10,7 +12,7 @@ fn status_bar_content_survives_restart_without_touching_config() { use crate::config::StatusBarItem; const AUTHORED: &[u8] = b"# authored config bytes stay exact\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); fs::write(&config_path, AUTHORED).unwrap(); @@ -37,8 +39,8 @@ fn status_bar_content_survives_restart_without_touching_config() { .begin_toolbar_mutation(target, &input) .expect("item permit"); input.set_status_bar_item_visible_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), StatusBarItem::Size, false, ); @@ -52,8 +54,8 @@ fn status_bar_content_survives_restart_without_touching_config() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); @@ -79,7 +81,7 @@ fn status_bar_content_survives_restart_without_touching_config() { #[test] fn toolbar_preference_toggles_survive_restart_without_touching_config() { const AUTHORED: &[u8] = b"# authored config bytes stay exact\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); fs::write(&config_path, AUTHORED).unwrap(); @@ -147,8 +149,8 @@ fn toolbar_preference_toggles_survive_restart_without_touching_config() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); @@ -185,7 +187,7 @@ fn toolbar_preference_toggles_survive_restart_without_touching_config() { /// the persistence at the next unrelated refresh. #[test] fn a_seed_refresh_does_not_prune_persisted_preference_overrides() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("data/runtime-ui.toml"); let config = Config::default(); let mut input = input_from_config(&config); @@ -203,8 +205,8 @@ fn a_seed_refresh_does_not_prune_persisted_preference_overrides() { // Whatever else the run does, the seed baseline stays authored. let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; runtime.refresh_config_seeds( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &config, &mut input, &mut positions, @@ -215,8 +217,8 @@ fn a_seed_refresh_does_not_prune_persisted_preference_overrides() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); assert_eq!( @@ -235,7 +237,7 @@ fn section_visibility_survives_restart_without_touching_config() { use crate::config::{ToolbarSectionFlag, resolve_section_visibility}; const AUTHORED: &[u8] = b"# authored config bytes stay exact\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); fs::write(&config_path, AUTHORED).unwrap(); @@ -270,8 +272,8 @@ fn section_visibility_survives_restart_without_touching_config() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); @@ -306,7 +308,7 @@ fn toolbar_layout_mode_survives_restart_without_touching_config() { use crate::config::{ToolbarLayoutMode, ToolbarSectionFlag, resolve_section_visibility}; const AUTHORED: &[u8] = b"# authored config bytes stay exact\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); fs::write(&config_path, AUTHORED).unwrap(); @@ -341,8 +343,8 @@ fn toolbar_layout_mode_survives_restart_without_touching_config() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); diff --git a/src/backend/wayland/runtime_ui_state/tests/recovery.rs b/src/backend/wayland/runtime_ui_state/tests/recovery.rs index daec3fb33..2ca99df12 100644 --- a/src/backend/wayland/runtime_ui_state/tests/recovery.rs +++ b/src/backend/wayland/runtime_ui_state/tests/recovery.rs @@ -1,3 +1,8 @@ +use crate::config::ToolbarLayoutMode; +use crate::input::state::TopMenuState; +use crate::test_temp; +use crate::ui::toolbar::ToolbarItemCustomizeGroup; + use super::*; #[test] @@ -19,7 +24,7 @@ fn toolbar_seed_registry_covers_every_runtime_routed_target() { ] { assert!(seeds.get(&target).is_some(), "missing seed for {target:?}"); } - for flag in crate::config::ToolbarSectionFlag::ALL { + for flag in ToolbarSectionFlag::ALL { assert!( seeds .get(&InteractionSeedTarget::SectionVisibility(flag)) @@ -34,7 +39,7 @@ fn toolbar_seed_registry_covers_every_runtime_routed_target() { .is_some() ); } - for flag in crate::config::ToolbarSectionFlag::ALL { + for flag in ToolbarSectionFlag::ALL { assert!( seeds .get(&InteractionSeedTarget::ItemVisibility(flag.item_id())) @@ -54,8 +59,8 @@ fn toolbar_seed_registry_covers_every_runtime_routed_target() { #[test] fn toolbar_section_visibility_is_not_seeded_into_runtime_state() { let mut config = Config::default(); - config.ui.toolbar.layout_mode = crate::config::ToolbarLayoutMode::Regular; - config.ui.toolbar.items = crate::config::ToolbarItemsConfig::default(); + config.ui.toolbar.layout_mode = ToolbarLayoutMode::Regular; + config.ui.toolbar.items = ToolbarItemsConfig::default(); config.ui.toolbar.show_zoom_actions = false; let board_pin_seeds = board_pin_seeds_from_input(&input_from_config(&config)); @@ -63,7 +68,7 @@ fn toolbar_section_visibility_is_not_seeded_into_runtime_state() { assert!( seeds .get(&InteractionSeedTarget::ItemVisibility( - crate::config::ToolbarSectionFlag::ZoomActions.item_id(), + ToolbarSectionFlag::ZoomActions.item_id(), )) .is_none() ); @@ -71,7 +76,7 @@ fn toolbar_section_visibility_is_not_seeded_into_runtime_state() { #[test] fn runtime_rebuild_reuses_minimize_transition_cleanup() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut source = input_from_config(&config); @@ -88,34 +93,31 @@ fn runtime_rebuild_reuses_minimize_transition_cleanup() { assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); for menu in [ - crate::input::state::TopMenuState::ShapePicker, - crate::input::state::TopMenuState::TopOverflow, - crate::input::state::TopMenuState::CanvasPopover, - crate::input::state::TopMenuState::SessionPopover, - crate::input::state::TopMenuState::SettingsPopover, - crate::input::state::TopMenuState::PenFeelPanel, + TopMenuState::ShapePicker, + TopMenuState::TopOverflow, + TopMenuState::CanvasPopover, + TopMenuState::SessionPopover, + TopMenuState::SettingsPopover, + TopMenuState::PenFeelPanel, ] { let mut rebuilt = input_from_config(&config); rebuilt.test_set_toolbar_menu_state(menu, rebuilt.toolbar_top_popover_scroll()); rebuilt.test_set_toolbar_customization( true, - Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopTools), + Some(ToolbarItemCustomizeGroup::TopTools), false, ); let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; runtime.apply_live_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut rebuilt, &mut positions, ); assert!(rebuilt.toolbar_top_minimized()); - assert_eq!( - rebuilt.toolbar_top_menu(), - crate::input::state::TopMenuState::Closed - ); + assert_eq!(rebuilt.toolbar_top_menu(), TopMenuState::Closed); assert!(!rebuilt.toolbar_customize_items_open()); assert!(rebuilt.toolbar_customize_items_group().is_none()); } @@ -124,7 +126,7 @@ fn runtime_rebuild_reuses_minimize_transition_cleanup() { #[test] fn supported_runtime_reset_returns_live_state_to_configured_defaults() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut input = input_from_config(&config); @@ -162,8 +164,8 @@ fn supported_runtime_reset_returns_live_state_to_configured_defaults() { let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; runtime.apply_live_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, ); @@ -172,7 +174,7 @@ fn supported_runtime_reset_returns_live_state_to_configured_defaults() { #[test] fn successful_writer_cleanup_artifacts_reach_toolbar_diagnostics() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let artifact_path = temp.path().join("runtime-ui.wayscriber-recovery-test.toml"); let config = Config::default(); @@ -223,7 +225,7 @@ fn successful_writer_cleanup_artifacts_reach_toolbar_diagnostics() { #[test] fn unsupported_runtime_reset_requires_confirmation_and_preserves_exact_source() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let unsupported = b"version = 73\nfuture = 'preserve exactly'\n"; fs::write(&runtime_path, unsupported).unwrap(); @@ -252,7 +254,7 @@ fn unsupported_runtime_reset_requires_confirmation_and_preserves_exact_source() assert!( runtime.handle_persistence_lifecycle_event(&ToolbarEvent::ConfirmUnsupportedRuntimeUiReset) ); - let snapshot = wait_for_runtime_mode(&mut runtime, RuntimeUiPersistenceMode::Missing); + let snapshot = wait_for_runtime_mode(&mut runtime, &RuntimeUiPersistenceMode::Missing); assert!(!runtime_path.exists()); assert_eq!(snapshot.recovery_artifacts.len(), 1); assert_eq!( @@ -263,7 +265,7 @@ fn unsupported_runtime_reset_requires_confirmation_and_preserves_exact_source() #[test] fn invalid_runtime_reset_keeps_the_incident_handle_paired_with_confirmation() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let invalid = b"this is not = valid = toml\n"; fs::write(&runtime_path, invalid).unwrap(); @@ -285,7 +287,7 @@ fn invalid_runtime_reset_keeps_the_incident_handle_paired_with_confirmation() { ); wait_for_runtime_mode( &mut runtime, - RuntimeUiPersistenceMode::AwaitingInvalidResetConfirmation, + &RuntimeUiPersistenceMode::AwaitingInvalidResetConfirmation, ); assert!( runtime @@ -304,14 +306,14 @@ fn invalid_runtime_reset_keeps_the_incident_handle_paired_with_confirmation() { ); wait_for_runtime_mode( &mut runtime, - RuntimeUiPersistenceMode::AwaitingInvalidResetConfirmation, + &RuntimeUiPersistenceMode::AwaitingInvalidResetConfirmation, ); assert!( runtime.handle_persistence_lifecycle_event( &ToolbarEvent::ConfirmPreserveInvalidRuntimeUiReset ) ); - let snapshot = wait_for_runtime_mode(&mut runtime, RuntimeUiPersistenceMode::Missing); + let snapshot = wait_for_runtime_mode(&mut runtime, &RuntimeUiPersistenceMode::Missing); assert!(!runtime_path.exists()); assert_eq!(snapshot.recovery_artifacts.len(), 1); assert_eq!(fs::read(&snapshot.recovery_artifacts[0]).unwrap(), invalid); @@ -319,7 +321,7 @@ fn invalid_runtime_reset_keeps_the_incident_handle_paired_with_confirmation() { #[test] fn cancelling_read_only_recovery_returns_the_same_incident_to_the_actor() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let invalid = b"not valid runtime state"; fs::write(&runtime_path, invalid).unwrap(); @@ -333,7 +335,7 @@ fn cancelling_read_only_recovery_returns_the_same_incident_to_the_actor() { ); assert!(runtime.has_retained_recovery_client()); assert!(runtime.handle_persistence_lifecycle_event(&ToolbarEvent::CancelRuntimeUiRecovery)); - wait_for_runtime_mode(&mut runtime, RuntimeUiPersistenceMode::Unhealthy); + wait_for_runtime_mode(&mut runtime, &RuntimeUiPersistenceMode::Unhealthy); assert!(!runtime.has_retained_recovery_client()); assert_eq!(fs::read(&runtime_path).unwrap(), invalid); @@ -347,13 +349,13 @@ fn cancelling_read_only_recovery_returns_the_same_incident_to_the_actor() { ); wait_for_runtime_mode( &mut runtime, - RuntimeUiPersistenceMode::AwaitingInvalidResetConfirmation, + &RuntimeUiPersistenceMode::AwaitingInvalidResetConfirmation, ); } #[test] fn cancelling_read_only_recovery_rebuilds_a_staged_seed_reload() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); fs::write(&runtime_path, b"not valid runtime state").unwrap(); let config_a = Config::default(); @@ -374,8 +376,8 @@ fn cancelling_read_only_recovery_rebuilds_a_staged_seed_reload() { let mut config_b = config_a; config_b.ui.toolbar.top_pinned = false; let refresh = runtime.refresh_config_seeds( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &config_b, &mut input, &mut positions, @@ -393,8 +395,8 @@ fn cancelling_read_only_recovery_rebuilds_a_staged_seed_reload() { "synchronous cancellation must publish the staged live authority" ); runtime.apply_live_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, ); @@ -406,11 +408,11 @@ fn cancelling_read_only_recovery_rebuilds_a_staged_seed_reload() { fn runtime_toolbar_routes_leave_authored_config_bytes_exactly_unchanged() { const AUTHORED: &[u8] = b"# keep this formatting and comment\n[ui.toolbar]\ntop_pinned = true\n"; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); fs::write(&config_path, AUTHORED).unwrap(); - let config: Config = toml::from_str(std::str::from_utf8(AUTHORED).unwrap()).unwrap(); + let config: Config = toml::from_str(str::from_utf8(AUTHORED).unwrap()).unwrap(); let mut input = input_from_config(&config); let mut runtime = test_runtime(&config, &runtime_path); @@ -434,9 +436,9 @@ fn runtime_toolbar_routes_leave_authored_config_bytes_exactly_unchanged() { } ToolbarRuntimeUiPersistenceTarget::TopDisplayMode => { input.set_top_display_mode_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), - crate::config::TopDisplayMode::Micro, + &UiTextEngine::default(), + &TextMeasurer::default(), + TopDisplayMode::Micro, ); } _ => unreachable!(), diff --git a/src/backend/wayland/runtime_ui_state/tests/text_geometry.rs b/src/backend/wayland/runtime_ui_state/tests/text_geometry.rs index 79f5bd479..1f417171a 100644 --- a/src/backend/wayland/runtime_ui_state/tests/text_geometry.rs +++ b/src/backend/wayland/runtime_ui_state/tests/text_geometry.rs @@ -1,3 +1,5 @@ +use crate::test_temp; + use super::*; use crate::config::{StatusBarItem, StatusBarStyle, StatusPosition}; use crate::ui::StatusHudSegmentKind; @@ -5,7 +7,7 @@ use crate::ui_text::UiTextEngine; #[test] fn preference_reapply_and_rollback_refresh_text_geometry_before_another_frame() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut config = Config::default(); config .ui @@ -13,14 +15,14 @@ fn preference_reapply_and_rollback_refresh_text_geometry_before_another_frame() let engine = UiTextEngine::default(); let mut input = input_from_config(&config); let mut runtime = controller_only_runtime(&config, &temp.path().join("runtime-ui.toml")); - runtime.apply_startup_state(&engine, &crate::draw::TextMeasurer::default(), &mut input); + runtime.apply_startup_state(&engine, &TextMeasurer::default(), &mut input); assert!( input.status_hud_layout().is_none(), "startup has no previous frame dimensions" ); input.update_status_hud_layout_for_pointer_with_resources( crate::input::state::InputTextResources { - measurer: &crate::draw::TextMeasurer::default(), + measurer: &TextMeasurer::default(), ui_engine: &engine, }, StatusPosition::BottomLeft, @@ -50,7 +52,7 @@ fn preference_reapply_and_rollback_refresh_text_geometry_before_another_frame() runtime .refresh_config_seeds( &engine, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &config, &mut input, &mut positions @@ -77,7 +79,7 @@ fn preference_reapply_and_rollback_refresh_text_geometry_before_another_frame() }; apply_toolbar_runtime_rollback( &engine, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut input, &mut positions, &rollback, @@ -94,7 +96,7 @@ fn preference_reapply_and_rollback_refresh_text_geometry_before_another_frame() assert_ne!(format!("{:?}", input.status_hud_layout()), hidden_geometry); runtime.apply_live_state( &engine, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut input, &mut positions, ); diff --git a/src/backend/wayland/runtime_ui_state/tests/visibility.rs b/src/backend/wayland/runtime_ui_state/tests/visibility.rs index 5810d88ed..577c19fd0 100644 --- a/src/backend/wayland/runtime_ui_state/tests/visibility.rs +++ b/src/backend/wayland/runtime_ui_state/tests/visibility.rs @@ -1,3 +1,5 @@ +use crate::test_temp; + use super::*; /// The keyboard visibility toggle (F9) persists through the same pin wire @@ -6,8 +8,8 @@ use super::*; /// exactly what the toggle left on screen. #[test] fn keyboard_visibility_toggle_persists_both_pins_and_startup_hides_the_toolbar() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -16,7 +18,7 @@ fn keyboard_visibility_toggle_persists_both_pins_and_startup_hides_the_toolbar() use crate::domain::Action; use crate::input::state::PendingToolbarPersistence; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); assert!(config.ui.toolbar.top_pinned); @@ -44,7 +46,7 @@ fn keyboard_visibility_toggle_persists_both_pins_and_startup_hides_the_toolbar() let prepared = runtime .begin_toolbar_mutation_with_rollback( ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility, - rollback, + &rollback, ) .expect("visibility permit"); assert!(matches!( @@ -58,7 +60,7 @@ fn keyboard_visibility_toggle_persists_both_pins_and_startup_hides_the_toolbar() ); assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); assert_eq!( - stored_bool(&runtime, InteractionSeedTarget::TopPinned), + stored_bool(&runtime, &InteractionSeedTarget::TopPinned), Some(false) ); runtime.shutdown_blocking(); @@ -67,8 +69,8 @@ fn keyboard_visibility_toggle_persists_both_pins_and_startup_hides_the_toolbar() assert!(restarted_input.toolbar_visible()); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); assert!(!restarted_input.toolbar_top_pinned()); @@ -92,8 +94,8 @@ fn a_rolled_back_hide_toggle_restores_live_visibility_from_the_pins() { input.set_toolbar_top_pinned(false); apply_toolbar_runtime_rollback( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, &pins_rollback(true), @@ -119,8 +121,8 @@ fn a_rolled_back_show_toggle_re_hides_the_toolbar() { input.set_toolbar_top_pinned(true); apply_toolbar_runtime_rollback( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, &pins_rollback(false), @@ -152,8 +154,8 @@ fn a_rolled_back_pin_button_keeps_a_visible_unpinned_toolbar_visible() { }; apply_toolbar_runtime_rollback( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, &rollback, @@ -171,7 +173,7 @@ fn a_rolled_back_pin_button_keeps_a_visible_unpinned_toolbar_visible() { /// and applying it restores the pre-toggle screen, not just the pins. #[test] fn visibility_toggle_rollback_through_a_failed_reset_restores_the_screen() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); assert!(config.ui.toolbar.top_pinned); @@ -187,7 +189,7 @@ fn visibility_toggle_rollback_through_a_failed_reset_restores_the_screen() { let prepared = runtime .begin_toolbar_mutation_with_rollback( ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility, - rollback, + &rollback, ) .expect("visibility permit"); assert!(input.set_toolbar_visible(false)); @@ -208,7 +210,7 @@ fn visibility_toggle_rollback_through_a_failed_reset_restores_the_screen() { id: reset.id, error: RuntimeStateIoError::new("test reset failure"), active: Some(RuntimeStateSourceObservation::missing( - reset.expected_source.clone(), + reset.expected_source, )), recovery_artifacts: Vec::new(), path_effect: RuntimeStateFailurePathEffect::Known( @@ -218,8 +220,8 @@ fn visibility_toggle_rollback_through_a_failed_reset_restores_the_screen() { let drain = runtime.drain_writer_completions(); assert_eq!(drain.rollbacks.len(), 1); apply_toolbar_runtime_rollback( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, &drain.rollbacks[0], @@ -241,8 +243,8 @@ fn visibility_toggle_rollback_through_a_failed_reset_restores_the_screen() { /// instead, and the take's no-op filter then drops the entry as moot. #[test] fn a_barrier_defers_queued_visibility_persistence_instead_of_dropping_it() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -251,7 +253,7 @@ fn a_barrier_defers_queued_visibility_persistence_instead_of_dropping_it() { use crate::domain::Action; use crate::input::state::PendingToolbarPersistence; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); assert!(config.ui.toolbar.top_pinned); @@ -276,7 +278,7 @@ fn a_barrier_defers_queued_visibility_persistence_instead_of_dropping_it() { runtime .begin_toolbar_mutation_with_rollback( ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility, - pins_rollback_values(true), + &pins_rollback_values(true), ) .is_none(), "the barrier refuses new mutations, which is why the drain defers" @@ -315,7 +317,7 @@ fn a_barrier_defers_queued_visibility_persistence_instead_of_dropping_it() { let prepared = runtime .begin_toolbar_mutation_with_rollback( ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility, - pins_rollback_values(true), + &pins_rollback_values(true), ) .expect("visibility permit after the barrier resolves"); assert!(matches!( @@ -328,7 +330,7 @@ fn a_barrier_defers_queued_visibility_persistence_instead_of_dropping_it() { "the deferred toggle still settles through one accepted revision" ); assert_eq!( - stored_bool(&runtime, InteractionSeedTarget::TopPinned), + stored_bool(&runtime, &InteractionSeedTarget::TopPinned), Some(false) ); } @@ -342,8 +344,8 @@ fn a_barrier_defers_queued_visibility_persistence_instead_of_dropping_it() { /// not the pre-toggle pins. #[test] fn an_exit_during_an_active_reset_barrier_still_lands_the_deferred_toggle() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -352,7 +354,7 @@ fn an_exit_during_an_active_reset_barrier_still_lands_the_deferred_toggle() { use crate::domain::Action; use crate::input::state::PendingToolbarPersistence; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); assert!(config.ui.toolbar.top_pinned); @@ -378,8 +380,8 @@ fn an_exit_during_an_active_reset_barrier_still_lands_the_deferred_toggle() { assert!(drain.rollbacks.is_empty()); if drain.rebuild_live { runtime.apply_live_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, ); @@ -396,7 +398,7 @@ fn an_exit_during_an_active_reset_barrier_still_lands_the_deferred_toggle() { let prepared = runtime .begin_toolbar_mutation_with_rollback( ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility, - pins_rollback_values(true), + &pins_rollback_values(true), ) .expect("visibility permit after teardown settled the barrier"); assert!(matches!( @@ -409,8 +411,8 @@ fn an_exit_during_an_active_reset_barrier_still_lands_the_deferred_toggle() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); assert!(!restarted_input.toolbar_top_pinned()); diff --git a/src/backend/wayland/runtime_ui_state/tests/visibility_recovery.rs b/src/backend/wayland/runtime_ui_state/tests/visibility_recovery.rs index bcf8481cd..b58da63a8 100644 --- a/src/backend/wayland/runtime_ui_state/tests/visibility_recovery.rs +++ b/src/backend/wayland/runtime_ui_state/tests/visibility_recovery.rs @@ -1,3 +1,5 @@ +use crate::test_temp; + use super::*; /// The recovery twin of the teardown race above: a transient write failure @@ -10,8 +12,8 @@ use super::*; /// exit-time screen. #[test] fn an_exit_during_retry_pending_recovery_still_lands_the_deferred_toggle() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -21,7 +23,7 @@ fn an_exit_during_retry_pending_recovery_still_lands_the_deferred_toggle() { use crate::input::state::PendingToolbarPersistence; use std::os::unix::fs::PermissionsExt; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let store_dir = temp.path().join("store"); fs::create_dir(&store_dir).unwrap(); let runtime_path = store_dir.join("runtime-ui.toml"); @@ -33,7 +35,7 @@ fn an_exit_during_retry_pending_recovery_still_lands_the_deferred_toggle() { // A transient failure: the store directory briefly refuses writes. fs::set_permissions(&store_dir, fs::Permissions::from_mode(0o555)).unwrap(); commit_display_mode(&mut runtime, &mut input, TopDisplayMode::Micro); - wait_for_runtime_mode(&mut runtime, RuntimeUiPersistenceMode::Unhealthy); + wait_for_runtime_mode(&mut runtime, &RuntimeUiPersistenceMode::Unhealthy); assert!(runtime.mutation_barrier_active()); fs::set_permissions(&store_dir, fs::Permissions::from_mode(0o755)).unwrap(); @@ -66,7 +68,7 @@ fn an_exit_during_retry_pending_recovery_still_lands_the_deferred_toggle() { let prepared = runtime .begin_toolbar_mutation_with_rollback( ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility, - pins_rollback_values(true), + &pins_rollback_values(true), ) .expect("visibility permit after recovery settled"); assert!(matches!( @@ -79,8 +81,8 @@ fn an_exit_during_retry_pending_recovery_still_lands_the_deferred_toggle() { let mut restarted_input = input_from_config(&config); let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut restarted_input, ); assert_eq!( @@ -104,8 +106,8 @@ fn an_exit_during_retry_pending_recovery_still_lands_the_deferred_toggle() { /// screen agreeing with the rolled-back pins. #[test] fn a_deferred_hide_rollback_lands_in_the_presenter_restore_snapshot() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -125,8 +127,8 @@ fn a_deferred_hide_rollback_lands_in_the_presenter_restore_snapshot() { assert!(input.presenter_mode_active()); apply_toolbar_runtime_rollback( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, &pins_rollback(true), @@ -151,8 +153,8 @@ fn a_deferred_hide_rollback_lands_in_the_presenter_restore_snapshot() { /// screen agreeing with the rolled-back pins. #[test] fn a_deferred_hide_rollback_lands_in_the_focus_mode_snapshot() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -182,8 +184,8 @@ fn a_deferred_hide_rollback_lands_in_the_focus_mode_snapshot() { assert!(input.focus_mode_active()); apply_toolbar_runtime_rollback( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, &pins_rollback(true), @@ -207,8 +209,8 @@ fn a_deferred_hide_rollback_lands_in_the_focus_mode_snapshot() { /// toolbar flags and its exit writes the snapshot back). #[test] fn a_deferred_hide_rollback_lands_in_the_light_mode_snapshot() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -230,8 +232,8 @@ fn a_deferred_hide_rollback_lands_in_the_light_mode_snapshot() { assert!(input.light_mode_active()); apply_toolbar_runtime_rollback( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), &mut input, &mut positions, &pins_rollback(true), diff --git a/src/backend/wayland/runtime_ui_state/wayland.rs b/src/backend/wayland/runtime_ui_state/wayland.rs index c109b4144..d1c371bf4 100644 --- a/src/backend/wayland/runtime_ui_state/wayland.rs +++ b/src/backend/wayland/runtime_ui_state/wayland.rs @@ -1,9 +1,18 @@ +use crate::config::TopDisplayMode; +use crate::input::boards::PendingBoardRuntimeUiAction; + use super::super::state::{MoveDragKind, WaylandState}; -#[cfg(not(test))] +use super::live_state::top_display_mode_values; +use super::rollback::apply_toolbar_runtime_rollback; use super::seed_refresh::{ RuntimeUiSeedRefresh, SeedRefreshContext, refresh_runtime_ui_config_seeds, }; -use super::*; +use super::{ToolbarPositionSnapshot, ToolbarRuntimeFinish, ToolbarRuntimeState}; +use crate::config::ToolbarItemOrderGroup; +use crate::runtime_ui_state::{ + InteractionSeedTarget, InteractionSeedValue, RuntimeUiMutationValues, +}; +use crate::ui::toolbar::model::ToolbarRuntimeUiPersistenceTarget; impl RuntimeUiSeedRefresh for WaylandState { fn seed_refresh_context(&mut self) -> SeedRefreshContext<'_> { SeedRefreshContext { @@ -140,7 +149,7 @@ impl WaylandState { /// travels with the pending action and supplies the preview's rollback. pub(in crate::backend::wayland) fn persist_toolbar_display_mode( &mut self, - previous: crate::config::TopDisplayMode, + previous: TopDisplayMode, ) { let target = ToolbarRuntimeUiPersistenceTarget::TopDisplayMode; let rollback = match top_display_mode_values(previous, &self.input_state) { @@ -156,7 +165,7 @@ impl WaylandState { let Some(runtime) = self.preferences.runtime_ui_mut().state_mut() else { return; }; - let Some(prepared) = runtime.begin_toolbar_mutation_with_rollback(target, rollback) else { + let Some(prepared) = runtime.begin_toolbar_mutation_with_rollback(target, &rollback) else { return; }; let finish = runtime.finish_toolbar_mutation(prepared, true, &self.input_state); @@ -187,7 +196,7 @@ impl WaylandState { let Some(runtime) = self.preferences.runtime_ui_mut().state_mut() else { return; }; - let Some(prepared) = runtime.begin_toolbar_mutation_with_rollback(target, rollback) else { + let Some(prepared) = runtime.begin_toolbar_mutation_with_rollback(target, &rollback) else { return; }; let finish = runtime.finish_toolbar_mutation(prepared, true, &self.input_state); @@ -214,7 +223,7 @@ impl WaylandState { let Some(runtime) = self.preferences.runtime_ui_mut().state_mut() else { return; }; - let Some(prepared) = runtime.begin_toolbar_mutation_with_rollback(target, rollback) else { + let Some(prepared) = runtime.begin_toolbar_mutation_with_rollback(target, &rollback) else { return; }; let finish = runtime.finish_toolbar_mutation(prepared, true, &self.input_state); @@ -248,7 +257,7 @@ impl WaylandState { let Some(runtime) = self.preferences.runtime_ui_mut().state_mut() else { return; }; - let Some(prepared) = runtime.begin_toolbar_mutation_with_rollback(target, rollback) else { + let Some(prepared) = runtime.begin_toolbar_mutation_with_rollback(target, &rollback) else { return; }; let finish = runtime.finish_toolbar_mutation(prepared, true, &self.input_state); @@ -354,7 +363,7 @@ impl WaylandState { pub(in crate::backend::wayland) fn apply_board_runtime_ui_action( &mut self, - action: crate::input::boards::PendingBoardRuntimeUiAction, + action: PendingBoardRuntimeUiAction, ) { use crate::input::boards::PendingBoardRuntimeUiAction; diff --git a/src/backend/wayland/session.rs b/src/backend/wayland/session.rs index d696d037f..ec58188b6 100644 --- a/src/backend/wayland/session.rs +++ b/src/backend/wayland/session.rs @@ -3,10 +3,15 @@ //! Tracks the current session options and whether a snapshot has been loaded //! so WaylandState can coordinate persistence without storing extra fields. +use std::mem; + use anyhow::{Result, anyhow}; +#[cfg(test)] use crate::input::InputState; -use crate::session::{self as stored_session, LoadSnapshotOutcome, SessionOptions}; +#[cfg(test)] +use crate::session::LoadSnapshotOutcome; +use crate::session::{self as stored_session, SessionOptions}; use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; @@ -103,7 +108,7 @@ impl SessionState { /// Allow one synchronous initial load from the surface callback. Drawing /// may already exist; the caller must retain it instead of replacing it. pub(in crate::backend::wayland) fn begin_initial_load(&mut self) -> bool { - !std::mem::replace(&mut self.initial_load_attempted, true) + !mem::replace(&mut self.initial_load_attempted, true) } /// Whether the active logical session source has been resolved this run. @@ -389,7 +394,10 @@ impl SessionState { request_id: RequestId, window: DirtyWindow, ) { - debug_assert!(self.in_flight_autosave.is_none()); + debug_assert!( + self.in_flight_autosave.is_none(), + "submit one autosave at a time; the previous request is still in flight" + ); self.in_flight_autosave = Some(InFlightAutosave { request_id, window }); self.dirty = false; self.dirty_since = None; @@ -594,14 +602,19 @@ pub(in crate::backend::wayland) use home::HomeSession; pub(in crate::backend::wayland) use home::{ OutputSessionLoad, SessionHome, SessionLaunch, load_output_session, session_target, }; +pub(in crate::backend::wayland) use load_outcome::{ExpandedTooLarge, commit_output_load}; +#[cfg(test)] pub(in crate::backend::wayland) use load_outcome::{ - ExpandedTooLarge, apply_load_outcome, commit_output_load, replace_output_session_snapshot, + apply_load_outcome, replace_output_session_snapshot, }; pub(in crate::backend::wayland) use persistence::{ PersistenceCompletion, PersistenceController, PersistenceOperation, PersistenceOutcome, RequestId, SaveCompletion, SaveStrategy, SubmitFailure, }; +pub(super) use runtime::{ + ContentlessSaveCheck, has_session_artifact, should_skip_unloaded_contentless_save, +}; pub(in crate::backend::wayland) use runtime::{ QueuedSessionCommand, SessionCommand, SessionCommandAborted, SessionCommandReport, SessionCommandTransaction, SessionTransaction, TransactionStep, @@ -611,7 +624,6 @@ pub(in crate::backend::wayland) use runtime::{ RuntimeClearSessionReport, RuntimeClearToolStateReport, RuntimeOpenSessionReport, RuntimeSaveAsSessionReport, }; -pub(super) use runtime::{has_session_artifact, should_skip_unloaded_contentless_save}; pub(in crate::backend::wayland) use write_policy::SourceWriteRefused; #[cfg(test)] diff --git a/src/backend/wayland/session/driver.rs b/src/backend/wayland/session/driver.rs index 37ef489f1..6924e5d40 100644 --- a/src/backend/wayland/session/driver.rs +++ b/src/backend/wayland/session/driver.rs @@ -1,4 +1,7 @@ //! Live session-command orchestration, shared by Wayland and headless runtime tests. +use std::collections::VecDeque; +use std::mem; + use anyhow::{Result, anyhow}; use std::time::{Duration, Instant}; @@ -151,7 +154,7 @@ pub(in crate::backend::wayland) fn complete_session_command( transaction.command(), &anyhow!("explicit session completion identity mismatch"), ); - start_queued_commands(runtime, std::mem::take(&mut transaction.queued_commands)); + start_queued_commands(runtime, mem::take(&mut transaction.queued_commands)); return; } @@ -206,10 +209,7 @@ fn advance_session_command( } else { runtime.fail_session_command(transaction.command(), &error); } - start_queued_commands( - runtime, - std::mem::take(&mut transaction.queued_commands), - ); + start_queued_commands(runtime, mem::take(&mut transaction.queued_commands)); } } } @@ -226,7 +226,7 @@ fn advance_session_command( runtime.session_target_committed(); runtime.finish_session_command(*report); - start_queued_commands(runtime, std::mem::take(&mut transaction.queued_commands)); + start_queued_commands(runtime, mem::take(&mut transaction.queued_commands)); } Err(error) => { // Resave retained state if a clear may have changed disk, without postponing @@ -238,14 +238,14 @@ fn advance_session_command( .record_input_dirty(Instant::now(), needs_recovery); runtime.fail_session_command(transaction.command(), &error); - start_queued_commands(runtime, std::mem::take(&mut transaction.queued_commands)); + start_queued_commands(runtime, mem::take(&mut transaction.queued_commands)); } } } fn start_queued_commands( runtime: &mut impl SessionCommandRuntime, - mut commands: std::collections::VecDeque, + mut commands: VecDeque, ) { if let Some(pending) = runtime.pending_command().as_mut() { pending.queued_commands.extend(commands); @@ -425,7 +425,7 @@ pub(in crate::backend::wayland) fn persist_after_pending_commands Result, ) -> Result { if let Some(transaction) = runtime.pending_command().as_mut() { - let canceled = std::mem::take(&mut transaction.queued_commands) + let canceled = mem::take(&mut transaction.queued_commands) .into_iter() .map(|queued| { ( diff --git a/src/backend/wayland/session/driver/tests.rs b/src/backend/wayland/session/driver/tests.rs index 0c58b371c..e1d10c02c 100644 --- a/src/backend/wayland/session/driver/tests.rs +++ b/src/backend/wayland/session/driver/tests.rs @@ -1,28 +1,38 @@ +use std::fs::File; +use std::os::unix; +use std::path::Path; +use std::{fs, io, thread}; + +use flate2::write::GzEncoder; + use super::*; use crate::backend::wayland::backend::runtime_wake::RuntimeWakeSource; mod lifecycle_regressions; mod output; +use crate::backend::wayland::backend::event_loop::session_save::record_autosave_failure; +use crate::backend::wayland::runtime_ui_state::{ + RuntimeUiSeedRefresh, SeedRefreshContext, ToolbarRuntimeState, refresh_runtime_ui_config_seeds, +}; +use crate::backend::wayland::session::persistence::SubmitError; +use crate::backend::wayland::session::tests::{ + EnvGuard, add_line, loaded_line_x2, named_options, sample_snapshot, test_input_state, +}; use crate::backend::wayland::session::{ PersistenceOperation, RequestId, SaveStrategy, SessionState, - persistence::SubmitError, - tests::{EnvGuard, add_line, loaded_line_x2, named_options, sample_snapshot, test_input_state}, }; -use crate::backend::wayland::{ - backend::event_loop::session_save::record_autosave_failure, - runtime_ui_state::{ - RuntimeUiSeedRefresh, SeedRefreshContext, ToolbarRuntimeState, - refresh_runtime_ui_config_seeds, - }, - state::{ToolbarChrome, ToolbarDrag}, - toolbar::ToolbarSurfaceManager, -}; -use crate::{config::Config, draw::TextMeasurer, input::InputState, session as stored_session}; - -pub(in crate::backend::wayland::session) struct CommandRuntime<'a> { - pub input: &'a mut InputState, - measurer: &'a TextMeasurer, - pub session: &'a mut SessionState, +use crate::backend::wayland::state::{ToolbarChrome, ToolbarDrag}; +use crate::backend::wayland::toolbar::ToolbarSurfaceManager; +use crate::config::Config; +use crate::draw::{Shape, TextMeasurer}; +use crate::input::InputState; +use crate::ui_text::UiTextEngine; +use crate::{session as stored_session, test_temp}; + +pub(in crate::backend::wayland::session) struct CommandRuntime<'state> { + pub input: &'state mut InputState, + measurer: &'state TextMeasurer, + pub session: &'state mut SessionState, pub persistence: PersistenceController, pending: Option, config: Config, @@ -36,7 +46,7 @@ pub(in crate::backend::wayland::session) struct CommandRuntime<'a> { /// terminal reports published before it. pub committed_targets: Vec<(Option, usize)>, ui: Option, - ui_engine: crate::ui_text::UiTextEngine, + ui_engine: UiTextEngine, chrome: ToolbarChrome, drag: ToolbarDrag, toolbar: ToolbarSurfaceManager, @@ -44,11 +54,11 @@ pub(in crate::backend::wayland::session) struct CommandRuntime<'a> { autosave_failures: usize, } -impl<'a> CommandRuntime<'a> { +impl<'state> CommandRuntime<'state> { pub fn new( - input: &'a mut InputState, - measurer: &'a TextMeasurer, - session: &'a mut SessionState, + input: &'state mut InputState, + measurer: &'state TextMeasurer, + session: &'state mut SessionState, persistence: PersistenceController, ) -> Self { Self { @@ -64,7 +74,7 @@ impl<'a> CommandRuntime<'a> { queued_failures: Vec::new(), committed_targets: Vec::new(), ui: None, - ui_engine: crate::ui_text::UiTextEngine::default(), + ui_engine: UiTextEngine::default(), chrome: ToolbarChrome::new(true, (0.0, 0.0)), drag: ToolbarDrag::new(), toolbar: ToolbarSurfaceManager::new(), @@ -82,8 +92,8 @@ impl<'a> CommandRuntime<'a> { .ok_or_else(|| anyhow!("command did not publish a terminal report")) } - fn enable_ui_runtime(&mut self, path: &std::path::Path) { - let wake = RuntimeWakeSource::new().unwrap(); + fn enable_ui_runtime(&mut self, path: &Path) { + let wake = RuntimeWakeSource::try_new().unwrap(); self.ui = Some( ToolbarRuntimeState::start(&self.config, self.input, path, wake.handle()).unwrap(), ); @@ -232,7 +242,7 @@ fn admission_rejects_each_guard_without_submitting_or_retargeting() { Guard::ClearConfig, Guard::ToolsConfig, ] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = named_options(temp.path(), "current"); let mut input = test_input_state(); let mut session = SessionState::new(Some(options.clone())); @@ -316,7 +326,7 @@ fn admission_rejects_each_guard_without_submitting_or_retargeting() { #[test] fn explicit_command_waits_for_autosave_receipt_and_survives_dispatch_work() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = named_options(temp.path(), "current"); let mut input = test_input_state(); let mut session = SessionState::new(Some(options.clone())); @@ -364,7 +374,7 @@ fn live_completion_gate_rejects_a_different_request_identity() { let (persistence, worker) = PersistenceController::controlled_for_test(); let mut runtime = CommandRuntime::new(&mut input, &measurer, &mut session, persistence); // No active session makes Inspect complete inline; use a disk-bound command instead. - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = named_options(temp.path(), "current"); *runtime.session = SessionState::new(Some(options)); @@ -382,7 +392,7 @@ fn live_completion_gate_rejects_a_different_request_identity() { #[test] fn advance_observes_live_edits_and_finalizes_stale_destructive_work() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = named_options(temp.path(), "clear"); stored_session::save_snapshot(&sample_snapshot(), &options).unwrap(); let mut input = test_input_state(); @@ -409,7 +419,7 @@ fn advance_observes_live_edits_and_finalizes_stale_destructive_work() { #[test] fn deferred_submission_rejection_is_terminal() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = named_options(temp.path(), "current"); let mut input = test_input_state(); let mut session = SessionState::new(Some(options.clone())); @@ -462,7 +472,7 @@ impl CatalogOutcome { self, runtime: &CommandRuntime<'_>, target: &stored_session::SessionOptions, - root: &std::path::Path, + root: &Path, ) { assert!(runtime.pending.is_none()); assert!(runtime.errors.is_empty()); @@ -477,7 +487,7 @@ impl CatalogOutcome { assert_eq!(runtime.input.boards.active_frame().shapes.len(), 1); assert!(matches!( runtime.input.boards.active_frame().shapes[0].shape, - crate::draw::Shape::Line { x2: 42, .. } + Shape::Line { x2: 42, .. } )); assert_eq!(loaded_line_x2(target), 42); // Catalog I/O failure leaves the worker usable; rejected transport marks it unhealthy. @@ -489,9 +499,9 @@ impl CatalogOutcome { assert!( format!("{error:#}").contains("failed to create session catalog directory") ); - assert!(error.downcast_ref::().is_some()); + assert!(error.downcast_ref::().is_some()); assert_eq!( - std::fs::read(root.join("wayscriber")).unwrap(), + fs::read(root.join("wayscriber")).unwrap(), b"catalog blocked" ); } @@ -499,7 +509,7 @@ impl CatalogOutcome { let entries = stored_session::catalog::recent_sessions().unwrap(); assert_eq!(entries.len(), 1); assert!(stored_session::catalog::session_paths_match( - std::path::Path::new(&entries[0].path), + Path::new(&entries[0].path), &target.session_file_path() )); assert!(entries[0].last_opened_at_millis.is_some()); @@ -525,14 +535,14 @@ fn open_refreshes_consumer_seeds_before_catalog_work_and_at_completion() { CatalogOutcome::Failure, CatalogOutcome::Rejected, ] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "target"); stored_session::save_snapshot(&sample_snapshot(), &target).unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); if catalog == CatalogOutcome::Failure { // A regular file blocks catalog directory creation, without relying on uid or modes. - std::fs::write(temp.path().join("wayscriber"), b"catalog blocked").unwrap(); + fs::write(temp.path().join("wayscriber"), b"catalog blocked").unwrap(); } let mut input = test_input_state(); @@ -612,13 +622,13 @@ fn committed_open_announces_its_target_before_the_terminal_report() { CatalogOutcome::Failure, CatalogOutcome::Rejected, ] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "target"); stored_session::save_snapshot(&sample_snapshot(), &target).unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); if catalog == CatalogOutcome::Failure { - std::fs::write(temp.path().join("wayscriber"), b"catalog blocked").unwrap(); + fs::write(temp.path().join("wayscriber"), b"catalog blocked").unwrap(); } let mut input = test_input_state(); let mut session = SessionState::new(Some(current)); @@ -661,7 +671,7 @@ fn committed_open_announces_its_target_before_the_terminal_report() { #[test] fn committed_save_as_announces_its_target_before_the_terminal_report() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "target"); let mut input = test_input_state(); @@ -690,15 +700,12 @@ fn committed_save_as_announces_its_target_before_the_terminal_report() { runtime.reports.as_slice(), [SessionCommandReport::SaveAs(_)] )); - assert_eq!( - runtime.committed_targets, - [(Some(target.target.clone()), 0)] - ); + assert_eq!(runtime.committed_targets, [(Some(target.target), 0)]); } #[test] fn failed_open_and_save_as_announce_nothing() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let existing = named_options(temp.path(), "existing"); stored_session::save_snapshot(&sample_snapshot(), &existing).unwrap(); @@ -728,7 +735,7 @@ fn failed_open_and_save_as_announce_nothing() { } } -fn configured_home(base: &std::path::Path) -> stored_session::SessionOptions { +fn configured_home(base: &Path) -> stored_session::SessionOptions { let mut options = stored_session::SessionOptions::new(base.join("configured"), "home"); options.persist_transparent = true; options @@ -736,7 +743,7 @@ fn configured_home(base: &std::path::Path) -> stored_session::SessionOptions { #[test] fn returning_home_saves_the_current_session_then_loads_home_like_a_launch() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let home = configured_home(temp.path()); stored_session::save_snapshot(&sample_snapshot(), &home).unwrap(); @@ -772,53 +779,45 @@ fn returning_home_saves_the_current_session_then_loads_home_like_a_launch() { assert!(!runtime.session.is_dirty() && !runtime.input.is_session_dirty()); let shapes = &runtime.input.boards.active_frame().shapes; assert_eq!(shapes.len(), 1); - assert!(matches!( - shapes[0].shape, - crate::draw::Shape::Line { x2: 42, .. } - )); + assert!(matches!(shapes[0].shape, Shape::Line { x2: 42, .. })); // The commit was announced before the terminal report. assert_eq!( runtime.committed_targets.last(), - Some(&(Some(home.target.clone()), 0)) + Some(&(Some(home.target), 0)) ); } /// A home whose session file a save could not replace, of the kind `case` /// names, and the error that names it. -fn unsaveable_home( - base: &std::path::Path, - case: &str, -) -> (stored_session::SessionOptions, &'static str) { +fn unsaveable_home(base: &Path, case: &str) -> (stored_session::SessionOptions, &'static str) { let configured = configured_home(base); let saved = named_options(base, "saved"); stored_session::save_snapshot(&sample_snapshot(), &saved).unwrap(); match case { // A launch would start on an empty canvas here. "configured directory" => { - std::fs::create_dir_all(configured.session_file_path()).unwrap(); + fs::create_dir_all(configured.session_file_path()).unwrap(); (configured, "is a directory") } // A launch would restore the recovery copy, then fail every save. "configured directory beside a recovery copy" => { - std::fs::create_dir_all(configured.session_file_path()).unwrap(); - std::fs::copy(saved.session_file_path(), configured.recovery_file_path()).unwrap(); + fs::create_dir_all(configured.session_file_path()).unwrap(); + fs::copy(saved.session_file_path(), configured.recovery_file_path()).unwrap(); (configured, "is a directory") } // A launch would follow the link, then fail every save. "configured symlink" => { - std::fs::create_dir_all(&configured.base_dir).unwrap(); - std::os::unix::fs::symlink(saved.session_file_path(), configured.session_file_path()) - .unwrap(); + fs::create_dir_all(&configured.base_dir).unwrap(); + unix::fs::symlink(saved.session_file_path(), configured.session_file_path()).unwrap(); (configured, "is a symlink") } "expanded primary" => { use std::io::Read; - std::fs::create_dir_all(&configured.base_dir).unwrap(); - let file = std::fs::File::create(configured.session_file_path()).unwrap(); - let mut encoder = flate2::write::GzEncoder::new(file, flate2::Compression::fast()); - std::io::copy( - &mut std::io::repeat(b' ') - .take(stored_session::DEFAULT_MAX_EXPANDED_SESSION_BYTES + 1), + fs::create_dir_all(&configured.base_dir).unwrap(); + let file = File::create(configured.session_file_path()).unwrap(); + let mut encoder = GzEncoder::new(file, flate2::Compression::fast()); + io::copy( + &mut io::repeat(b' ').take(stored_session::DEFAULT_MAX_EXPANDED_SESSION_BYTES + 1), &mut encoder, ) .unwrap(); @@ -827,7 +826,7 @@ fn unsaveable_home( } "named directory" => { let named = named_options(base, "named-home"); - std::fs::create_dir(named.session_file_path()).unwrap(); + fs::create_dir(named.session_file_path()).unwrap(); (named, "directory") } _ => unreachable!(), @@ -843,7 +842,7 @@ fn a_home_that_cannot_be_loaded_leaves_the_current_session() { "named directory", "expanded primary", ] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let (home, expected) = unsaveable_home(temp.path(), case); let mut input = test_input_state(); @@ -871,17 +870,14 @@ fn a_home_that_cannot_be_loaded_leaves_the_current_session() { assert_eq!(runtime.session.options().unwrap().target, current.target); let shapes = &runtime.input.boards.active_frame().shapes; assert_eq!(shapes.len(), 1, "{case}"); - assert!(matches!( - shapes[0].shape, - crate::draw::Shape::Line { x2: 51, .. } - )); + assert!(matches!(shapes[0].shape, Shape::Line { x2: 51, .. })); assert!(runtime.committed_targets.is_empty()); } } #[test] fn returning_home_is_refused_when_the_canvas_changes_while_home_loads() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let home = configured_home(temp.path()); let mut input = test_input_state(); @@ -910,7 +906,7 @@ fn returning_home_is_refused_when_the_canvas_changes_while_home_loads() { #[test] fn returning_to_a_home_without_persistence_leaves_an_unsaved_empty_canvas() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let mut input = test_input_state(); add_line(&mut input, 51); @@ -938,7 +934,7 @@ fn returning_to_a_home_without_persistence_leaves_an_unsaved_empty_canvas() { #[test] fn clear_completion_refreshes_consumer_seeds() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = named_options(temp.path(), "clear"); options.autosave_enabled = true; let mut input = test_input_state(); @@ -990,7 +986,7 @@ fn clear_completion_refreshes_consumer_seeds() { fn shutdown_drains_save_as_and_open_at_every_disk_phase_before_final_save() { for (open, phases) in [(false, 2), (true, 4)] { for completed in 0..phases { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "target"); if open { @@ -1019,7 +1015,7 @@ fn shutdown_drains_save_as_and_open_at_every_disk_phase_before_final_save() { } assert!(runtime.pending.is_some()); - std::thread::scope(|scope| { + thread::scope(|scope| { scope.spawn(move || { for _ in completed..phases { worker.complete_next(); @@ -1065,7 +1061,7 @@ fn shutdown_drains_save_as_and_open_at_every_disk_phase_before_final_save() { #[test] fn shutdown_starts_ready_to_poll_open_and_save_as_after_an_autosave_receipt() { for open in [true, false] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "target"); if open { @@ -1100,7 +1096,7 @@ fn shutdown_starts_ready_to_poll_open_and_save_as_after_an_autosave_receipt() { assert_eq!(runtime.pending.as_ref().unwrap().request_id, None); assert!(runtime.reports.is_empty()); - std::thread::scope(|scope| { + thread::scope(|scope| { scope.spawn(move || { for _ in 0..if open { 3 } else { 2 } { worker.complete_next(); @@ -1132,7 +1128,7 @@ fn shutdown_starts_ready_to_poll_open_and_save_as_after_an_autosave_receipt() { fn shutdown_reports_open_and_save_as_phase_failures_before_final_persistence() { for (open, phases) in [(false, 2), (true, 4)] { for completed in 0..phases { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "target"); if open { @@ -1212,7 +1208,7 @@ fn shutdown_reports_open_and_save_as_phase_failures_before_final_persistence() { #[test] fn shutdown_cleans_up_pending_work_after_worker_disconnect_and_when_ready_to_poll() { for disconnect in [true, false] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = named_options(temp.path(), "current"); let mut input = test_input_state(); let mut session = SessionState::new(Some(options.clone())); @@ -1238,7 +1234,7 @@ fn shutdown_cleans_up_pending_work_after_worker_disconnect_and_when_ready_to_pol } else { worker.complete_next(); runtime.persistence.wait_for_completion().unwrap().unwrap(); - std::thread::scope(|scope| { + thread::scope(|scope| { scope.spawn(move || worker.complete_next()); persist_after_pending_commands(&mut runtime, |runtime| { assert!(runtime.pending.is_none()); diff --git a/src/backend/wayland/session/driver/tests/lifecycle_regressions.rs b/src/backend/wayland/session/driver/tests/lifecycle_regressions.rs index 5526b92b5..0a3b70045 100644 --- a/src/backend/wayland/session/driver/tests/lifecycle_regressions.rs +++ b/src/backend/wayland/session/driver/tests/lifecycle_regressions.rs @@ -1,12 +1,17 @@ +use crate::config::{Action, ToolbarItemOrderGroup, toolbar_item_ids}; +use crate::input::{DrawingState, MouseButton}; +use crate::toolbar_gtk::GtkToolbarKind; + use super::*; -use crate::backend::wayland::{runtime_ui_state::ToolbarPositionSnapshot, state::MoveDragKind}; +use crate::backend::wayland::runtime_ui_state::ToolbarPositionSnapshot; +use crate::backend::wayland::state::MoveDragKind; #[test] fn pending_destructive_commands_preserve_live_slider_edits_and_undo() { use crate::input::state::{PropertiesPanelHit, SelectionPropertyKind}; for open in [false, true] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = named_options(temp.path(), "current"); let target = named_options(temp.path(), "candidate"); stored_session::save_snapshot(&sample_snapshot(), &options).unwrap(); @@ -69,10 +74,7 @@ fn pending_destructive_commands_preserve_live_slider_edits_and_undo() { .active_frame() .shape(id) .expect("pending command must retain the edited shape"); - assert!(matches!( - retained.shape, - crate::draw::Shape::Line { thick: 50.0, .. } - )); + assert!(matches!(retained.shape, Shape::Line { thick: 50.0, .. })); assert_eq!( runtime.session.options().unwrap().session_file_path(), options.session_file_path() @@ -96,9 +98,9 @@ fn pending_destructive_commands_preserve_live_slider_edits_and_undo() { runtime.input.handle_action_with_resources( crate::input::state::InputTextResources { measurer: &measurer, - ui_engine: &crate::ui_text::UiTextEngine::default(), + ui_engine: &UiTextEngine::default(), }, - crate::config::Action::Undo, + Action::Undo, ); assert_eq!( runtime.input.boards.active_frame().shape(id).unwrap().shape, @@ -146,7 +148,7 @@ fn rejected_disk_clears_are_autosaved_after_an_uncommitted_gesture_is_canceled() fn assert_rejected_disk_clear_recovers_after_canceled_gesture(command: SessionCommand) { let clears_boards = matches!(command, SessionCommand::Clear); - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = named_options(temp.path(), "current"); options.autosave_enabled = true; options.autosave_idle = Duration::from_millis(10); @@ -173,18 +175,15 @@ fn assert_rejected_disk_clear_recovers_after_canceled_gesture(command: SessionCo runtime.input.on_mouse_press_with_canvas_and_resources( crate::input::state::InputTextResources { measurer: &measurer, - ui_engine: &crate::ui_text::UiTextEngine::default(), + ui_engine: &UiTextEngine::default(), }, - crate::input::MouseButton::Left, + MouseButton::Left, 500, 400, 500, 400, ); - assert!(matches!( - runtime.input.state, - crate::input::DrawingState::Drawing { .. } - )); + assert!(matches!(runtime.input.state, DrawingState::Drawing { .. })); assert!( !runtime.input.is_session_dirty(), "unfinished stroke has not committed" @@ -265,7 +264,7 @@ fn autosave_ownership_errors_do_not_publish_failures_or_delay_retry() { Receipt::WrongEpoch, Receipt::WriteFailure, ] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = named_options(temp.path(), "current"); options.autosave_enabled = true; options.autosave_idle = Duration::from_millis(1); @@ -324,7 +323,7 @@ fn autosave_ownership_errors_do_not_publish_failures_or_delay_retry() { #[test] fn shutdown_caller_retires_a_command_queued_behind_an_owned_failed_autosave() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "target"); stored_session::save_snapshot(&sample_snapshot(), &target).unwrap(); @@ -388,7 +387,7 @@ fn session_seed_refresh_aborts_changed_drags_and_publishes_live_chrome_positions } for drag in [Drag::Item, Drag::Builtin, Drag::Gtk] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = named_options(temp.path(), "current"); let mut input = test_input_state(); @@ -401,8 +400,8 @@ fn session_seed_refresh_aborts_changed_drags_and_publishes_live_chrome_positions runtime.enable_ui_runtime(&temp.path().join("runtime-ui.toml")); - let group = crate::config::ToolbarItemOrderGroup::TopTools; - let pen = crate::config::toolbar_item_ids::TOP_TOOL_PEN; + let group = ToolbarItemOrderGroup::TopTools; + let pen = toolbar_item_ids::TOP_TOOL_PEN; match drag { Drag::Item => { @@ -435,9 +434,7 @@ fn session_seed_refresh_aborts_changed_drags_and_publishes_live_chrome_positions .drag .begin_move(MoveDragKind::Top, (0.0, 0.0), false, (0.0, 0.0)); } else { - runtime - .drag - .begin_gtk_preview(crate::toolbar_gtk::GtkToolbarKind::Top, 0.0); + runtime.drag.begin_gtk_preview(GtkToolbarKind::Top, 0.0); } runtime.chrome.set_top_offset((80.0, 100.0)); } diff --git a/src/backend/wayland/session/driver/tests/output.rs b/src/backend/wayland/session/driver/tests/output.rs index 9364bf836..5371d0518 100644 --- a/src/backend/wayland/session/driver/tests/output.rs +++ b/src/backend/wayland/session/driver/tests/output.rs @@ -1,6 +1,9 @@ +use crate::draw::ShapeId; +use crate::input::MouseButton; + use super::*; -fn add_line(input: &mut InputState, x2: i32) -> crate::draw::ShapeId { +fn add_line(input: &mut InputState, x2: i32) -> ShapeId { let id = super::add_line(input, x2); input.mark_session_dirty(); id @@ -20,7 +23,7 @@ fn output_command( #[test] fn output_switch_returns_with_save_and_load_held_then_commits_in_order() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "destination"); stored_session::save_snapshot(&sample_snapshot(), &target).unwrap(); @@ -54,14 +57,14 @@ fn output_switch_returns_with_save_and_load_held_then_commits_in_order() { assert!(runtime.session.pending_output_transition().is_none()); assert!(matches!( runtime.input.boards.active_frame().shapes[0].shape, - crate::draw::Shape::Line { x2: 42, .. } + Shape::Line { x2: 42, .. } )); } #[test] fn output_load_cannot_replace_a_newer_destination_or_live_edits() { for change in ["destination", "return to source", "edit", "interaction"] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "destination"); stored_session::save_snapshot(&sample_snapshot(), &target).unwrap(); @@ -97,9 +100,9 @@ fn output_load_cannot_replace_a_newer_destination_or_live_edits() { runtime.input.on_mouse_press_with_canvas_and_resources( crate::input::state::InputTextResources { measurer: &measurer, - ui_engine: &crate::ui_text::UiTextEngine::default(), + ui_engine: &UiTextEngine::default(), }, - crate::input::MouseButton::Left, + MouseButton::Left, 500, 400, 500, @@ -125,7 +128,7 @@ fn output_load_cannot_replace_a_newer_destination_or_live_edits() { } assert!(matches!( runtime.input.boards.active_frame().shapes[0].shape, - crate::draw::Shape::Line { x2: 51, .. } + Shape::Line { x2: 51, .. } )); if change == "edit" { assert!(runtime.session.is_dirty()); @@ -162,11 +165,11 @@ fn output_load_cannot_replace_a_newer_destination_or_live_edits() { #[test] fn output_switch_does_not_recreate_missing_remembered_primary_or_restore_its_backup() { for home_enabled in [false, true] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let remembered = named_options(temp.path(), "remembered"); let home = configured_home(temp.path()); stored_session::save_snapshot(&sample_snapshot(), &remembered).unwrap(); - std::fs::rename( + fs::rename( remembered.session_file_path(), remembered.backup_file_path(), ) @@ -219,7 +222,7 @@ fn output_switch_does_not_recreate_missing_remembered_primary_or_restore_its_bac assert!(runtime.session.options().is_none()); assert!(matches!( runtime.input.boards.active_frame().shapes[0].shape, - crate::draw::Shape::Line { x2: 51, .. } + Shape::Line { x2: 51, .. } )); } } @@ -228,12 +231,12 @@ fn output_switch_does_not_recreate_missing_remembered_primary_or_restore_its_bac #[test] fn clean_output_loads_the_destination_without_rotating_existing_source_bytes() { for protected in [false, true] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "destination"); stored_session::save_snapshot(&sample_snapshot(), ¤t).unwrap(); stored_session::save_snapshot(&sample_snapshot(), &target).unwrap(); - let before = std::fs::read(current.session_file_path()).unwrap(); + let before = fs::read(current.session_file_path()).unwrap(); let mut input = test_input_state(); let measurer = TextMeasurer::default(); let mut session = SessionState::new(Some(current.clone())); @@ -250,19 +253,19 @@ fn clean_output_loads_the_destination_without_rotating_existing_source_bytes() { runtime.receive(); assert!(runtime.errors.is_empty(), "{:?}", runtime.errors); assert_eq!(runtime.session.options().unwrap().target, target.target); - assert_eq!(std::fs::read(current.session_file_path()).unwrap(), before); + assert_eq!(fs::read(current.session_file_path()).unwrap(), before); assert!(!current.backup_file_path().exists()); } } #[test] fn failed_output_save_keeps_the_source_canvas_and_pending_destination() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "destination"); stored_session::save_snapshot(&sample_snapshot(), &target).unwrap(); // A forbidden primary is an actual save error, independent of worker control. - std::fs::create_dir(current.session_file_path()).unwrap(); + fs::create_dir(current.session_file_path()).unwrap(); let mut input = test_input_state(); add_line(&mut input, 51); let measurer = TextMeasurer::default(); @@ -289,14 +292,14 @@ fn failed_output_save_keeps_the_source_canvas_and_pending_destination() { assert_eq!(runtime.session.options().unwrap().target, current.target); assert!(matches!( runtime.input.boards.active_frame().shapes[0].shape, - crate::draw::Shape::Line { x2: 51, .. } + Shape::Line { x2: 51, .. } )); } #[test] fn queued_target_commands_refuse_a_changed_session_instead_of_using_its_canvas() { for action in ["open", "save as", "home"] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "destination"); let explicit = named_options(temp.path(), "explicit"); @@ -355,11 +358,11 @@ fn queued_target_commands_refuse_a_changed_session_instead_of_using_its_canvas() #[test] fn shutdown_finishes_remembered_home_fallback_before_final_persistence() { for home_enabled in [false, true] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let remembered = named_options(temp.path(), "remembered"); let home = configured_home(temp.path()); stored_session::save_snapshot(&sample_snapshot(), &remembered).unwrap(); - std::fs::rename( + fs::rename( remembered.session_file_path(), remembered.backup_file_path(), ) @@ -408,7 +411,7 @@ fn shutdown_finishes_remembered_home_fallback_before_final_persistence() { assert!(!home_enabled); assert!(matches!( runtime.input.boards.active_frame().shapes[0].shape, - crate::draw::Shape::Line { x2: 51, .. } + Shape::Line { x2: 51, .. } )); } Ok(()) @@ -428,14 +431,14 @@ fn shutdown_finishes_remembered_home_fallback_before_final_persistence() { #[test] fn quarantine_failure_blocks_dirty_autosave_and_save_before_open() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let other = named_options(temp.path(), "other"); stored_session::save_snapshot(&sample_snapshot(), ¤t).unwrap(); - std::fs::rename(current.session_file_path(), current.backup_file_path()).unwrap(); - let backup = std::fs::read(current.backup_file_path()).unwrap(); - std::fs::write(current.session_file_path(), b"broken primary").unwrap(); - std::fs::write( + fs::rename(current.session_file_path(), current.backup_file_path()).unwrap(); + let backup = fs::read(current.backup_file_path()).unwrap(); + fs::write(current.session_file_path(), b"broken primary").unwrap(); + fs::write( stored_session::append_path_suffix( ¤t.session_file_path(), ".corrupt-18446744073709551615", @@ -481,10 +484,10 @@ fn quarantine_failure_blocks_dirty_autosave_and_save_before_open() { assert!(runtime.pending.is_none()); assert_eq!(runtime.errors.len(), 2); assert_eq!( - std::fs::read(current.session_file_path()).unwrap(), + fs::read(current.session_file_path()).unwrap(), b"broken primary" ); - assert_eq!(std::fs::read(current.backup_file_path()).unwrap(), backup); + assert_eq!(fs::read(current.backup_file_path()).unwrap(), backup); } fn drain_output_worker(runtime: &mut CommandRuntime<'_>, mut complete: impl FnMut()) { @@ -502,7 +505,7 @@ fn drain_output_worker(runtime: &mut CommandRuntime<'_>, mut complete: impl FnMu fn unloaded_edits_cannot_overwrite_primary_on_retry_or_return_to_source() { for source in ["named", "remembered", "configured", "per-output"] { for cancel in [false, true] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = if source == "named" || source == "remembered" { named_options(temp.path(), "current") } else { @@ -517,7 +520,7 @@ fn unloaded_edits_cannot_overwrite_primary_on_retry_or_return_to_source() { target.set_output_identity(Some("new-output")); } stored_session::save_snapshot(&sample_snapshot(), &options).unwrap(); - let original = std::fs::read(options.session_file_path()).unwrap(); + let original = fs::read(options.session_file_path()).unwrap(); let mut input = test_input_state(); let measurer = TextMeasurer::default(); let mut session = SessionState::new(Some(options.clone())); @@ -549,13 +552,13 @@ fn unloaded_edits_cannot_overwrite_primary_on_retry_or_return_to_source() { assert!(!target.session_file_path().exists()); } assert!( - std::fs::read(options.session_file_path()).unwrap() == original, + fs::read(options.session_file_path()).unwrap() == original, "the unloaded source must retain its saved ink" ); assert!(!options.backup_file_path().exists()); assert!(matches!( runtime.input.boards.active_frame().shapes[0].shape, - crate::draw::Shape::Line { x2: 77, .. } + Shape::Line { x2: 77, .. } )); assert!( !runtime @@ -567,12 +570,12 @@ fn unloaded_edits_cannot_overwrite_primary_on_retry_or_return_to_source() { } #[test] fn corrupt_remembered_backup_continues_and_recreates_primary_on_final_save() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = named_options(temp.path(), "remembered"); stored_session::save_snapshot(&sample_snapshot(), &options).unwrap(); - std::fs::rename(options.session_file_path(), options.backup_file_path()).unwrap(); - let backup = std::fs::read(options.backup_file_path()).unwrap(); - std::fs::write(options.session_file_path(), b"corrupt remembered").unwrap(); + fs::rename(options.session_file_path(), options.backup_file_path()).unwrap(); + let backup = fs::read(options.backup_file_path()).unwrap(); + fs::write(options.session_file_path(), b"corrupt remembered").unwrap(); let mut input = test_input_state(); let measurer = TextMeasurer::default(); let mut session = SessionState::new(Some(options.clone())); @@ -608,7 +611,7 @@ fn corrupt_remembered_backup_continues_and_recreates_primary_on_final_save() { }) .unwrap(); assert_eq!(loaded_line_x2(&options), 42); - assert_eq!(std::fs::read(options.backup_file_path()).unwrap(), backup); + assert_eq!(fs::read(options.backup_file_path()).unwrap(), backup); assert!( runtime .input @@ -622,14 +625,14 @@ fn corrupt_remembered_backup_continues_and_recreates_primary_on_final_save() { #[test] fn repaired_reload_unblocks_writes_and_dirty_blocked_sources_cannot_switch() { for repair in [false, true] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let destination = named_options(temp.path(), "destination"); stored_session::save_snapshot(&sample_snapshot(), ¤t).unwrap(); - std::fs::rename(current.session_file_path(), current.backup_file_path()).unwrap(); - let backup = std::fs::read(current.backup_file_path()).unwrap(); - std::fs::write(current.session_file_path(), b"corrupt primary").unwrap(); - std::fs::write( + fs::rename(current.session_file_path(), current.backup_file_path()).unwrap(); + let backup = fs::read(current.backup_file_path()).unwrap(); + fs::write(current.session_file_path(), b"corrupt primary").unwrap(); + fs::write( stored_session::append_path_suffix( ¤t.session_file_path(), ".corrupt-18446744073709551615", @@ -651,7 +654,7 @@ fn repaired_reload_unblocks_writes_and_dirty_blocked_sources_cannot_switch() { .refuses_source_write(¤t.session_file_path()) ); if repair { - std::fs::copy(current.backup_file_path(), current.session_file_path()).unwrap(); + fs::copy(current.backup_file_path(), current.session_file_path()).unwrap(); let retry = output_command(runtime.session, ¤t); start_session_command(&mut runtime, retry).unwrap(); drain_output_worker(&mut runtime, || worker.complete_next()); @@ -683,7 +686,7 @@ fn repaired_reload_unblocks_writes_and_dirty_blocked_sources_cannot_switch() { saved.boards[0].pages.pages[0] .shapes .iter() - .any(|shape| matches!(shape.shape, crate::draw::Shape::Line { x2: 77, .. })) + .any(|shape| matches!(shape.shape, Shape::Line { x2: 77, .. })) ); } else { assert_eq!(runtime.session.options().unwrap().target, current.target); @@ -694,13 +697,13 @@ fn repaired_reload_unblocks_writes_and_dirty_blocked_sources_cannot_switch() { .active_frame() .shapes .iter() - .any(|shape| matches!(shape.shape, crate::draw::Shape::Line { x2: 77, .. })) + .any(|shape| matches!(shape.shape, Shape::Line { x2: 77, .. })) ); assert_eq!( - std::fs::read(current.session_file_path()).unwrap(), + fs::read(current.session_file_path()).unwrap(), b"corrupt primary" ); - assert_eq!(std::fs::read(current.backup_file_path()).unwrap(), backup); + assert_eq!(fs::read(current.backup_file_path()).unwrap(), backup); assert!(!destination.session_file_path().exists()); } } @@ -709,7 +712,7 @@ fn repaired_reload_unblocks_writes_and_dirty_blocked_sources_cannot_switch() { #[test] fn queued_commands_remain_bound_to_the_visible_source_and_report_failures() { for fail_transport in [false, true] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "destination"); stored_session::save_snapshot(&sample_snapshot(), &target).unwrap(); @@ -764,7 +767,7 @@ fn queued_commands_remain_bound_to_the_visible_source_and_report_failures() { #[test] fn output_queue_coalesces_duplicates_limits_growth_and_cancels_edits_at_shutdown() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "destination"); stored_session::save_snapshot(&sample_snapshot(), &target).unwrap(); @@ -828,7 +831,7 @@ fn output_queue_coalesces_duplicates_limits_growth_and_cancels_edits_at_shutdown #[test] fn a_queued_clear_clears_what_it_was_requested_over_and_nothing_drawn_after() { for drawn_after_request in [false, true] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "destination"); stored_session::save_snapshot(&sample_snapshot(), &target).unwrap(); @@ -863,10 +866,7 @@ fn a_queued_clear_clears_what_it_was_requested_over_and_nothing_drawn_after() { assert_eq!(refused, drawn_after_request, "{:?}", runtime.errors); let shapes = &runtime.input.boards.active_frame().shapes; if drawn_after_request { - assert!(matches!( - shapes[0].shape, - crate::draw::Shape::Line { x2: 77, .. } - )); + assert!(matches!(shapes[0].shape, Shape::Line { x2: 77, .. })); } else { assert!(shapes.is_empty()); } @@ -875,7 +875,7 @@ fn a_queued_clear_clears_what_it_was_requested_over_and_nothing_drawn_after() { #[test] fn repeated_failed_output_loads_do_not_rotate_a_clean_sources_backup() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "current"); let target = named_options(temp.path(), "destination"); stored_session::save_snapshot(&sample_snapshot(), ¤t).unwrap(); @@ -890,10 +890,10 @@ fn repeated_failed_output_loads_do_not_rotate_a_clean_sources_backup() { ) .unwrap(); input.clear_session_dirty(); - let primary = std::fs::read(current.session_file_path()).unwrap(); - let backup = std::fs::read(current.backup_file_path()).unwrap(); - std::fs::write(target.session_file_path(), b"corrupt destination").unwrap(); - std::fs::write( + let primary = fs::read(current.session_file_path()).unwrap(); + let backup = fs::read(current.backup_file_path()).unwrap(); + fs::write(target.session_file_path(), b"corrupt destination").unwrap(); + fs::write( stored_session::append_path_suffix( &target.session_file_path(), ".corrupt-18446744073709551615", @@ -911,8 +911,8 @@ fn repeated_failed_output_loads_do_not_rotate_a_clean_sources_backup() { start_session_command(&mut runtime, command).unwrap(); finish_pending_session_command(&mut runtime).unwrap(); assert_eq!(runtime.session.options().unwrap().target, current.target); - assert_eq!(std::fs::read(current.session_file_path()).unwrap(), primary); - assert_eq!(std::fs::read(current.backup_file_path()).unwrap(), backup); + assert_eq!(fs::read(current.session_file_path()).unwrap(), primary); + assert_eq!(fs::read(current.backup_file_path()).unwrap(), backup); } assert_eq!(runtime.errors.len(), 3); assert!(runtime.reports.is_empty()); @@ -921,14 +921,14 @@ fn repeated_failed_output_loads_do_not_rotate_a_clean_sources_backup() { #[test] fn remembered_preservation_failure_goes_home_without_retrying_the_unreadable_source() { for home_enabled in [false, true] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "remembered"); let home = named_options(temp.path(), "home"); stored_session::save_snapshot(&sample_snapshot(), ¤t).unwrap(); - std::fs::rename(current.session_file_path(), current.backup_file_path()).unwrap(); - let backup = std::fs::read(current.backup_file_path()).unwrap(); - std::fs::write(current.session_file_path(), b"corrupt remembered").unwrap(); - std::fs::write( + fs::rename(current.session_file_path(), current.backup_file_path()).unwrap(); + let backup = fs::read(current.backup_file_path()).unwrap(); + fs::write(current.session_file_path(), b"corrupt remembered").unwrap(); + fs::write( stored_session::append_path_suffix( ¤t.session_file_path(), ".corrupt-18446744073709551615", @@ -970,9 +970,9 @@ fn remembered_preservation_failure_goes_home_without_retrying_the_unreadable_sou }] )); assert_eq!( - std::fs::read(current.session_file_path()).unwrap(), + fs::read(current.session_file_path()).unwrap(), b"corrupt remembered" ); - assert_eq!(std::fs::read(current.backup_file_path()).unwrap(), backup); + assert_eq!(fs::read(current.backup_file_path()).unwrap(), backup); } } diff --git a/src/backend/wayland/session/home.rs b/src/backend/wayland/session/home.rs index 353df00b7..838d6b84b 100644 --- a/src/backend/wayland/session/home.rs +++ b/src/backend/wayland/session/home.rs @@ -6,6 +6,7 @@ //! Neither input is a command-line flag, so an older overlay ignores both and //! keeps starting in the session its `--session-file` names. +use std::env; use std::ffi::OsString; use std::path::{Path, PathBuf}; @@ -54,7 +55,7 @@ impl SessionLaunch { /// The inputs for an overlay launched with `startup_file` as its /// `--session-file`. pub(in crate::backend::wayland) fn from_environment(startup_file: Option<&Path>) -> Self { - Self::from_lookup(startup_file, |name| std::env::var_os(name)) + Self::from_lookup(startup_file, |name| env::var_os(name)) } fn from_lookup(startup_file: Option<&Path>, lookup: impl Fn(&str) -> Option) -> Self { diff --git a/src/backend/wayland/session/home/tests.rs b/src/backend/wayland/session/home/tests.rs index c1e59a189..bb3278e65 100644 --- a/src/backend/wayland/session/home/tests.rs +++ b/src/backend/wayland/session/home/tests.rs @@ -227,13 +227,23 @@ fn a_run_without_persistence_is_in_the_default_session() { } mod load { + use std::fs; + use std::fs::{File, FileTimes}; + use std::os::unix; + use std::time::{Duration, SystemTime}; + + use crate::config::Config; + use crate::draw::{Color, Shape}; + use crate::test_temp::TempDir; + use crate::{draw, test_temp}; + use super::super::super::tests::{EnvGuard, named_options, sample_snapshot}; use super::*; use crate::backend::wayland::session::PersistenceController; use crate::session as stored_session; struct Sessions { - temp: crate::test_temp::TempDir, + temp: TempDir, remembered: SessionOptions, home: SessionOptions, persistence: PersistenceController, @@ -241,21 +251,21 @@ mod load { /// A saved remembered session and a saved configured home. /// A session file as this build writes one with tool settings and no ink. - fn blank_session_bytes(dir: &std::path::Path) -> Vec { + fn blank_session_bytes(dir: &Path) -> Vec { let scratch = named_options(dir, "blank"); let snapshot = stored_session::SessionSnapshot { active_board_id: "transparent".to_string(), boards: Vec::new(), tool_state: Some(stored_session::ToolStateSnapshot::from_config( - &crate::config::Config::default(), + &Config::default(), )), }; stored_session::save_snapshot(&snapshot, &scratch).unwrap(); - std::fs::read(scratch.session_file_path()).unwrap() + fs::read(scratch.session_file_path()).unwrap() } fn sessions() -> Sessions { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let remembered = named_options(temp.path(), "remembered"); let mut home = SessionOptions::new(temp.path().join("configured"), "home"); home.persist_transparent = true; @@ -311,7 +321,7 @@ mod load { #[test] fn a_deleted_remembered_session_starts_at_home() { let mut sessions = sessions(); - std::fs::remove_file(sessions.remembered.session_file_path()).unwrap(); + fs::remove_file(sessions.remembered.session_file_path()).unwrap(); let home = sessions.home.clone(); let load = sessions.load(Some(home)).unwrap(); @@ -332,7 +342,7 @@ mod load { // A second save leaves a backup of the first beside the file. stored_session::save_snapshot(&sample_snapshot(), &sessions.remembered).unwrap(); assert!(sessions.remembered.backup_file_path().exists()); - std::fs::rename(&path, path.with_extension("moved")).unwrap(); + fs::rename(&path, path.with_extension("moved")).unwrap(); let home = sessions.home.clone(); let _env = EnvGuard::set_xdg_data_home(sessions.temp.path()); @@ -353,11 +363,11 @@ mod load { let mut sessions = sessions(); let path = sessions.remembered.session_file_path(); let moved = path.with_extension("moved"); - std::fs::rename(&path, &moved).unwrap(); + fs::rename(&path, &moved).unwrap(); if replace == "symlink" { - std::os::unix::fs::symlink(&moved, &path).unwrap(); + unix::fs::symlink(&moved, &path).unwrap(); } else { - std::fs::create_dir(&path).unwrap(); + fs::create_dir(&path).unwrap(); } let home = sessions.home.clone(); @@ -371,7 +381,7 @@ mod load { #[test] fn without_persistence_home_has_nothing_to_load() { let mut sessions = sessions(); - std::fs::remove_file(sessions.remembered.session_file_path()).unwrap(); + fs::remove_file(sessions.remembered.session_file_path()).unwrap(); let load = sessions.load(None).unwrap(); @@ -384,7 +394,7 @@ mod load { #[test] fn a_session_file_given_at_launch_keeps_its_startup_rules() { let mut sessions = sessions(); - std::fs::remove_file(sessions.remembered.session_file_path()).unwrap(); + fs::remove_file(sessions.remembered.session_file_path()).unwrap(); let home = sessions.home.clone(); let remembered = sessions.remembered.clone(); @@ -407,7 +417,7 @@ mod load { let mut sessions = sessions(); let options = sessions.remembered.clone(); let primary = options.session_file_path(); - let ink = std::fs::read(&primary).unwrap(); + let ink = fs::read(&primary).unwrap(); let blank = blank_session_bytes(sessions.temp.path()); let bytes = match primary_kind { "ink" => ink.as_slice(), @@ -415,26 +425,21 @@ mod load { "blank" => blank.as_slice(), _ => unreachable!(), }; - std::fs::write(&primary, bytes).unwrap(); - std::fs::write(options.backup_file_path(), &ink).unwrap(); - std::fs::write(options.recovery_file_path(), &ink).unwrap(); + fs::write(&primary, bytes).unwrap(); + fs::write(options.backup_file_path(), &ink).unwrap(); + fs::write(options.recovery_file_path(), &ink).unwrap(); let clear = options.clear_marker_file_path(); - std::fs::write(&clear, b"cleared").unwrap(); + fs::write(&clear, b"cleared").unwrap(); let modified = |seconds| { - std::fs::FileTimes::new().set_modified( - std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(seconds), - ) + FileTimes::new().set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(seconds)) }; - std::fs::File::open(&primary) + File::open(&primary) .unwrap() .set_times(modified(10)) .unwrap(); - std::fs::File::open(&clear) - .unwrap() - .set_times(modified(20)) - .unwrap(); + File::open(&clear).unwrap().set_times(modified(20)).unwrap(); for sidecar in [options.backup_file_path(), options.recovery_file_path()] { - std::fs::File::open(sidecar) + File::open(sidecar) .unwrap() .set_times(modified(10)) .unwrap(); @@ -455,9 +460,9 @@ mod load { } else { assert!(matches!(outcome, LoadSnapshotOutcome::Empty)); } - assert_eq!(std::fs::read(&primary).unwrap(), bytes); - assert_eq!(std::fs::read(options.backup_file_path()).unwrap(), ink); - assert_eq!(std::fs::read(options.recovery_file_path()).unwrap(), ink); + assert_eq!(fs::read(&primary).unwrap(), bytes); + assert_eq!(fs::read(options.backup_file_path()).unwrap(), ink); + assert_eq!(fs::read(options.recovery_file_path()).unwrap(), ink); assert!(!stored_session::append_path_suffix(&primary, ".corrupt-1").exists()); } } @@ -467,27 +472,25 @@ mod load { let mut sessions = sessions(); let options = &sessions.remembered; let primary = options.session_file_path(); - let saved = std::fs::read(&primary).unwrap(); - std::fs::write(options.backup_file_path(), &saved).unwrap(); - std::fs::write(options.recovery_file_path(), &saved).unwrap(); - std::fs::write(options.backup_recovery_marker_file_path(), b"recoverable").unwrap(); - std::fs::write( + let saved = fs::read(&primary).unwrap(); + fs::write(options.backup_file_path(), &saved).unwrap(); + fs::write(options.recovery_file_path(), &saved).unwrap(); + fs::write(options.backup_recovery_marker_file_path(), b"recoverable").unwrap(); + fs::write( options.recovery_recoverable_marker_file_path(), b"recoverable", ) .unwrap(); let bytes = b"broken primary".as_slice(); - std::fs::write(&primary, bytes).unwrap(); + fs::write(&primary, bytes).unwrap(); let modified = |seconds| { - std::fs::FileTimes::new().set_modified( - std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(seconds), - ) + FileTimes::new().set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(seconds)) }; - std::fs::File::open(options.recovery_file_path()) + File::open(options.recovery_file_path()) .unwrap() .set_times(modified(10)) .unwrap(); - std::fs::File::open(&primary) + File::open(&primary) .unwrap() .set_times(modified(20)) .unwrap(); @@ -504,30 +507,30 @@ mod load { assert_eq!(loaded_options.target, sessions.remembered.target); assert!(snapshot.has_board_data()); assert_eq!( - std::fs::read(stored_session::append_path_suffix(&primary, ".corrupt-1")).unwrap(), + fs::read(stored_session::append_path_suffix(&primary, ".corrupt-1")).unwrap(), bytes ); let options = &sessions.remembered; - assert_eq!(std::fs::read(options.session_file_path()).unwrap(), saved); - assert_eq!(std::fs::read(options.backup_file_path()).unwrap(), saved); - assert_eq!(std::fs::read(options.recovery_file_path()).unwrap(), saved); + assert_eq!(fs::read(options.session_file_path()).unwrap(), saved); + assert_eq!(fs::read(options.backup_file_path()).unwrap(), saved); + assert_eq!(fs::read(options.recovery_file_path()).unwrap(), saved); } #[test] fn a_remembered_session_goes_home_when_oversized_corruption_restore_leaves_no_primary() { let mut sessions = sessions(); let mut snapshot = sample_snapshot(); - snapshot.boards[0].pages.pages[0].add_shape(crate::draw::Shape::Freehand { + snapshot.boards[0].pages.pages[0].add_shape(Shape::Freehand { points: (0..40_000) .map(|index| (f64::from(index % 1000), f64::from((index * 17) % 800))) .collect(), - color: crate::draw::WHITE, + color: draw::WHITE, thick: 2.0, }); let mut scratch = named_options(sessions.temp.path(), "large-recovery"); scratch.compression = stored_session::CompressionMode::Off; stored_session::save_snapshot(&snapshot, &scratch).unwrap(); - let saved = std::fs::read(scratch.session_file_path()).unwrap(); + let saved = fs::read(scratch.session_file_path()).unwrap(); // This valid filename leaves room for the sidecars, but not the atomic // diagnostic copy's temporary name. Its failed copy moves the corrupt @@ -541,9 +544,9 @@ mod load { let primary = sessions.remembered.session_file_path(); let recovery = sessions.remembered.recovery_file_path(); let corrupt_bytes = b"broken primary"; - std::fs::write(&primary, corrupt_bytes).unwrap(); - std::fs::write(&recovery, &saved).unwrap(); - std::fs::write( + fs::write(&primary, corrupt_bytes).unwrap(); + fs::write(&recovery, &saved).unwrap(); + fs::write( sessions.remembered.recovery_recoverable_marker_file_path(), b"recoverable", ) @@ -551,11 +554,12 @@ mod load { // Older marked recovery forces the corrupt-primary restoration path, // rather than loading a newer recovery before examining the primary. for (path, seconds) in [(&primary, 20), (&recovery, 10)] { - std::fs::File::open(path) + File::open(path) .unwrap() - .set_times(std::fs::FileTimes::new().set_modified( - std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(seconds), - )) + .set_times( + FileTimes::new() + .set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(seconds)), + ) .unwrap(); } let home = sessions.home.clone(); @@ -570,9 +574,9 @@ mod load { "{error:#}" ); assert!(!primary.exists()); - assert_eq!(std::fs::read(&recovery).unwrap(), saved); + assert_eq!(fs::read(&recovery).unwrap(), saved); assert_eq!( - std::fs::read(stored_session::append_path_suffix(&primary, ".corrupt-1")).unwrap(), + fs::read(stored_session::append_path_suffix(&primary, ".corrupt-1")).unwrap(), corrupt_bytes ); } @@ -583,12 +587,12 @@ mod load { let mut sessions = sessions(); let mut options = sessions.remembered.clone(); let mut newest = sample_snapshot(); - newest.boards[0].pages.pages[0].add_shape(crate::draw::Shape::Line { + newest.boards[0].pages.pages[0].add_shape(Shape::Line { x1: 10, y1: 10, x2: 71, y2: 20, - color: crate::draw::Color { + color: Color { r: 1.0, g: 0.0, b: 0.0, @@ -600,15 +604,14 @@ mod load { options.max_file_size_bytes = 64; stored_session::save_snapshot(&newest, &options).unwrap_err(); let modified = |seconds| { - std::fs::FileTimes::new().set_modified( - std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(seconds), - ) + FileTimes::new() + .set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(seconds)) }; - std::fs::File::open(options.session_file_path()) + File::open(options.session_file_path()) .unwrap() .set_times(modified(10)) .unwrap(); - std::fs::File::open(options.recovery_file_path()) + File::open(options.recovery_file_path()) .unwrap() .set_times(modified(20)) .unwrap(); @@ -623,7 +626,7 @@ mod load { // Tool-state-only blank retains a live accidental-blank marker. let mut blank = blank; blank.tool_state = Some(stored_session::ToolStateSnapshot::from_config( - &crate::config::Config::default(), + &Config::default(), )); stored_session::save_snapshot(&blank, &options).unwrap(); } diff --git a/src/backend/wayland/session/load_outcome.rs b/src/backend/wayland/session/load_outcome.rs index 978c794b9..792ddce4d 100644 --- a/src/backend/wayland/session/load_outcome.rs +++ b/src/backend/wayland/session/load_outcome.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use log::{debug, warn}; +use crate::draw::TextMeasurer; use crate::i18n::{tr, trf}; use crate::input::InputState; use crate::input::state::{Toast, ToastPriority}; @@ -23,7 +24,7 @@ pub(in crate::backend::wayland) struct ExpandedTooLarge { /// Publish one loaded output through the same canvas and write-protection rules. pub(in crate::backend::wayland) fn commit_output_load( input: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, session: &mut super::SessionState, options: SessionOptions, outcome: LoadSnapshotOutcome, @@ -54,7 +55,7 @@ pub(in crate::backend::wayland) fn commit_output_load( /// report. pub(in crate::backend::wayland) fn apply_load_outcome( input_state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, outcome: LoadSnapshotOutcome, options: &SessionOptions, context: &str, @@ -173,7 +174,7 @@ fn notify_unreadable_session( /// Replaces every board with `snapshot`, or with empty boards without one. pub(in crate::backend::wayland) fn replace_output_session_snapshot( input_state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, snapshot: Option, options: &SessionOptions, ) -> Result<()> { diff --git a/src/backend/wayland/session/persistence.rs b/src/backend/wayland/session/persistence.rs index 21342f29a..02856965f 100644 --- a/src/backend/wayland/session/persistence.rs +++ b/src/backend/wayland/session/persistence.rs @@ -1,9 +1,11 @@ +use std::fmt::{Display, Formatter}; #[cfg(test)] use std::path::Path; use std::path::PathBuf; use std::sync::mpsc::{self, Receiver, SyncSender, TryRecvError, TrySendError}; use std::thread::{self, JoinHandle}; use std::time::{Duration, Instant}; +use std::{error, fmt}; use anyhow::{Context, Result, anyhow}; @@ -11,7 +13,7 @@ use crate::backend::wayland::backend::runtime_wake::RuntimeWakeHandle; #[cfg(test)] use crate::backend::wayland::backend::runtime_wake::RuntimeWakeSource; use crate::session::{ - self, ClearToolStateOutcome, LoadSnapshotOutcome, SaveAsOverwrite, SaveSnapshotReport, + ClearToolStateOutcome, LoadSnapshotOutcome, SaveAsOverwrite, SaveSnapshotReport, SessionInspection, SessionOptions, SessionSnapshot, }; @@ -182,8 +184,8 @@ pub(in crate::backend::wayland) enum SubmitError { Unhealthy, } -impl std::fmt::Display for SubmitError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for SubmitError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let message = match self { Self::Busy => "a persistence operation is already active", Self::Full => "persistence request channel unexpectedly full", @@ -194,7 +196,7 @@ impl std::fmt::Display for SubmitError { } } -impl std::error::Error for SubmitError {} +impl error::Error for SubmitError {} #[derive(Debug)] pub(in crate::backend::wayland) struct SubmitFailure { @@ -222,7 +224,7 @@ impl PersistenceController { let (completion_tx, completion_rx) = mpsc::sync_channel(1); let worker = thread::Builder::new() .name("wayscriber-persistence".to_string()) - .spawn(move || worker_main(request_rx, completion_tx, wake)) + .spawn(move || worker_main(&request_rx, completion_tx, wake)) .context("failed to start session persistence worker")?; Ok(Self { @@ -237,7 +239,8 @@ impl PersistenceController { #[cfg(test)] pub(in crate::backend::wayland) fn start_for_test() -> Result { - let wake = RuntimeWakeSource::new().context("failed to create test runtime wake source")?; + let wake = + RuntimeWakeSource::try_new().context("failed to create test runtime wake source")?; Self::start(wake.handle()) } @@ -265,7 +268,7 @@ impl PersistenceController { &mut self, target_epoch: u64, operation: PersistenceOperation, - ) -> std::result::Result { + ) -> Result { if !self.healthy { return Err(SubmitFailure { error: SubmitError::Unhealthy, @@ -467,7 +470,11 @@ fn assert_send_static() {} #[cfg(test)] mod tests { + use std::io::ErrorKind; use std::os::fd::AsRawFd; + use std::{fs, io}; + + use crate::test_temp; use super::*; use crate::session::SaveSnapshotOutcome; @@ -490,7 +497,7 @@ mod tests { } fn controller_with_wake() -> (RuntimeWakeSource, PersistenceController) { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let controller = PersistenceController::start(wake.handle()).unwrap(); (wake, controller) } @@ -512,8 +519,8 @@ mod tests { if ready == 0 { panic!("persistence worker did not wake runtime within one second"); } - let err = std::io::Error::last_os_error(); - if err.kind() != std::io::ErrorKind::Interrupted { + let err = io::Error::last_os_error(); + if err.kind() != ErrorKind::Interrupted { panic!("runtime wake poll failed: {err}"); } } @@ -521,7 +528,7 @@ mod tests { #[test] fn real_worker_publishes_completion_before_waking_runtime() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = test_options(temp.path().to_path_buf()); let (wake, mut controller) = controller_with_wake(); controller @@ -543,7 +550,7 @@ mod tests { #[test] fn production_error_completion_wakes_runtime_and_worker_remains_healthy() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let missing = temp.path().join("missing.wayscriber-session"); let (wake, mut controller) = controller_with_wake(); controller @@ -578,7 +585,7 @@ mod tests { #[test] fn synchronous_barriers_and_shutdown_tolerate_redundant_unread_wake() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = test_options(temp.path().to_path_buf()); let (wake, mut controller) = controller_with_wake(); @@ -594,7 +601,7 @@ mod tests { #[test] fn real_worker_executes_production_operation_off_caller_thread() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = test_options(temp.path().to_path_buf()); let mut controller = PersistenceController::start_for_test().unwrap(); controller @@ -614,7 +621,7 @@ mod tests { #[test] fn real_worker_uses_existing_save_pipeline_and_reports_committed_state() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = test_options(temp.path().to_path_buf()); let snapshot = SessionSnapshot { active_board_id: "transparent".to_string(), @@ -653,7 +660,7 @@ mod tests { #[test] fn production_error_does_not_kill_worker() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let missing = temp.path().join("missing.wayscriber-session"); let options = test_options(temp.path().to_path_buf()); let mut controller = PersistenceController::start_for_test().unwrap(); @@ -677,10 +684,10 @@ mod tests { fn failed_autosave_can_be_followed_by_normal_save_on_same_worker() { use std::os::unix::fs::symlink; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let target = temp.path().join("autosave-target.wayscriber-session"); let alias = temp.path().join("autosave-alias.wayscriber-session"); - std::fs::write(&target, b"{}").unwrap(); + fs::write(&target, b"{}").unwrap(); symlink(&target, &alias).unwrap(); let mut invalid_options = test_options(temp.path().to_path_buf()); invalid_options.set_named_file_target(alias); @@ -730,10 +737,10 @@ mod tests { fn save_as_preflight_rejects_same_target_symlink_before_identity_matching() { use std::os::unix::fs::symlink; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current_path = temp.path().join("current.wayscriber-session"); let alias_path = temp.path().join("current-alias.wayscriber-session"); - std::fs::write(¤t_path, b"{}").unwrap(); + fs::write(¤t_path, b"{}").unwrap(); symlink(¤t_path, &alias_path).unwrap(); let mut options = test_options(temp.path().to_path_buf()); @@ -756,9 +763,9 @@ mod tests { #[test] fn save_as_preflight_reports_regular_current_target_after_validation() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current_path = temp.path().join("current.wayscriber-session"); - std::fs::write(¤t_path, b"{}").unwrap(); + fs::write(¤t_path, b"{}").unwrap(); let mut options = test_options(temp.path().to_path_buf()); options.set_named_file_target(current_path.clone()); let mut controller = PersistenceController::start_for_test().unwrap(); @@ -816,7 +823,7 @@ mod tests { #[test] fn full_try_submit_returns_owned_operation_without_installing_ticket() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = test_options(temp.path().to_path_buf()); let (request_tx, request_rx) = mpsc::sync_channel(1); request_tx @@ -839,12 +846,7 @@ mod tests { healthy: true, }; - let result = controller.try_submit( - 0, - PersistenceOperation::HasArtifacts { - options: options.clone(), - }, - ); + let result = controller.try_submit(0, PersistenceOperation::HasArtifacts { options }); let failure = result.expect_err("full request channel must reject submission"); assert!(matches!(failure.error, SubmitError::Full)); assert!(matches!( @@ -858,7 +860,7 @@ mod tests { #[test] fn disconnected_try_submit_returns_owned_operation_without_installing_ticket() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = test_options(temp.path().to_path_buf()); let (request_tx, request_rx) = mpsc::sync_channel(1); drop(request_rx); diff --git a/src/backend/wayland/session/persistence/test_support.rs b/src/backend/wayland/session/persistence/test_support.rs index b3eb75028..d9fcc9413 100644 --- a/src/backend/wayland/session/persistence/test_support.rs +++ b/src/backend/wayland/session/persistence/test_support.rs @@ -1,5 +1,6 @@ //! Controlled channel peer for session driver tests, not a second command driver. -use super::{worker::execute, *}; +use super::worker::execute; +use super::*; impl PersistenceController { pub(in crate::backend::wayland) fn controlled_for_test() -> (Self, ControlledPersistenceWorker) diff --git a/src/backend/wayland/session/persistence/worker.rs b/src/backend/wayland/session/persistence/worker.rs index c19a45405..7b81ca238 100644 --- a/src/backend/wayland/session/persistence/worker.rs +++ b/src/backend/wayland/session/persistence/worker.rs @@ -1,11 +1,24 @@ //! Background disk operations and completion-before-wake publication. +use std::fs; +use std::io::ErrorKind; use std::path::Path; -use super::*; -use crate::session::SaveSnapshotOutcome; +use crate::draw::Shape; + +use super::{ + PersistenceCompletion, PersistenceOperation, PersistenceOutcome, PersistenceRequest, + SaveCompletion, SaveStrategy, +}; +use crate::backend::wayland::backend::runtime_wake::RuntimeWakeHandle; +use crate::session; +use crate::session::{LoadSnapshotOutcome, SaveSnapshotOutcome, SessionOptions, SessionSnapshot}; +use anyhow::{Context, Result, anyhow}; +use std::sync::mpsc::{Receiver, SyncSender}; +use std::thread; +use std::time::Instant; pub(super) fn worker_main( - request_rx: Receiver, + request_rx: &Receiver, completion_tx: SyncSender, wake: RuntimeWakeHandle, ) { @@ -210,9 +223,9 @@ fn load_home(options: &SessionOptions) -> Result { /// A session file that a save can replace: a regular file, or none yet. fn require_replaceable_session_file(path: &Path) -> Result<()> { - let metadata = match std::fs::symlink_metadata(path) { + let metadata = match fs::symlink_metadata(path) { Ok(metadata) => metadata, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(()), Err(error) => { return Err(error) .with_context(|| format!("failed to inspect session file {}", path.display())); @@ -306,7 +319,7 @@ fn log_snapshot_summary( redo_entries += redo; max_history_depth = max_history_depth.max(undo.max(redo)); for drawn in &frame.shapes { - if let crate::draw::Shape::Image { data, .. } = &drawn.shape { + if let Shape::Image { data, .. } = &drawn.shape { visible_image_shapes += 1; visible_image_bytes = visible_image_bytes.saturating_add(data.bytes.len()); } diff --git a/src/backend/wayland/session/runtime.rs b/src/backend/wayland/session/runtime.rs index e68f3f55c..81fd09f63 100644 --- a/src/backend/wayland/session/runtime.rs +++ b/src/backend/wayland/session/runtime.rs @@ -1,5 +1,15 @@ -use super::*; -use crate::session::{SaveAsOverwrite, SessionSnapshot, ToolStateSnapshot}; +use crate::backend::wayland::session; +use crate::draw::TextMeasurer; + +use super::persistence::PersistenceOutcome; +use super::{SessionState, stored_session}; +use crate::input::InputState; +use crate::session::{LoadSnapshotOutcome, SaveAsOverwrite, SessionOptions, SessionSnapshot}; +use anyhow::{Result, anyhow}; +use std::fs; +use std::path::{Path, PathBuf}; +#[cfg(test)] +use std::time::Instant; #[allow(dead_code)] #[derive(Debug)] @@ -37,10 +47,10 @@ pub(in crate::backend::wayland) struct RuntimeClearToolStateReport { pub outcome: Option, } -pub(in crate::backend::wayland) struct SessionTransaction<'a> { - pub input_state: &'a mut InputState, - pub measurer: &'a crate::draw::TextMeasurer, - pub session: &'a mut SessionState, +pub(in crate::backend::wayland) struct SessionTransaction<'state> { + pub input_state: &'state mut InputState, + pub measurer: &'state TextMeasurer, + pub session: &'state mut SessionState, } mod transaction; @@ -56,8 +66,8 @@ enum SaveAsPreflightDecision { } fn accept_open_preflight( - session: &mut crate::backend::wayland::session::SessionState, - validation: PersistenceOutcome, + session: &mut session::SessionState, + validation: &PersistenceOutcome, ) -> Result<()> { if !matches!(validation, PersistenceOutcome::Unit) { return Err(anyhow!("unexpected named-session validation outcome")); @@ -67,15 +77,15 @@ fn accept_open_preflight( } fn accept_save_as_preflight( - session: &mut crate::backend::wayland::session::SessionState, - preflight: PersistenceOutcome, + session: &mut session::SessionState, + preflight: &PersistenceOutcome, overwrite: SaveAsOverwrite, target_path: &Path, ) -> Result { let PersistenceOutcome::SaveAsPreflight { same_target, overwrite_required, - } = preflight + } = *preflight else { return Err(anyhow!("unexpected Save As preflight outcome")); }; @@ -93,7 +103,7 @@ fn accept_save_as_preflight( } fn cancel_pending_output_transition_for_explicit_target( - session: &mut crate::backend::wayland::session::SessionState, + session: &mut session::SessionState, operation: &str, ) { if let Some(pending) = session.cancel_pending_output_transition() { @@ -163,7 +173,7 @@ fn has_recovery_artifact(options: &SessionOptions) -> bool { return false; }; let preserved_prefix = format!("{recovery_name}."); - let Ok(entries) = std::fs::read_dir(parent) else { + let Ok(entries) = fs::read_dir(parent) else { return false; }; entries.filter_map(Result::ok).any(|entry| { @@ -176,28 +186,36 @@ fn has_recovery_artifact(options: &SessionOptions) -> bool { }) } +/// What a save knows about the session before it would replace the artifact. +#[derive(Debug, Clone, Copy, Default)] +pub(in crate::backend::wayland) struct ContentlessSaveCheck { + /// The session started from a file that held board data. + pub(in crate::backend::wayland) loaded_board_data: bool, + pub(in crate::backend::wayland) session_dirty: bool, + pub(in crate::backend::wayland) input_dirty: bool, + /// The snapshot about to be saved holds board data. + pub(in crate::backend::wayland) has_board_data: bool, + pub(in crate::backend::wayland) session_artifact_exists: bool, +} + pub(in crate::backend::wayland) fn should_skip_unloaded_contentless_save( - loaded_board_data: bool, - session_dirty: bool, - input_dirty: bool, - has_board_data: bool, - session_artifact_exists: bool, + check: ContentlessSaveCheck, ) -> bool { - !has_board_data - && !loaded_board_data - && !session_dirty - && !input_dirty - && session_artifact_exists + !check.has_board_data + && !check.loaded_board_data + && !check.session_dirty + && !check.input_dirty + && check.session_artifact_exists } #[cfg(test)] mod tests { use super::*; - fn session_with_pending_transition() -> crate::backend::wayland::session::SessionState { + fn session_with_pending_transition() -> session::SessionState { let options = SessionOptions::new(PathBuf::from("/tmp"), "source-output"); let mut staged = options.clone(); staged.set_output_identity(Some("target-output")); - let mut session = crate::backend::wayland::session::SessionState::new(Some(options)); + let mut session = session::SessionState::new(Some(options)); session.stage_output_transition(staged, Some("target-output".to_string()), Instant::now()); session } @@ -206,12 +224,12 @@ mod tests { fn open_cancels_pending_transition_only_after_accepted_preflight() { let mut rejected = session_with_pending_transition(); assert!( - accept_open_preflight(&mut rejected, PersistenceOutcome::HasArtifacts(false)).is_err() + accept_open_preflight(&mut rejected, &PersistenceOutcome::HasArtifacts(false)).is_err() ); assert!(rejected.pending_output_transition().is_some()); let mut accepted = session_with_pending_transition(); - accept_open_preflight(&mut accepted, PersistenceOutcome::Unit).unwrap(); + accept_open_preflight(&mut accepted, &PersistenceOutcome::Unit).unwrap(); assert!(accepted.pending_output_transition().is_none()); } @@ -222,7 +240,7 @@ mod tests { assert!( accept_save_as_preflight( &mut denied, - PersistenceOutcome::SaveAsPreflight { + &PersistenceOutcome::SaveAsPreflight { same_target: false, overwrite_required: true, }, @@ -237,7 +255,7 @@ mod tests { assert_eq!( accept_save_as_preflight( &mut accepted, - PersistenceOutcome::SaveAsPreflight { + &PersistenceOutcome::SaveAsPreflight { same_target: false, overwrite_required: false, }, @@ -256,7 +274,7 @@ mod tests { assert_eq!( accept_save_as_preflight( &mut session, - PersistenceOutcome::SaveAsPreflight { + &PersistenceOutcome::SaveAsPreflight { same_target: true, overwrite_required: true, }, diff --git a/src/backend/wayland/session/runtime/transaction.rs b/src/backend/wayland/session/runtime/transaction.rs index d4cf5f73f..55890b9f1 100644 --- a/src/backend/wayland/session/runtime/transaction.rs +++ b/src/backend/wayland/session/runtime/transaction.rs @@ -1,5 +1,21 @@ //! Session commands advance only when their disk phase completes. -use super::*; +use std::collections::VecDeque; +use std::fmt::{Display, Formatter}; +use std::{error, fmt}; + +use super::super::persistence::{ + PersistenceOperation, PersistenceOutcome, RequestId, SaveStrategy, +}; +use super::super::stored_session; +use super::{ + RuntimeClearSessionReport, RuntimeClearToolStateReport, RuntimeOpenSessionReport, + RuntimeSaveAsSessionReport, SessionTransaction, session_persistence_enabled, +}; +use crate::input::InputState; +use crate::session::{SaveAsOverwrite, SessionOptions, SessionSnapshot, ToolStateSnapshot}; +use anyhow::{Result, anyhow}; +use std::path::PathBuf; +use std::time::Instant; mod output; mod phases; @@ -7,13 +23,13 @@ mod phases; #[derive(Debug)] pub(in crate::backend::wayland) struct SessionCommandAborted(&'static str); -impl std::fmt::Display for SessionCommandAborted { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for SessionCommandAborted { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter.write_str(self.0) } } -impl std::error::Error for SessionCommandAborted {} +impl error::Error for SessionCommandAborted {} #[derive(Debug)] pub(in crate::backend::wayland) enum SessionCommand { @@ -103,8 +119,7 @@ impl SessionCommand { pub(in crate::backend::wayland) struct SessionCommandTransaction { command: SessionCommand, - pub(in crate::backend::wayland::session) queued_commands: - std::collections::VecDeque, + pub(in crate::backend::wayland::session) queued_commands: VecDeque, phase: Phase, current: Option, target: Option, @@ -122,7 +137,7 @@ impl SessionCommandTransaction { pub fn new(command: SessionCommand, epoch: u64, interaction: (u64, bool)) -> Self { Self { command, - queued_commands: std::collections::VecDeque::new(), + queued_commands: VecDeque::new(), phase: Phase::Start, current: None, target: None, @@ -192,7 +207,7 @@ impl SessionCommandTransaction { Phase::OutputCheckSource => self.complete_output_check_source(context, outcome), Phase::OutputArtifacts => self.complete_output_artifacts(context, outcome), Phase::OutputLoad | Phase::OutputHome => self.complete_output_load(context, outcome), - Phase::Start => self.complete_start(context, outcome), + Phase::Start => self.complete_start(context, outcome.as_ref()), Phase::OpenPreflight => self.complete_open_preflight(context, outcome), Phase::SaveCurrent => self.complete_save_current(context, outcome), Phase::Load => self.complete_load(context, outcome), @@ -209,7 +224,11 @@ impl SessionCommandTransaction { // Catalog bookkeeping is best effort after a successful open. pub fn catalog_failure_report(&self, error: anyhow::Error) -> SessionCommandReport { - debug_assert!(self.has_committed_open()); + debug_assert!( + self.has_committed_open(), + "catalog failures are reported only after the open committed, got phase {:?}", + self.phase + ); let mut report = self.open_report(); report.catalog_error = Some(error); @@ -336,7 +355,7 @@ impl SessionCommandTransaction { fn apply_default_tools( &self, context: &mut SessionTransaction<'_>, - defaults: ToolStateSnapshot, + defaults: &ToolStateSnapshot, ) { stored_session::apply_tool_state_snapshot(context.input_state, context.measurer, defaults); context.input_state.mark_session_dirty(); diff --git a/src/backend/wayland/session/runtime/transaction/output.rs b/src/backend/wayland/session/runtime/transaction/output.rs index 30f14e3f5..1905582f3 100644 --- a/src/backend/wayland/session/runtime/transaction/output.rs +++ b/src/backend/wayland/session/runtime/transaction/output.rs @@ -1,5 +1,14 @@ //! Output switches use the same ordered, nonblocking disk phases as session commands. -use super::*; +use super::super::{SessionTransaction, session_persistence_enabled}; +use super::{ + Phase, SessionCommand, SessionCommandReport, SessionCommandTransaction, TransactionStep, + empty_snapshot, required_outcome, +}; +use crate::backend::wayland::session::persistence::{ + PersistenceOperation, PersistenceOutcome, SaveStrategy, +}; +use crate::session::SessionSnapshot; +use anyhow::{Result, anyhow}; impl SessionCommandTransaction { pub(super) fn start_output( diff --git a/src/backend/wayland/session/runtime/transaction/phases.rs b/src/backend/wayland/session/runtime/transaction/phases.rs index 800ac72f4..fd37bbbaf 100644 --- a/src/backend/wayland/session/runtime/transaction/phases.rs +++ b/src/backend/wayland/session/runtime/transaction/phases.rs @@ -1,10 +1,28 @@ -use super::*; +use super::super::{ + RuntimeClearSessionReport, RuntimeClearToolStateReport, RuntimeSaveAsSessionReport, + SaveAsPreflightDecision, SessionTransaction, accept_open_preflight, accept_save_as_preflight, + cancel_pending_output_transition_for_explicit_target, named_candidate_snapshot, +}; +use super::{ + Phase, SessionCommand, SessionCommandReport, SessionCommandTransaction, TransactionStep, + empty_snapshot, required_outcome, +}; +use crate::backend::wayland::session::load_outcome::{ + apply_load_outcome, replace_output_session_snapshot, +}; +use crate::backend::wayland::session::persistence::{ + PersistenceOperation, PersistenceOutcome, SaveStrategy, +}; +use crate::backend::wayland::session::stored_session; +use crate::session::LoadSnapshotOutcome; +use anyhow::{Result, anyhow}; +use std::time::Instant; impl SessionCommandTransaction { pub(super) fn complete_start( &mut self, context: &mut SessionTransaction<'_>, - outcome: Option, + outcome: Option<&PersistenceOutcome>, ) -> Result { if outcome.is_some() { return Err(anyhow!( @@ -14,8 +32,7 @@ impl SessionCommandTransaction { self.current = context.session.options().cloned(); match &self.command { SessionCommand::ClearTools(defaults) if self.current.is_none() => { - let defaults = defaults.clone(); - self.apply_default_tools(context, *defaults); + self.apply_default_tools(context, defaults); Ok(TransactionStep::Complete(Box::new( SessionCommandReport::ClearTools(RuntimeClearToolStateReport { session_path: None, @@ -98,7 +115,7 @@ impl SessionCommandTransaction { context: &mut SessionTransaction<'_>, outcome: Option, ) -> Result { - accept_open_preflight(context.session, required_outcome(outcome)?)?; + accept_open_preflight(context.session, &required_outcome(outcome)?)?; self.save_current_or_continue(context) } @@ -271,7 +288,7 @@ impl SessionCommandTransaction { let SessionCommand::SaveAs(path, overwrite) = &self.command else { unreachable!() }; - match accept_save_as_preflight(context.session, outcome, *overwrite, path)? { + match accept_save_as_preflight(context.session, &outcome, *overwrite, path)? { SaveAsPreflightDecision::SameTarget => self.save_current_or_continue(context), SaveAsPreflightDecision::SwitchTarget => { let overwrite = *overwrite; @@ -373,7 +390,7 @@ impl SessionCommandTransaction { let SessionCommand::ClearTools(defaults) = &self.command else { unreachable!() }; - self.apply_default_tools(context, *defaults.clone()); + self.apply_default_tools(context, defaults); Ok(TransactionStep::Complete(Box::new( SessionCommandReport::ClearTools(RuntimeClearToolStateReport { session_path: Some(self.current_path()), diff --git a/src/backend/wayland/session/tests.rs b/src/backend/wayland/session/tests.rs index 66f7ef85a..1c0c0c5ce 100644 --- a/src/backend/wayland/session/tests.rs +++ b/src/backend/wayland/session/tests.rs @@ -1,3 +1,12 @@ +use crate::draw::TextMeasurer; +use crate::input::state::test_support::TestInputStateBuilder; +use crate::input::state::{PickerDrag, ToastPriority}; +use crate::input::{Key, MouseButton}; +use crate::session::{SaveSnapshotOutcome, SaveSnapshotReport}; +use crate::test_env::LeaseGuard; +use crate::ui_text::UiTextEngine; +use crate::{durable_io, test_env, test_temp}; + use super::*; use crate::config::{Action, Config, Shortcut}; use crate::draw::{ @@ -10,25 +19,29 @@ use crate::input::{BOARD_ID_TRANSPARENT, BOARD_ID_WHITEBOARD, DrawingState, Eras use crate::util::Rect; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::sync::MutexGuard; +use std::ffi::OsString; +use std::fs::{OpenOptions, Permissions}; #[cfg(unix)] use std::os::unix::fs::{PermissionsExt, symlink}; +use std::{env, fs, process}; pub(super) struct EnvGuard { - _guard: MutexGuard<'static, ()>, - catalog_hooks: Option, - xdg_data_home: Option, + _guard: LeaseGuard<'static>, + catalog_hooks: Option, + xdg_data_home: Option, } impl EnvGuard { pub(super) fn set_xdg_data_home(path: &Path) -> Self { - let guard = crate::test_env::lock(); - let catalog_hooks = std::env::var_os(CATALOG_HOOKS_TEST_ENV); - let xdg_data_home = std::env::var_os(XDG_DATA_HOME_ENV); + let guard = test_env::lock(); + let catalog_hooks = env::var_os(CATALOG_HOOKS_TEST_ENV); + let xdg_data_home = env::var_os(XDG_DATA_HOME_ENV); + // SAFETY: serialized by the test environment mutex in `guard`, which + // the returned value keeps until it restores both variables. unsafe { - std::env::set_var(CATALOG_HOOKS_TEST_ENV, path); - std::env::set_var(XDG_DATA_HOME_ENV, path); + env::set_var(CATALOG_HOOKS_TEST_ENV, path); + env::set_var(XDG_DATA_HOME_ENV, path); } Self { _guard: guard, @@ -40,13 +53,17 @@ impl EnvGuard { impl Drop for EnvGuard { fn drop(&mut self) { - match self.catalog_hooks.take() { - Some(value) => unsafe { std::env::set_var(CATALOG_HOOKS_TEST_ENV, value) }, - None => unsafe { std::env::remove_var(CATALOG_HOOKS_TEST_ENV) }, - } - match self.xdg_data_home.take() { - Some(value) => unsafe { std::env::set_var(XDG_DATA_HOME_ENV, value) }, - None => unsafe { std::env::remove_var(XDG_DATA_HOME_ENV) }, + // SAFETY: fields drop only after this body returns, so `_guard` still + // holds the test environment mutex here. + unsafe { + match self.catalog_hooks.take() { + Some(value) => env::set_var(CATALOG_HOOKS_TEST_ENV, value), + None => env::remove_var(CATALOG_HOOKS_TEST_ENV), + } + match self.xdg_data_home.take() { + Some(value) => env::set_var(XDG_DATA_HOME_ENV, value), + None => env::remove_var(XDG_DATA_HOME_ENV), + } } } } @@ -60,21 +77,17 @@ struct ReadonlyDirGuard { #[cfg(unix)] impl Drop for ReadonlyDirGuard { fn drop(&mut self) { - let _ = std::fs::set_permissions( - &self.path, - std::fs::Permissions::from_mode(self.original_mode), - ); + let _ = fs::set_permissions(&self.path, Permissions::from_mode(self.original_mode)); } } #[cfg(unix)] fn readonly_dir_guard(path: &Path) -> ReadonlyDirGuard { - let original_mode = std::fs::metadata(path) + let original_mode = fs::metadata(path) .expect("parent metadata") .permissions() .mode(); - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o555)) - .expect("make parent read-only"); + fs::set_permissions(path, Permissions::from_mode(0o555)).expect("make parent read-only"); ReadonlyDirGuard { path: path.to_path_buf(), original_mode, @@ -85,16 +98,12 @@ fn readonly_dir_guard(path: &Path) -> ReadonlyDirGuard { fn can_create_probe(parent: &Path) -> bool { let probe = parent.join(format!( ".wayscriber-runtime-open-manual-access-test-{}", - std::process::id() + process::id() )); - match std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&probe) - { + match OpenOptions::new().write(true).create_new(true).open(&probe) { Ok(file) => { drop(file); - let _ = std::fs::remove_file(&probe); + let _ = fs::remove_file(&probe); true } Err(_) => false, @@ -104,7 +113,7 @@ fn can_create_probe(parent: &Path) -> bool { pub(super) fn test_input_state() -> InputState { let mut action_map = HashMap::new(); action_map.insert(Shortcut::parse("Escape").unwrap(), Action::Exit); - crate::input::state::test_support::TestInputStateBuilder::default() + TestInputStateBuilder::default() .action_map(action_map) .action_bindings(HashMap::new()) .thickness(3.0) @@ -250,7 +259,7 @@ pub(super) fn loaded_line_x2(options: &SessionOptions) -> i32 { #[test] fn runtime_save_as_new_path_writes_and_switches_active_target() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-save-as-new"); let target_options = named_options(temp.path(), "target-save-as-new"); @@ -262,7 +271,7 @@ fn runtime_save_as_new_path_writes_and_switches_active_target() { let report = save_named_session_as_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &target_options.session_file_path(), stored_session::SaveAsOverwrite::Deny, @@ -302,12 +311,12 @@ fn runtime_save_as_new_path_writes_and_switches_active_target() { #[test] fn runtime_save_as_rejects_existing_target_without_confirmation() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-save-as-existing"); let target_options = named_options(temp.path(), "target-save-as-existing"); stored_session::save_snapshot(&snapshot_for_board("transparent", 17), &target_options) .expect("save existing target"); - let before = std::fs::read(target_options.session_file_path()).expect("existing target bytes"); + let before = fs::read(target_options.session_file_path()).expect("existing target bytes"); let mut input = test_input_state(); add_line(&mut input, 88); @@ -316,7 +325,7 @@ fn runtime_save_as_rejects_existing_target_without_confirmation() { let err = save_named_session_as_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &target_options.session_file_path(), stored_session::SaveAsOverwrite::Deny, @@ -336,7 +345,7 @@ fn runtime_save_as_rejects_existing_target_without_confirmation() { ); assert!(session_state.is_dirty()); assert_eq!( - std::fs::read(target_options.session_file_path()).expect("target unchanged"), + fs::read(target_options.session_file_path()).expect("target unchanged"), before ); assert_eq!(loaded_line_x2(&target_options), 17); @@ -344,10 +353,10 @@ fn runtime_save_as_rejects_existing_target_without_confirmation() { #[test] fn runtime_save_as_rejects_existing_sidecar_without_confirmation() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-save-as-sidecar"); let target_options = named_options(temp.path(), "target-save-as-sidecar"); - std::fs::write(target_options.clear_marker_file_path(), b"stale clear").expect("stale clear"); + fs::write(target_options.clear_marker_file_path(), b"stale clear").expect("stale clear"); let mut input = test_input_state(); add_line(&mut input, 88); @@ -356,7 +365,7 @@ fn runtime_save_as_rejects_existing_sidecar_without_confirmation() { let err = save_named_session_as_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &target_options.session_file_path(), stored_session::SaveAsOverwrite::Deny, @@ -381,14 +390,14 @@ fn runtime_save_as_rejects_existing_sidecar_without_confirmation() { #[test] fn runtime_save_as_overwrite_preflight_reports_existing_artifacts() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-save-as-preflight"); let fresh_options = named_options(temp.path(), "fresh-save-as-preflight"); let primary_options = named_options(temp.path(), "primary-save-as-preflight"); let sidecar_options = named_options(temp.path(), "sidecar-save-as-preflight"); stored_session::save_snapshot(&snapshot_for_board("transparent", 17), &primary_options) .expect("save existing target"); - std::fs::write(sidecar_options.clear_marker_file_path(), b"stale clear").expect("stale clear"); + fs::write(sidecar_options.clear_marker_file_path(), b"stale clear").expect("stale clear"); let mut session_state = SessionState::new(Some(current_options)); assert!( @@ -416,9 +425,9 @@ fn runtime_save_as_overwrite_preflight_reports_existing_artifacts() { #[test] fn runtime_save_as_overwrite_preflight_skips_current_target() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-save-as-preflight-self"); - std::fs::write(current_options.backup_file_path(), b"stale backup").expect("stale backup"); + fs::write(current_options.backup_file_path(), b"stale backup").expect("stale backup"); let mut session_state = SessionState::new(Some(current_options.clone())); assert!( @@ -432,25 +441,24 @@ fn runtime_save_as_overwrite_preflight_skips_current_target() { #[test] fn runtime_save_as_confirmed_overwrite_removes_stale_sidecars_but_keeps_lock() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-save-as-overwrite"); let target_options = named_options(temp.path(), "target-save-as-overwrite"); - std::fs::write(target_options.session_file_path(), b"old primary").expect("old primary"); - std::fs::write(target_options.backup_file_path(), b"old backup").expect("old backup"); - std::fs::write(target_options.recovery_file_path(), b"old recovery").expect("old recovery"); + fs::write(target_options.session_file_path(), b"old primary").expect("old primary"); + fs::write(target_options.backup_file_path(), b"old backup").expect("old backup"); + fs::write(target_options.recovery_file_path(), b"old recovery").expect("old recovery"); let preserved_recovery = target_options .recovery_file_path() .with_extension("recovery.preserved"); - std::fs::write(&preserved_recovery, b"old preserved recovery").expect("old preserved recovery"); + fs::write(&preserved_recovery, b"old preserved recovery").expect("old preserved recovery"); let preserved_newer = PathBuf::from(format!( "{}.v99-preserved-0123456789abcdef", target_options.session_file_path().display() )); - std::fs::write(&preserved_newer, b"old newer-version session") - .expect("old newer-version session"); - std::fs::write(target_options.clear_marker_file_path(), b"old clear").expect("old clear"); - std::fs::write(target_options.lock_file_path(), b"lock").expect("old lock"); + fs::write(&preserved_newer, b"old newer-version session").expect("old newer-version session"); + fs::write(target_options.clear_marker_file_path(), b"old clear").expect("old clear"); + fs::write(target_options.lock_file_path(), b"lock").expect("old lock"); let mut input = test_input_state(); add_line(&mut input, 63); @@ -459,7 +467,7 @@ fn runtime_save_as_confirmed_overwrite_removes_stale_sidecars_but_keeps_lock() { save_named_session_as_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &target_options.session_file_path(), stored_session::SaveAsOverwrite::ConfirmReplace, @@ -487,15 +495,15 @@ fn runtime_save_as_confirmed_overwrite_removes_stale_sidecars_but_keeps_lock() { #[test] fn runtime_save_as_current_target_noop_does_not_require_overwrite_cleanup() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-save-as-self"); - std::fs::write(current_options.backup_file_path(), b"stale backup").expect("stale backup"); + fs::write(current_options.backup_file_path(), b"stale backup").expect("stale backup"); let mut input = test_input_state(); let mut session_state = SessionState::new(Some(current_options.clone())); let report = save_named_session_as_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, ¤t_options.session_file_path(), stored_session::SaveAsOverwrite::Deny, @@ -520,10 +528,9 @@ fn runtime_save_as_current_target_noop_does_not_require_overwrite_cleanup() { #[cfg(unix)] #[test] fn runtime_save_as_rejects_symlink_before_current_target_shortcut() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-save-as-symlink"); - std::fs::write(current_options.session_file_path(), b"current primary") - .expect("current primary"); + fs::write(current_options.session_file_path(), b"current primary").expect("current primary"); let symlink_path = temp.path().join("current-save-as-link.wayscriber-session"); symlink(current_options.session_file_path(), &symlink_path).expect("target symlink"); @@ -531,7 +538,7 @@ fn runtime_save_as_rejects_symlink_before_current_target_shortcut() { let mut session_state = SessionState::new(Some(current_options.clone())); let err = save_named_session_as_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &symlink_path, stored_session::SaveAsOverwrite::Deny, @@ -550,13 +557,12 @@ fn runtime_save_as_rejects_symlink_before_current_target_shortcut() { #[test] fn runtime_clear_persists_boundary_then_clears_live_session() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-runtime-clear"); stored_session::save_snapshot(&snapshot_for_board("transparent", 12), ¤t_options) .expect("seed saved session"); - std::fs::write(current_options.backup_file_path(), b"stale backup").expect("stale backup"); - std::fs::write(current_options.recovery_file_path(), b"stale recovery") - .expect("stale recovery"); + fs::write(current_options.backup_file_path(), b"stale backup").expect("stale backup"); + fs::write(current_options.recovery_file_path(), b"stale recovery").expect("stale recovery"); let mut input = test_input_state(); add_line(&mut input, 77); @@ -567,7 +573,7 @@ fn runtime_clear_persists_boundary_then_clears_live_session() { let report = clear_current_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, Instant::now(), ) @@ -592,12 +598,11 @@ fn runtime_clear_persists_boundary_then_clears_live_session() { #[test] fn runtime_clear_primary_cleanup_failure_after_marker_still_clears_live_session() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let mut current_options = SessionOptions::new(temp.path().to_path_buf(), "runtime-clear-cleanup-fail"); current_options.persist_transparent = true; - std::fs::create_dir(current_options.session_file_path()) - .expect("primary cleanup failure fixture"); + fs::create_dir(current_options.session_file_path()).expect("primary cleanup failure fixture"); let mut input = test_input_state(); add_line(&mut input, 83); @@ -608,7 +613,7 @@ fn runtime_clear_primary_cleanup_failure_after_marker_still_clears_live_session( let report = clear_current_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, Instant::now(), ) @@ -629,22 +634,22 @@ fn runtime_clear_primary_cleanup_failure_after_marker_still_clears_live_session( #[cfg(unix)] #[test] fn runtime_clear_persistence_failure_leaves_live_session_unchanged() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-runtime-clear-fail"); let current_target = temp.path().join("current-runtime-clear-symlink-target"); - std::fs::write(¤t_target, b"preserve current target").expect("write target"); + fs::write(¤t_target, b"preserve current target").expect("write target"); symlink(¤t_target, current_options.session_file_path()).expect("current symlink"); let mut input = test_input_state(); add_line(&mut input, 91); input.mark_session_dirty(); - let mut session_state = SessionState::new(Some(current_options.clone())); + let mut session_state = SessionState::new(Some(current_options)); session_state.commit_output_options(session_state.options().unwrap().clone(), true); session_state.record_input_dirty(Instant::now(), true); let err = clear_current_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, Instant::now(), ) @@ -655,14 +660,14 @@ fn runtime_clear_persistence_failure_leaves_live_session_unchanged() { assert!(session_state.is_dirty()); assert!(session_state.has_loaded_board_data()); assert_eq!( - std::fs::read(¤t_target).expect("current target bytes"), + fs::read(¤t_target).expect("current target bytes"), b"preserve current target" ); } #[test] fn runtime_clear_saved_tool_state_resets_live_tools_and_preserves_saved_boards() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-tool-reset"); let mut saved = snapshot_for_board("transparent", 121); saved.tool_state = Some(sample_tool_state()); @@ -683,7 +688,7 @@ fn runtime_clear_saved_tool_state_resets_live_tools_and_preserves_saved_boards() config.ui.show_status_bar = true; let report = clear_saved_tool_state_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, stored_session::ToolStateSnapshot::from_config(&config), Instant::now(), @@ -729,7 +734,7 @@ fn runtime_clear_saved_tool_state_without_active_session_resets_live_tools_only( let report = clear_saved_tool_state_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, stored_session::ToolStateSnapshot::from_config(&config), Instant::now(), @@ -745,7 +750,7 @@ fn runtime_clear_saved_tool_state_without_active_session_resets_live_tools_only( #[test] fn runtime_open_success_commits_target_and_catalog_after_apply() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current"); let candidate_options = named_options(temp.path(), "candidate"); @@ -756,7 +761,7 @@ fn runtime_open_success_commits_target_and_catalog_after_apply() { let mut session_state = SessionState::new(Some(current_options.clone())); let report = open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -791,7 +796,7 @@ fn runtime_open_success_commits_target_and_catalog_after_apply() { #[test] fn runtime_open_marks_full_damage_after_replacing_boards() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-damage"); let candidate_options = named_options(temp.path(), "candidate-damage"); @@ -807,7 +812,7 @@ fn runtime_open_marks_full_damage_after_replacing_boards() { open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -822,13 +827,12 @@ fn runtime_open_marks_full_damage_after_replacing_boards() { #[test] fn runtime_open_uses_recoverable_backup_without_mutating_candidate_artifacts() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-before-backup-open"); let source_options = named_options(temp.path(), "source-contentful-backup"); stored_session::save_snapshot(&sample_snapshot(), &source_options) .expect("save source contentful session"); - let backup_bytes = - std::fs::read(source_options.session_file_path()).expect("source session bytes"); + let backup_bytes = fs::read(source_options.session_file_path()).expect("source session bytes"); let candidate_options = named_options(temp.path(), "candidate-recoverable-backup"); let contentless = stored_session::SessionSnapshot { @@ -838,22 +842,22 @@ fn runtime_open_uses_recoverable_backup_without_mutating_candidate_artifacts() { }; stored_session::save_snapshot(&contentless, &candidate_options) .expect("save contentless candidate primary"); - std::fs::remove_file(candidate_options.lock_file_path()).expect("remove save-created lock"); - std::fs::write(candidate_options.backup_file_path(), &backup_bytes) + fs::remove_file(candidate_options.lock_file_path()).expect("remove save-created lock"); + fs::write(candidate_options.backup_file_path(), &backup_bytes) .expect("seed recoverable backup"); - std::fs::write( + fs::write( candidate_options.backup_recovery_marker_file_path(), b"recoverable", ) .expect("seed recoverable backup marker"); let primary_before = - std::fs::read(candidate_options.session_file_path()).expect("candidate primary bytes"); + fs::read(candidate_options.session_file_path()).expect("candidate primary bytes"); let mut input = test_input_state(); let mut session_state = SessionState::new(Some(current_options)); open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_options.session_file_path(), Instant::now(), @@ -862,11 +866,11 @@ fn runtime_open_uses_recoverable_backup_without_mutating_candidate_artifacts() { assert_eq!(input.boards.active_frame().shapes.len(), 1); assert_eq!( - std::fs::read(candidate_options.session_file_path()).expect("candidate primary unchanged"), + fs::read(candidate_options.session_file_path()).expect("candidate primary unchanged"), primary_before ); assert_eq!( - std::fs::read(candidate_options.backup_file_path()).expect("backup unchanged"), + fs::read(candidate_options.backup_file_path()).expect("backup unchanged"), backup_bytes ); assert!( @@ -882,7 +886,7 @@ fn runtime_open_uses_recoverable_backup_without_mutating_candidate_artifacts() { #[test] fn runtime_open_replaces_boards_missing_from_candidate_snapshot() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-with-whiteboard"); let candidate_options = named_options(temp.path(), "candidate-transparent-only"); @@ -898,7 +902,7 @@ fn runtime_open_replaces_boards_missing_from_candidate_snapshot() { open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -912,7 +916,7 @@ fn runtime_open_replaces_boards_missing_from_candidate_snapshot() { #[test] fn runtime_open_resyncs_canvas_pointer_after_same_active_board_view_offset() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-pointer-cache"); let mut candidate_options = named_options(temp.path(), "candidate-pointer-cache"); @@ -932,7 +936,7 @@ fn runtime_open_resyncs_canvas_pointer_after_same_active_board_view_offset() { open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -946,7 +950,7 @@ fn runtime_open_resyncs_canvas_pointer_after_same_active_board_view_offset() { #[test] fn runtime_open_releases_old_board_slots_for_candidate_boards() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-full-board-slots"); let candidate_options = named_options(temp.path(), "candidate-custom-board"); @@ -965,7 +969,7 @@ fn runtime_open_releases_old_board_slots_for_candidate_boards() { open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -979,7 +983,7 @@ fn runtime_open_releases_old_board_slots_for_candidate_boards() { #[test] fn runtime_open_apply_capacity_failure_keeps_current_session_active() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-apply-failure"); let candidate_options = named_options(temp.path(), "candidate-too-many-boards"); @@ -995,7 +999,7 @@ fn runtime_open_apply_capacity_failure_keeps_current_session_active() { }; stored_session::save_snapshot(&oversized_snapshot, &candidate_options) .expect("save oversized candidate"); - std::fs::remove_file(candidate_options.lock_file_path()).expect("remove save-created lock"); + fs::remove_file(candidate_options.lock_file_path()).expect("remove save-created lock"); let mut input = input; add_line(&mut input, 121); @@ -1003,7 +1007,7 @@ fn runtime_open_apply_capacity_failure_keeps_current_session_active() { let err = open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_options.session_file_path(), Instant::now(), @@ -1037,7 +1041,7 @@ fn runtime_open_apply_capacity_failure_keeps_current_session_active() { #[test] fn runtime_open_rejects_full_candidate_snapshot_that_omits_overlay_board() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-overlay-preserved"); let candidate_options = named_options(temp.path(), "candidate-without-overlay"); @@ -1059,12 +1063,12 @@ fn runtime_open_rejects_full_candidate_snapshot_that_omits_overlay_board() { }; stored_session::save_snapshot(&full_non_overlay_snapshot, &candidate_options) .expect("save full candidate"); - std::fs::remove_file(candidate_options.lock_file_path()).expect("remove save-created lock"); + fs::remove_file(candidate_options.lock_file_path()).expect("remove save-created lock"); let mut session_state = SessionState::new(Some(current_options.clone())); let err = open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_options.session_file_path(), Instant::now(), @@ -1097,7 +1101,7 @@ fn runtime_open_rejects_full_candidate_snapshot_that_omits_overlay_board() { #[test] fn runtime_open_clears_deleted_page_restore_state_from_previous_session() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-with-deleted-page"); let candidate_options = named_options(temp.path(), "candidate-after-deleted-page"); @@ -1115,7 +1119,7 @@ fn runtime_open_clears_deleted_page_restore_state_from_previous_session() { open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -1129,7 +1133,7 @@ fn runtime_open_clears_deleted_page_restore_state_from_previous_session() { #[test] fn runtime_open_cancels_active_interaction_from_previous_session() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-active-interaction"); let candidate_options = named_options(temp.path(), "candidate-after-interaction"); @@ -1149,7 +1153,7 @@ fn runtime_open_cancels_active_interaction_from_previous_session() { open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -1163,7 +1167,7 @@ fn runtime_open_cancels_active_interaction_from_previous_session() { #[test] fn runtime_open_closes_active_board_picker_drag() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-board-picker-drag"); let candidate_options = named_options(temp.path(), "candidate-after-board-picker-drag"); @@ -1171,7 +1175,7 @@ fn runtime_open_closes_active_board_picker_drag() { let candidate_path = candidate_options.session_file_path(); let mut input = test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); assert!(input.board_picker_start_drag(0)); assert!(input.is_board_picker_open()); assert!(input.board_picker_is_dragging()); @@ -1179,7 +1183,7 @@ fn runtime_open_closes_active_board_picker_drag() { open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -1194,7 +1198,7 @@ fn runtime_open_closes_active_board_picker_drag() { #[test] fn runtime_open_closes_active_board_picker_page_drag() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-board-picker-page-drag"); let candidate_options = named_options(temp.path(), "candidate-after-board-picker-page-drag"); @@ -1202,7 +1206,7 @@ fn runtime_open_closes_active_board_picker_page_drag() { let candidate_path = candidate_options.session_file_path(); let mut input = test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); assert!(input.board_picker_start_page_drag(0)); assert!(input.is_board_picker_open()); assert!(input.board_picker_is_page_dragging()); @@ -1210,7 +1214,7 @@ fn runtime_open_closes_active_board_picker_page_drag() { open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -1225,9 +1229,9 @@ fn runtime_open_closes_active_board_picker_page_drag() { #[test] fn runtime_open_saves_current_after_canceling_active_selection_move() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-active-selection-move"); let candidate_options = named_options(temp.path(), "candidate-after-selection-move"); @@ -1252,7 +1256,7 @@ fn runtime_open_saves_current_after_canceling_active_selection_move() { open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -1275,9 +1279,9 @@ fn runtime_open_saves_current_after_canceling_active_selection_move() { #[test] fn runtime_open_saves_current_after_canceling_active_text_edit() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-active-text-edit"); let candidate_options = named_options(temp.path(), "candidate-after-text-edit"); @@ -1317,7 +1321,7 @@ fn runtime_open_saves_current_after_canceling_active_text_edit() { open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -1343,7 +1347,7 @@ fn runtime_open_saves_current_after_canceling_active_text_edit() { #[test] fn runtime_open_saves_current_after_canceling_color_picker_preview() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-color-picker-preview"); let candidate_options = named_options(temp.path(), "candidate-after-color-picker"); @@ -1354,7 +1358,7 @@ fn runtime_open_saves_current_after_canceling_color_picker_preview() { let original = input.color_for_tool(Tool::Pen); input.open_color_picker_popup(); input.color_picker_popup_set_from_gradient(0.6, 0.1); - input.color_picker_popup_set_dragging(Some(crate::input::state::PickerDrag::SatVal)); + input.color_picker_popup_set_dragging(Some(PickerDrag::SatVal)); assert_ne!(input.color_for_tool(Tool::Pen), original); input.mark_session_dirty(); let mut session_state = SessionState::new(Some(current_options.clone())); @@ -1362,7 +1366,7 @@ fn runtime_open_saves_current_after_canceling_color_picker_preview() { open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -1384,12 +1388,12 @@ fn runtime_open_saves_current_after_canceling_color_picker_preview() { #[cfg(unix)] #[test] fn runtime_open_current_save_failure_preserves_active_selection_move() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-active-save-fail"); let current_target = temp.path().join("current-active-symlink-target"); - std::fs::write(¤t_target, b"preserve current target").expect("write target"); + fs::write(¤t_target, b"preserve current target").expect("write target"); symlink(¤t_target, current_options.session_file_path()).expect("current symlink"); let candidate_options = named_options(temp.path(), "candidate-active-save-fail"); @@ -1408,12 +1412,12 @@ fn runtime_open_current_save_failure_preserves_active_selection_move() { moved: true, }; input.mark_session_dirty(); - let mut session_state = SessionState::new(Some(current_options.clone())); + let mut session_state = SessionState::new(Some(current_options)); session_state.commit_output_options(session_state.options().unwrap().clone(), true); let err = open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_options.session_file_path(), Instant::now(), @@ -1429,7 +1433,7 @@ fn runtime_open_current_save_failure_preserves_active_selection_move() { assert_eq!((*x1, *y1, *x2, *y2), (100, 0, 109, 10)); assert!(session_state.is_dirty()); assert_eq!( - std::fs::read(¤t_target).expect("current target bytes"), + fs::read(¤t_target).expect("current target bytes"), b"preserve current target" ); } @@ -1437,11 +1441,11 @@ fn runtime_open_current_save_failure_preserves_active_selection_move() { #[cfg(unix)] #[test] fn runtime_open_current_save_failure_preserves_spatial_index_for_active_selection_move() { - let measurer = crate::draw::TextMeasurer::default(); - let temp = crate::test_temp::tempdir().expect("tempdir"); + let measurer = TextMeasurer::default(); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-spatial-save-fail"); let current_target = temp.path().join("current-spatial-symlink-target"); - std::fs::write(¤t_target, b"preserve current target").expect("write target"); + fs::write(¤t_target, b"preserve current target").expect("write target"); symlink(¤t_target, current_options.session_file_path()).expect("current symlink"); let candidate_options = named_options(temp.path(), "candidate-spatial-save-fail"); @@ -1479,12 +1483,12 @@ fn runtime_open_current_save_failure_preserves_spatial_index_for_active_selectio moved: true, }; input.mark_session_dirty(); - let mut session_state = SessionState::new(Some(current_options.clone())); + let mut session_state = SessionState::new(Some(current_options)); session_state.commit_output_options(session_state.options().unwrap().clone(), true); let err = open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_options.session_file_path(), Instant::now(), @@ -1500,14 +1504,14 @@ fn runtime_open_current_save_failure_preserves_spatial_index_for_active_selectio "hit testing should use the restored in-progress selection position" ); assert_eq!( - std::fs::read(¤t_target).expect("current target bytes"), + fs::read(¤t_target).expect("current target bytes"), b"preserve current target" ); } #[test] fn runtime_open_clears_stale_selection_and_hit_cache() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-selected-shape"); let candidate_options = named_options(temp.path(), "candidate-reuses-shape-id"); @@ -1535,7 +1539,7 @@ fn runtime_open_clears_stale_selection_and_hit_cache() { open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_path, Instant::now(), @@ -1548,11 +1552,11 @@ fn runtime_open_clears_stale_selection_and_hit_cache() { #[test] fn runtime_open_candidate_failure_after_current_save_keeps_current_active() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let _env = EnvGuard::set_xdg_data_home(temp.path()); let current_options = named_options(temp.path(), "current-save-first"); let candidate_options = named_options(temp.path(), "candidate-corrupt"); - std::fs::write(candidate_options.session_file_path(), b"{not valid json") + fs::write(candidate_options.session_file_path(), b"{not valid json") .expect("write corrupt candidate"); let mut input = test_input_state(); @@ -1563,7 +1567,7 @@ fn runtime_open_candidate_failure_after_current_save_keeps_current_active() { let err = open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_options.session_file_path(), Instant::now(), @@ -1590,7 +1594,7 @@ fn runtime_open_candidate_failure_after_current_save_keeps_current_active() { ); assert!(!session_state.is_dirty()); assert_eq!( - std::fs::read(candidate_options.session_file_path()).expect("candidate bytes"), + fs::read(candidate_options.session_file_path()).expect("candidate bytes"), b"{not valid json" ); assert_no_candidate_sidecars(&candidate_options); @@ -1607,11 +1611,11 @@ fn runtime_open_candidate_failure_after_current_save_keeps_current_active() { #[cfg(unix)] #[test] fn runtime_open_rejects_readonly_candidate_parent_before_commit() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_dir = temp.path().join("current"); let candidate_dir = temp.path().join("candidate"); - std::fs::create_dir(¤t_dir).expect("create current dir"); - std::fs::create_dir(&candidate_dir).expect("create candidate dir"); + fs::create_dir(¤t_dir).expect("create current dir"); + fs::create_dir(&candidate_dir).expect("create candidate dir"); let current_options = named_options(¤t_dir, "current"); let candidate_options = named_options(&candidate_dir, "candidate"); stored_session::save_snapshot(&sample_snapshot(), &candidate_options).expect("save candidate"); @@ -1625,7 +1629,7 @@ fn runtime_open_rejects_readonly_candidate_parent_before_commit() { let mut session_state = SessionState::new(Some(current_options.clone())); let err = open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_options.session_file_path(), Instant::now(), @@ -1647,10 +1651,10 @@ fn runtime_open_rejects_readonly_candidate_parent_before_commit() { #[cfg(unix)] #[test] fn runtime_open_current_save_failure_aborts_before_candidate_load() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let current_options = named_options(temp.path(), "current-save-fail"); let current_target = temp.path().join("current-symlink-target"); - std::fs::write(¤t_target, b"preserve current target").expect("write target"); + fs::write(¤t_target, b"preserve current target").expect("write target"); symlink(¤t_target, current_options.session_file_path()).expect("current symlink"); let candidate_options = named_options(temp.path(), "candidate-valid"); @@ -1664,7 +1668,7 @@ fn runtime_open_current_save_failure_aborts_before_candidate_load() { let err = open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session_state, &candidate_options.session_file_path(), Instant::now(), @@ -1680,7 +1684,7 @@ fn runtime_open_current_save_failure_aborts_before_candidate_load() { ); assert!(session_state.is_dirty()); assert_eq!( - std::fs::read(¤t_target).expect("current target bytes"), + fs::read(¤t_target).expect("current target bytes"), b"preserve current target" ); } @@ -1763,7 +1767,7 @@ fn protected_session_path_blocks_save_until_session_is_dirty() { options.autosave_enabled = true; options.persist_transparent = true; let path = options.session_file_path(); - let mut state = SessionState::new(Some(options.clone())); + let mut state = SessionState::new(Some(options)); assert!(!state.should_skip_save_for_protected_path(&path, false)); state.protect_session_path(path.clone()); @@ -1781,8 +1785,8 @@ fn protected_session_path_blocks_save_until_session_is_dirty() { /// autosave or final save would replace the session that failed to restore. #[test] fn protected_session_path_survives_a_run_only_status_bar_toggle() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -1832,47 +1836,48 @@ fn load_baseline_clears_stale_dirty_before_protected_save_check() { #[test] fn contentless_save_guard_blocks_clean_unloaded_save_when_primary_exists() { assert!(should_skip_unloaded_contentless_save( - false, false, false, false, true, + ContentlessSaveCheck { + session_artifact_exists: true, + ..ContentlessSaveCheck::default() + } )); } #[test] fn contentless_save_guard_blocks_clean_unloaded_save_when_only_backup_exists() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let options = SessionOptions::new(temp.path().to_path_buf(), "backup-only"); - std::fs::write(options.backup_file_path(), b"backup").expect("backup write"); + fs::write(options.backup_file_path(), b"backup").expect("backup write"); assert!(has_session_artifact(&options)); assert!(should_skip_unloaded_contentless_save( - false, - false, - false, - false, - has_session_artifact(&options), + ContentlessSaveCheck { + session_artifact_exists: has_session_artifact(&options), + ..ContentlessSaveCheck::default() + } )); } #[test] fn contentless_save_guard_blocks_clean_unloaded_save_when_only_recovery_exists() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let options = SessionOptions::new(temp.path().to_path_buf(), "recovery-only"); - std::fs::write(options.recovery_file_path(), b"recovery").expect("recovery write"); + fs::write(options.recovery_file_path(), b"recovery").expect("recovery write"); assert!(has_session_artifact(&options)); assert!(should_skip_unloaded_contentless_save( - false, - false, - false, - false, - has_session_artifact(&options), + ContentlessSaveCheck { + session_artifact_exists: has_session_artifact(&options), + ..ContentlessSaveCheck::default() + } )); } #[test] fn contentless_save_guard_blocks_clean_loaded_empty_save_when_only_preserved_recovery_exists() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let options = SessionOptions::new(temp.path().to_path_buf(), "preserved-recovery"); - std::fs::write( + fs::write( options .recovery_file_path() .with_extension("recovery.empty"), @@ -1882,34 +1887,49 @@ fn contentless_save_guard_blocks_clean_loaded_empty_save_when_only_preserved_rec assert!(has_session_artifact(&options)); assert!(should_skip_unloaded_contentless_save( - false, - false, - false, - false, - has_session_artifact(&options), + ContentlessSaveCheck { + session_artifact_exists: has_session_artifact(&options), + ..ContentlessSaveCheck::default() + } )); } #[test] fn contentless_save_guard_allows_real_board_data_or_dirty_state() { + let clean_with_artifact = ContentlessSaveCheck { + session_artifact_exists: true, + ..ContentlessSaveCheck::default() + }; assert!(!should_skip_unloaded_contentless_save( - false, false, false, true, true, + ContentlessSaveCheck { + has_board_data: true, + ..clean_with_artifact + } )); assert!(!should_skip_unloaded_contentless_save( - false, true, false, false, true, + ContentlessSaveCheck { + session_dirty: true, + ..clean_with_artifact + } )); assert!(!should_skip_unloaded_contentless_save( - false, false, true, false, true, + ContentlessSaveCheck { + input_dirty: true, + ..clean_with_artifact + } )); assert!(!should_skip_unloaded_contentless_save( - true, false, false, false, true, + ContentlessSaveCheck { + loaded_board_data: true, + ..clean_with_artifact + } )); } #[test] fn contentless_save_guard_allows_noop_when_no_session_artifact_exists() { assert!(!should_skip_unloaded_contentless_save( - false, false, false, false, false, + ContentlessSaveCheck::default() )); } @@ -1934,10 +1954,10 @@ fn autosave_completion_does_not_clear_newer_generation() { let newer_edit = started + Duration::from_millis(2); state.record_input_dirty(newer_edit, true); let result = Ok(SaveCompletion { - report: Some(crate::session::SaveSnapshotReport { + report: Some(SaveSnapshotReport { generation: None, path: PathBuf::from("/tmp/generation-test.json"), - outcome: crate::session::SaveSnapshotOutcome::Full, + outcome: SaveSnapshotOutcome::Full, raw_size: 1, written_size: 1, max_file_size_bytes: 1024, @@ -2023,7 +2043,7 @@ fn output_transition_deferral_moves_deadline_forward() { fn drive_session_command( input: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, session: &mut SessionState, command: SessionCommand, ) -> Result { @@ -2040,7 +2060,7 @@ fn drive_session_command( fn open_named_session_runtime( input: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, session: &mut SessionState, target: &Path, _now: Instant, @@ -2058,7 +2078,7 @@ fn open_named_session_runtime( } fn save_named_session_as_runtime( input: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, session: &mut SessionState, target: &Path, overwrite: stored_session::SaveAsOverwrite, @@ -2082,7 +2102,7 @@ fn save_named_session_as_requires_overwrite( let mut input = test_input_state(); let SessionCommandReport::Overwrite(_, required) = drive_session_command( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), session, SessionCommand::CheckOverwrite(target.to_path_buf()), )? @@ -2093,7 +2113,7 @@ fn save_named_session_as_requires_overwrite( } fn clear_current_session_runtime( input: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, session: &mut SessionState, _now: Instant, ) -> Result { @@ -2106,7 +2126,7 @@ fn clear_current_session_runtime( } fn clear_saved_tool_state_runtime( input: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, session: &mut SessionState, defaults: stored_session::ToolStateSnapshot, _now: Instant, @@ -2125,18 +2145,18 @@ fn clear_saved_tool_state_runtime( #[test] fn explicit_clear_keeps_dispatch_available_and_preserves_edits_during_blocked_disk_work() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = named_options(temp.path(), "blocked-clear"); stored_session::save_snapshot(&sample_snapshot(), &options).unwrap(); - let lock = std::fs::OpenOptions::new() + let lock = OpenOptions::new() .read(true) .write(true) .open(options.lock_file_path()) .unwrap(); - crate::durable_io::lock_exclusive(&lock).unwrap(); + durable_io::lock_exclusive(&lock).unwrap(); let mut input = test_input_state(); add_line(&mut input, 51); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut session = SessionState::new(Some(options.clone())); let mut persistence = PersistenceController::start_for_test().unwrap(); let mut transaction = SessionCommandTransaction::new( @@ -2211,13 +2231,13 @@ fn explicit_clear_keeps_dispatch_available_and_preserves_edits_during_blocked_di #[test] fn explicit_open_rejects_a_completion_after_target_epoch_changes() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = named_options(temp.path(), "epoch-source"); let target = named_options(temp.path(), "epoch-candidate"); stored_session::save_snapshot(&sample_snapshot(), &target).unwrap(); let mut input = test_input_state(); add_line(&mut input, 91); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut session = SessionState::new(Some(options)); let mut persistence = PersistenceController::start_for_test().unwrap(); let mut transaction = SessionCommandTransaction::new( @@ -2261,11 +2281,11 @@ fn explicit_open_rejects_a_completion_after_target_epoch_changes() { #[cfg(unix)] #[test] fn runtime_open_validates_target_before_saving_dirty_current_session() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "unsaved-source"); let candidate = named_options(temp.path(), "symlink-candidate"); let destination = temp.path().join("keep-unrelated-bytes"); - std::fs::write(&destination, b"unrelated content").unwrap(); + fs::write(&destination, b"unrelated content").unwrap(); symlink(&destination, candidate.session_file_path()).unwrap(); let mut input = test_input_state(); add_line(&mut input, 63); @@ -2274,7 +2294,7 @@ fn runtime_open_validates_target_before_saving_dirty_current_session() { let error = open_named_session_runtime( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut session, &candidate.session_file_path(), Instant::now(), @@ -2292,18 +2312,18 @@ fn runtime_open_validates_target_before_saving_dirty_current_session() { session.options().unwrap().session_file_path(), current.session_file_path() ); - assert_eq!(std::fs::read(destination).unwrap(), b"unrelated content"); + assert_eq!(fs::read(destination).unwrap(), b"unrelated content"); } #[test] fn pending_session_handoffs_preserve_new_unfinished_pointer_and_text_drafts() { - fn begin_draft(input: &mut InputState, measurer: &crate::draw::TextMeasurer, kind: &str) { + fn begin_draft(input: &mut InputState, measurer: &TextMeasurer, kind: &str) { if kind == "pointer" { - input.on_mouse_press(crate::input::MouseButton::Left, 80, 80); + input.on_mouse_press(MouseButton::Left, 80, 80); input.on_mouse_motion(90, 90); assert!(matches!(input.state, DrawingState::Drawing { .. })); } else { - let ui_engine = crate::ui_text::UiTextEngine::default(); + let ui_engine = UiTextEngine::default(); input.handle_action_with_resources( crate::input::state::InputTextResources { measurer, @@ -2311,7 +2331,7 @@ fn pending_session_handoffs_preserve_new_unfinished_pointer_and_text_drafts() { }, Action::EnterTextMode, ); - input.on_key_press(crate::input::Key::Char('x')); + input.on_key_press(Key::Char('x')); assert!( matches!(&input.state, DrawingState::TextInput { buffer, .. } if buffer == "x") ); @@ -2333,7 +2353,7 @@ fn pending_session_handoffs_preserve_new_unfinished_pointer_and_text_drafts() { ("save_as", "snapshot"), ] { for draft_kind in ["pointer", "text"] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current = named_options(temp.path(), "draft-source"); let target = named_options(temp.path(), "draft-target"); if command_kind == "open" { @@ -2341,7 +2361,7 @@ fn pending_session_handoffs_preserve_new_unfinished_pointer_and_text_drafts() { } let mut input = test_input_state(); add_line(&mut input, 61); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut session = SessionState::new(Some(current.clone())); let mut worker = PersistenceController::start_for_test().unwrap(); let command = match command_kind { @@ -2437,15 +2457,15 @@ fn pending_session_handoffs_preserve_new_unfinished_pointer_and_text_drafts() { #[test] fn actual_corrupt_load_outcomes_publish_critical_preservation_notices() { for restore in [false, true] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = stored_session::SessionOptions::new(temp.path().to_path_buf(), "corrupt-toast"); options.persist_transparent = true; if restore { stored_session::save_snapshot(&sample_snapshot(), &options).unwrap(); - std::fs::rename(options.session_file_path(), options.backup_file_path()).unwrap(); + fs::rename(options.session_file_path(), options.backup_file_path()).unwrap(); } - std::fs::write(options.session_file_path(), b"unreadable primary").unwrap(); + fs::write(options.session_file_path(), b"unreadable primary").unwrap(); let outcome = stored_session::load_snapshot_with_outcome(&options).unwrap(); if restore { assert!(matches!( @@ -2461,14 +2481,14 @@ fn actual_corrupt_load_outcomes_publish_critical_preservation_notices() { let mut input = test_input_state(); apply_load_outcome( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), outcome, &options, "regression", ) .unwrap(); let toast = input.active_toast().unwrap(); - assert_eq!(toast.priority, crate::input::state::ToastPriority::Critical); + assert_eq!(toast.priority, ToastPriority::Critical); assert_eq!(toast.duration_ms, 20_000); assert!( toast.message.contains( diff --git a/src/backend/wayland/session/write_policy.rs b/src/backend/wayland/session/write_policy.rs index 98f4feefb..c80d05b3e 100644 --- a/src/backend/wayland/session/write_policy.rs +++ b/src/backend/wayland/session/write_policy.rs @@ -1,8 +1,13 @@ //! Which drawings may be written to the source session file. +use std::fmt::{Display, Formatter}; +use std::{error, fmt}; + use anyhow::Result; use std::path::Path; +use crate::i18n; + use super::{PersistenceOutcome, SessionState, stored_session}; impl SessionState { @@ -71,17 +76,17 @@ pub(in crate::backend::wayland) enum SourceWriteRefused { NotPreserved, } -impl std::fmt::Display for SourceWriteRefused { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for SourceWriteRefused { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter.write_str(match self { - Self::NotLoaded => crate::i18n::tr( + Self::NotLoaded => i18n::tr( "the source session has not loaded; new drawings are kept on screen and saved drawings are untouched; use Save As to keep new drawings elsewhere, or Clear before reloading", ), - Self::NotPreserved => crate::i18n::tr( + Self::NotPreserved => i18n::tr( "unreadable session data was not preserved; repair and reload its files or use Save As before replacing these drawings", ), }) } } -impl std::error::Error for SourceWriteRefused {} +impl error::Error for SourceWriteRefused {} diff --git a/src/backend/wayland/state.rs b/src/backend/wayland/state.rs index 9e8efdaaf..cc2aa578a 100644 --- a/src/backend/wayland/state.rs +++ b/src/backend/wayland/state.rs @@ -1,59 +1,45 @@ // Holds the live Wayland protocol state shared by the backend loop and the handler // submodules; provides rendering, capture routing, and overlay helpers used across them. -use anyhow::{Context, Result}; -use log::{debug, info, warn}; -use smithay_client_toolkit::{ - activation::{ActivationHandler, RequestData}, - globals::ProvidesBoundGlobal, - shell::wlr_layer::KeyboardInteractivity, -}; -use std::time::{Duration, Instant}; -use wayland_client::{QueueHandle, protocol::wl_output}; +use anyhow::Result; +use std::time::Instant; #[cfg(feature = "tablet-input")] use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_manager_v2::ZwpTabletManagerV2; -use wayland_protocols::wp::{ - pointer_constraints::zv1::client::zwp_pointer_constraints_v1, - text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3, -}; +use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; +use crate::backend::ExitAfterCaptureMode; +use crate::backend::wayland::RuntimeWakeHandle; +use crate::backend::wayland::runtime_ui_state::ToolbarRuntimeState; +use crate::backend::wayland::session::{ + PersistenceController, SessionCommandTransaction, SessionHome, +}; +use crate::capture::CaptureManager; +use crate::config::Config; +use crate::daemon::protocol_v2::ClaimedAction; +use crate::desktop_open::DesktopOpenRequest; +use crate::draw::TextMeasurer; +use crate::input::InputState; #[cfg(feature = "tablet-input")] use crate::input::tablet::TabletSettings; -use crate::{ - backend::ExitAfterCaptureMode, - canvas_export::{ - BoardExportSnapshot, BoardPdfExportSnapshot, CanvasExportBackdropSnapshot, - CanvasExportSnapshot, CanvasExportViewport, render_board_pdf, render_canvas_png, - }, - capture::{ - CaptureDestination, CaptureManager, DesktopBackdropCaptureRequest, - DesktopBackdropCaptureResult, DesktopBackdropGeometry, DesktopBackdropOutputGeometry, - ImageFormatMetadata, ImageOperationKind, RenderedDocument, - file::{FileSaveConfig, expand_tilde}, - types::CaptureType, - }, - config::{Action, Config}, - desktop_open::DesktopOpenRequest, - input::{DrawingState, EraserMode, InputState, Tool, ZoomAction}, - session::SessionOptions, - ui::toolbar::{ToolbarBindingHints, ToolbarEvent, ToolbarSnapshot}, -}; +use crate::ocr::OcrController; +use crate::onboarding::OnboardingStore; +use crate::palette_recents::PaletteRecentsWriter; +use crate::session::SessionOptions; +use crate::toolbar_gtk::GtkToolbarBridge; +use crate::ui::toolbar::RuntimeUiPersistenceSnapshot; +use crate::ui_text::UiTextEngine; -pub(in crate::backend::wayland) use self::core::overlay::{ - OverlaySuppression, OverlaySuppressionKeyboardPolicy, -}; +pub(in crate::backend::wayland) use self::core::overlay::OverlaySuppression; +#[cfg(test)] +pub(in crate::backend::wayland) use self::core::overlay::OverlaySuppressionKeyboardPolicy; pub(in crate::backend::wayland) use self::region_capture::WindowSnapDirection; pub(in crate::backend::wayland) use self::toolbar::{MoveDragKind, ToolbarChrome, ToolbarDrag}; -use super::{ - RuntimeOperationController, RuntimeOperationIdSource, - capture::{CapturePreflightRequest, CaptureState, PendingPdfExport}, - frozen::{ExtImageCopyManagers, FrozenState}, - overlay_passthrough::set_surface_clickthrough, - session::SessionState, - surface::{SurfacePlacement, SurfaceState}, - toolbar::{ToolbarSurfaceManager, layout::top_size, render::render_top_strip}, - toolbar_intent::intent_to_event, - zoom::ZoomState, -}; +use super::RuntimeOperationController; +use super::capture::CaptureState; +use super::frozen::{ExtImageCopyManagers, FrozenState}; +use super::session::SessionState; +use super::surface::SurfaceState; +use super::toolbar::ToolbarSurfaceManager; +use super::zoom::ZoomState; mod acquisition; mod activation; @@ -96,10 +82,9 @@ mod protocol_globals; mod seat_devices; pub(in crate::backend::wayland) use protocol_globals::{ProtocolGlobals, ProtocolGlobalsSeed}; mod region_capture; -pub(in crate::backend::wayland) use region_capture::RegionCaptureIntent; #[cfg(test)] pub(in crate::backend::wayland) use region_capture::RegionPickerOptions; -pub(in crate::backend::wayland) use region_capture::RegionReviewPress; +pub(in crate::backend::wayland) use region_capture::{RegionCaptureIntent, RegionReviewPress}; mod render; mod screen_image; mod spotlight_runtime; @@ -119,38 +104,40 @@ mod zoom; #[cfg(test)] mod tests; -type ScreencopyManager = wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1; +type ScreencopyManager = ZwlrScreencopyManagerV1; pub(in crate::backend::wayland) use self::buffer_damage::FullDamageReason; pub(in crate::backend::wayland) use self::perf::{ - PerfDamageDiagnostics, PerfFrameDamageContext, PerfInputSource, PerfRenderBreakdown, - PerfRenderProfileKind, PerfRenderSkipReason, damage_covers_logical_surface, + PerfDamageDiagnostics, PerfInputSource, PerfRenderSkipReason, }; pub(in crate::backend::wayland) use self::render::RenderOutcome; +#[cfg(test)] +pub(super) use helpers::damage_summary; pub(super) use helpers::{ - damage_summary, debug_damage_logging_enabled, debug_toolbar_drag_logging_enabled, drag_log, - force_inline_toolbars_requested, scale_damage_regions, surface_id, - toolbar_drag_preview_enabled, toolbar_drag_throttle_interval, toolbar_pointer_lock_enabled, + debug_toolbar_drag_logging_enabled, drag_log, force_inline_toolbars_requested, + scale_damage_regions, surface_id, }; +use tokio::runtime::Handle; +use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1; pub(in crate::backend::wayland) struct WaylandStateInit { - pub ui_text: crate::ui_text::UiTextEngine, - pub text_measurer: crate::draw::TextMeasurer, + pub ui_text: UiTextEngine, + pub text_measurer: TextMeasurer, pub globals: ProtocolGlobals, pub config: Config, pub input_state: InputState, pub startup_activation_token: Option, - pub onboarding: crate::onboarding::OnboardingStore, - pub palette_recents: crate::palette_recents::PaletteRecentsWriter, + pub onboarding: OnboardingStore, + pub palette_recents: PaletteRecentsWriter, pub capture_manager: CaptureManager, pub session_options: Option, - pub session_home: crate::backend::wayland::session::SessionHome, + pub session_home: SessionHome, pub session_config_failed: bool, - pub persistence: crate::backend::wayland::session::PersistenceController, - pub runtime_ui: Option, - pub runtime_ui_unavailable: Option, - pub runtime_wake: crate::backend::wayland::RuntimeWakeHandle, - pub tokio_handle: tokio::runtime::Handle, + pub persistence: PersistenceController, + pub runtime_ui: Option, + pub runtime_ui_unavailable: Option, + pub runtime_wake: RuntimeWakeHandle, + pub tokio_handle: Handle, pub exit_after_capture_mode: ExitAfterCaptureMode, pub frozen_enabled: bool, pub preferred_output_identity: Option, @@ -212,9 +199,9 @@ pub(super) struct WaylandState { pub(super) acquisition: acquisition::AcquisitionRuntime, /// Capacity-one screen text recognition. A busy controller reports /// busy rather than queuing a region the user has moved on from. - pub(super) ocr: crate::ocr::OcrController, + pub(super) ocr: OcrController, /// GTK toolbar frontend; `None` means the built-in bars are in charge. - pub(super) gtk_toolbar: Option, + pub(super) gtk_toolbar: Option, /// Tick scheduling for toasts, highlights, and preset feedback. pub(super) ui_animation: ui_animation::UiAnimationClock, @@ -242,16 +229,15 @@ pub(super) struct WaylandState { // Session persistence pub(super) session: SessionState, - pub(super) session_home: crate::backend::wayland::session::SessionHome, - pub(super) persistence: crate::backend::wayland::session::PersistenceController, - pub(super) session_transaction: - Option, + pub(super) session_home: SessionHome, + pub(super) persistence: PersistenceController, + pub(super) session_transaction: Option, session_dialog: self::toolbar::SessionFileDialogController, - pub(super) durable_action_finish: Option, + pub(super) durable_action_finish: Option, pub(super) durable_action_retry_at: Option, // Tokio runtime handle for async operations - pub(super) tokio_handle: tokio::runtime::Handle, + pub(super) tokio_handle: Handle, } #[cfg(feature = "tablet-input")] diff --git a/src/backend/wayland/state/acquisition.rs b/src/backend/wayland/state/acquisition.rs index 2e62136e9..4b911d535 100644 --- a/src/backend/wayland/state/acquisition.rs +++ b/src/backend/wayland/state/acquisition.rs @@ -5,10 +5,12 @@ use crate::backend::wayland::acquisition::{ ScreenAcquisitionId, ScreenAcquisitionOutcome, ScreenAcquisitionOwner, ScreenAcquisitionRegistry, }; +use crate::backend::wayland::capture::RegionCapturePhase; use crate::backend::wayland::zoom::{ ZoomSourceOutcome, ZoomSourceTerminal, ZoomWaiter, ZoomWaiterOwner, ZoomWaiterRegistry, }; -use crate::input::state::{EyedropperCaptureSource, ScreenCaptureSource}; +use crate::input::InputState; +use crate::input::state::{EyedropperCaptureSource, RegionPurposeTag, ScreenCaptureSource}; use transaction::{ AcquisitionTransactionRuntime, cancel_modal_owner_resources, release_owned_generation, @@ -129,7 +131,7 @@ impl AcquisitionTransactionRuntime for WaylandState { self.validate_owner_waiter(record, completion) } - fn input_state(&mut self) -> &mut crate::input::InputState { + fn input_state(&mut self) -> &mut InputState { &mut self.input_state } @@ -219,11 +221,11 @@ impl WaylandState { pub(in crate::backend::wayland) fn resolve_zoom_waiter( &mut self, - terminal: ZoomSourceTerminal, + terminal: &ZoomSourceTerminal, ) { - let Some((waiter, matches)) = self.acquisition.take_zoom_waiter_for_terminal(&terminal) + let Some((waiter, matches)) = self.acquisition.take_zoom_waiter_for_terminal(terminal) else { - report_zoom_terminal_to(&mut self.input_state, None, &terminal); + report_zoom_terminal_to(&mut self.input_state, None, terminal); return; }; if !matches { @@ -231,13 +233,13 @@ impl WaylandState { report_inconsistent_capture_to(&mut self.input_state); return; } - if !self.validate_zoom_waiter(waiter, &terminal) { + if !self.validate_zoom_waiter(waiter, terminal) { self.cancel_zoom_owner_ui_only(waiter.owner); report_inconsistent_capture_to(&mut self.input_state); return; } - self.report_zoom_terminal(waiter.owner, &terminal); + self.report_zoom_terminal(waiter.owner, terminal); match terminal.outcome { ZoomSourceOutcome::Ready { installed_generation, @@ -271,7 +273,7 @@ impl WaylandState { pub(in crate::backend::wayland) fn resolve_pending_zoom_terminal(&mut self) { if let Some(terminal) = self.zoom.take_source_terminal() { - self.resolve_zoom_waiter(terminal); + self.resolve_zoom_waiter(&terminal); } } @@ -282,7 +284,7 @@ impl WaylandState { == Some(EyedropperCaptureSource::Zoom) } ZoomWaiterOwner::Ocr => self.region_capture.active().is_some_and(|region| { - region.purpose() == crate::input::state::RegionPurposeTag::Ocr + region.purpose() == RegionPurposeTag::Ocr && matches!( region, super::region_capture::ActiveScreenRegion::PendingZoom { .. } @@ -294,10 +296,7 @@ impl WaylandState { region, super::region_capture::ActiveScreenRegion::PendingZoom { .. } ) - && matches!( - self.capture.region_phase(), - crate::backend::wayland::capture::RegionCapturePhase::Reserved(_) - ) + && matches!(self.capture.region_phase(), RegionCapturePhase::Reserved(_)) }), }; if !waiting { @@ -337,7 +336,10 @@ impl WaylandState { owner: ScreenAcquisitionOwner, outcome: ScreenAcquisitionOutcome, ) { - debug_assert!(!matches!(outcome, ScreenAcquisitionOutcome::Ready { .. })); + debug_assert!( + !matches!(outcome, ScreenAcquisitionOutcome::Ready { .. }), + "a queued acquisition never started, so it cannot be ready, got {outcome:?}" + ); self.route_acquisition_completion(ScreenAcquisitionCompletion { id, owner, outcome }); } @@ -356,22 +358,23 @@ impl WaylandState { let waiting = match completion.owner { ScreenAcquisitionOwner::UserFreeze => true, ScreenAcquisitionOwner::Eyedropper => { - debug_assert_eq!(record.owner, ScreenAcquisitionOwner::Eyedropper); + debug_assert_eq!( + record.owner, + ScreenAcquisitionOwner::Eyedropper, + "the completion and its acquisition record name different owners" + ); self.input_state.eyedropper_state().pending_source() == Some(EyedropperCaptureSource::Frozen) } ScreenAcquisitionOwner::Ocr => self.region_capture.active().is_some_and(|region| { - region.purpose() == crate::input::state::RegionPurposeTag::Ocr + region.purpose() == RegionPurposeTag::Ocr && region.waits_for_acquisition(completion.id) }), ScreenAcquisitionOwner::RegionCapture => { self.region_capture.active().is_some_and(|region| { region.purpose().is_capture() && region.waits_for_acquisition(completion.id) - && matches!( - self.capture.region_phase(), - crate::backend::wayland::capture::RegionCapturePhase::Reserved(_) - ) + && matches!(self.capture.region_phase(), RegionCapturePhase::Reserved(_)) }) } }; diff --git a/src/backend/wayland/state/acquisition/transaction.rs b/src/backend/wayland/state/acquisition/transaction.rs index 50a55d9fa..d0103525d 100644 --- a/src/backend/wayland/state/acquisition/transaction.rs +++ b/src/backend/wayland/state/acquisition/transaction.rs @@ -3,6 +3,7 @@ use crate::backend::wayland::acquisition::{ ScreenAcquisitionOutcome, ScreenAcquisitionOwner, rejected_ready_generation, }; use crate::backend::wayland::zoom::{ZoomSourceOutcome, ZoomSourceTerminal, ZoomWaiterOwner}; +use crate::input::InputState; #[cfg(test)] use crate::input::state::{EyedropperCaptureSource, ScreenCaptureSource}; use crate::input::state::{Toast, ToastPriority}; @@ -19,7 +20,7 @@ const INCONSISTENT_CAPTURE_MESSAGE: &str = "Screen capture state was inconsisten const SOURCE_ACTIVATION_REJECTED_MESSAGE: &str = "Screen image changed before the selection could start; try again."; -pub(super) fn report_inconsistent_capture_to(input_state: &mut crate::input::InputState) { +pub(super) fn report_inconsistent_capture_to(input_state: &mut InputState) { input_state.push_toast( ToastPriority::Critical, "capture", @@ -28,7 +29,7 @@ pub(super) fn report_inconsistent_capture_to(input_state: &mut crate::input::Inp } pub(in crate::backend::wayland::state) fn report_screen_source_activation_rejected_to( - input_state: &mut crate::input::InputState, + input_state: &mut InputState, owner: ScreenAcquisitionOwner, ) { let source = match owner { @@ -85,7 +86,7 @@ fn zoom_terminal_report( } pub(in crate::backend::wayland::state) fn report_zoom_terminal_to( - input_state: &mut crate::input::InputState, + input_state: &mut InputState, owner: Option, terminal: &ZoomSourceTerminal, ) { @@ -96,7 +97,7 @@ pub(in crate::backend::wayland::state) fn report_zoom_terminal_to( } pub(super) fn report_screen_terminal_to( - input_state: &mut crate::input::InputState, + input_state: &mut InputState, owner: ScreenAcquisitionOwner, outcome: &ScreenAcquisitionOutcome, ) { @@ -163,7 +164,7 @@ pub(super) trait AcquisitionTransactionRuntime { record: &AcquisitionRecord, completion: &ScreenAcquisitionCompletion, ) -> bool; - fn input_state(&mut self) -> &mut crate::input::InputState; + fn input_state(&mut self) -> &mut InputState; fn finish_eyedropper_ready(&mut self, installed_generation: u64) -> bool; fn finish_ocr_ready(&mut self, installed_generation: u64) -> bool; fn finish_region_capture_ready(&mut self, installed_generation: u64) -> bool; @@ -229,7 +230,7 @@ fn cancel_owner_transition( fn fail_closed_acquisition_transaction( runtime: &mut R, record: Option, - completion: ScreenAcquisitionCompletion, + completion: &ScreenAcquisitionCompletion, ) { if record.is_some_and(|record| { record.stage == AcquisitionStage::Started @@ -266,12 +267,12 @@ pub(super) fn route_acquisition_transaction( .is_some_and(|record| record.id == completion.id && record.owner == completion.owner); let record = runtime.take_acquisition_record(); if !registry_matches { - fail_closed_acquisition_transaction(runtime, record, completion); + fail_closed_acquisition_transaction(runtime, record, &completion); return; } let record = record.expect("a matched screen acquisition record exists"); if !runtime.owner_waiter_matches(&record, &completion) { - fail_closed_acquisition_transaction(runtime, Some(record), completion); + fail_closed_acquisition_transaction(runtime, Some(record), &completion); return; } diff --git a/src/backend/wayland/state/acquisition/transaction/tests.rs b/src/backend/wayland/state/acquisition/transaction/tests.rs index 8640ef75b..93219f1dc 100644 --- a/src/backend/wayland/state/acquisition/transaction/tests.rs +++ b/src/backend/wayland/state/acquisition/transaction/tests.rs @@ -1,3 +1,8 @@ +use std::mem; + +use crate::backend::wayland::zoom::ZoomTerminalReport; +use crate::input::state::RegionPurposeTag; + use super::*; use crate::backend::wayland::acquisition::ScreenAcquisitionRegistry; use crate::input::state::test_support::make_test_input_state; @@ -5,7 +10,7 @@ use crate::input::state::test_support::make_test_input_state; struct TransactionRuntime { registry: ScreenAcquisitionRegistry, waiter_matches: bool, - input_state: crate::input::InputState, + input_state: InputState, finished_eyedropper: Vec, finished_ocr: Vec, finished_region_capture: Vec, @@ -82,7 +87,7 @@ impl AcquisitionTransactionRuntime for TransactionRuntime { self.waiter_matches } - fn input_state(&mut self) -> &mut crate::input::InputState { + fn input_state(&mut self) -> &mut InputState { &mut self.input_state } @@ -169,7 +174,7 @@ impl AcquisitionTransactionRuntime for TransactionRuntime { } fn take_frozen_capture_done(&mut self) -> bool { - std::mem::take(&mut self.capture_done) + mem::take(&mut self.capture_done) } fn frozen_suppressed(&self) -> bool { @@ -256,7 +261,7 @@ fn region_capture_zoom_abort_and_deactivation_cancel_quietly() { fn specific_zoom_failure_report_replaces_the_owner_fallback() { let terminal = ZoomSourceTerminal::for_test( ZoomSourceOutcome::Failed("specific backend failure".to_string()), - Some(crate::backend::wayland::zoom::ZoomTerminalReport { + Some(ZoomTerminalReport { source: "zoom", message: "specific backend failure".to_string(), }), @@ -434,13 +439,13 @@ fn ready_activation_rejection_releases_the_modal_owned_freeze_once() { .input_state .set_eyedropper_pending_capture(EyedropperCaptureSource::Frozen), ScreenAcquisitionOwner::Ocr => runtime.input_state.set_region_pending_capture( - crate::input::state::RegionPurposeTag::Ocr, + RegionPurposeTag::Ocr, 1, ScreenCaptureSource::Frozen, ), ScreenAcquisitionOwner::RegionCapture => { runtime.input_state.set_region_pending_capture( - crate::input::state::RegionPurposeTag::CaptureDeliver, + RegionPurposeTag::CaptureDeliver, 1, ScreenCaptureSource::Frozen, ) diff --git a/src/backend/wayland/state/activation.rs b/src/backend/wayland/state/activation.rs index f0f53f9e9..fb39ee1de 100644 --- a/src/backend/wayland/state/activation.rs +++ b/src/backend/wayland/state/activation.rs @@ -2,7 +2,9 @@ use log::info; use crate::app_id::runtime_app_id; -use super::*; +use super::WaylandState; +use smithay_client_toolkit::activation::{ActivationHandler, RequestData}; +use wayland_client::QueueHandle; impl WaylandState { pub(in crate::backend::wayland) fn activate_xdg_window_with_startup_token_if_present( diff --git a/src/backend/wayland/state/buffer_damage.rs b/src/backend/wayland/state/buffer_damage.rs index 21fbee1bd..8414094ad 100644 --- a/src/backend/wayland/state/buffer_damage.rs +++ b/src/backend/wayland/state/buffer_damage.rs @@ -13,7 +13,7 @@ //! previous canvas pointers become invalid. use std::collections::HashMap; -use std::fmt; +use std::{fmt, mem}; use crate::util::Rect; @@ -277,7 +277,7 @@ impl BufferDamageTracker { (true, full_reason, Vec::new()) } else { // Take and deduplicate regions - let mut regions = std::mem::take(&mut damage.regions); + let mut regions = mem::take(&mut damage.regions); regions.retain(Rect::is_valid); (false, None, regions) } @@ -605,9 +605,9 @@ mod tests { assert_eq!(tracker.buffers.len(), MAX_TRACKED_BUFFERS); // Adding a new slot should evict the oldest - let _ = tracker.take_buffer_damage(0xF000, 100, 100, TEST_GEN, TEST_SIZE); + let _ = tracker.take_buffer_damage(0xf000, 100, 100, TEST_GEN, TEST_SIZE); assert_eq!(tracker.buffers.len(), MAX_TRACKED_BUFFERS); - assert!(tracker.buffers.contains_key(&0xF000)); + assert!(tracker.buffers.contains_key(&0xf000)); } #[test] diff --git a/src/backend/wayland/state/canvas_layer.rs b/src/backend/wayland/state/canvas_layer.rs index 04182a66b..2d4cb1a5d 100644 --- a/src/backend/wayland/state/canvas_layer.rs +++ b/src/backend/wayland/state/canvas_layer.rs @@ -12,7 +12,13 @@ use log::debug; -use crate::draw::{Color, ShapeId}; +use crate::domain::{BoardGrid, BoardGridKind}; +use crate::draw; +use crate::draw::{ + BlurRectParams, BoardPaper, Color, DrawnShape, EraserReplayContext, RenderCaches, RenderCtx, + Shape, ShapeId, TextMeasurer, +}; +use crate::input::BoardBackground; use crate::util::Rect; use super::WaylandState; @@ -36,7 +42,7 @@ pub(in crate::backend::wayland) struct CanvasLayerCache { shapes_len: usize, last_shape_id: Option, background: Option, - grid: crate::domain::BoardGrid, + grid: BoardGrid, text_halo_enabled: bool, board_key: (usize, usize), valid: bool, @@ -94,17 +100,17 @@ impl CanvasLayerCache { /// Renders one committed shape with the standard eraser/blur replay handling. /// Shared between the direct canvas render path and the layer-cache bake. pub(in crate::backend::wayland) fn render_committed_shape( - measurer: &crate::draw::TextMeasurer, - render: &mut crate::draw::RenderCtx<'_, '_>, - drawn_shape: &crate::draw::DrawnShape, - replay_ctx: &crate::draw::EraserReplayContext<'_>, + measurer: &TextMeasurer, + render: &mut RenderCtx<'_, '_>, + drawn_shape: &DrawnShape, + replay_ctx: &EraserReplayContext<'_>, text_halo_enabled: bool, ) { match &drawn_shape.shape { - crate::draw::Shape::EraserStroke { points, brush } => { - crate::draw::render_eraser_stroke(render.cairo, points, brush, replay_ctx); + Shape::EraserStroke { points, brush } => { + draw::render_eraser_stroke(render.cairo, points, brush, replay_ctx); } - crate::draw::Shape::BlurRect { + Shape::BlurRect { x, y, w, @@ -113,7 +119,7 @@ pub(in crate::backend::wayland) fn render_committed_shape( style, } => { render.render_blur_rect( - crate::draw::BlurRectParams { + BlurRectParams { x: *x, y: *y, w: *w, @@ -161,8 +167,8 @@ impl WaylandState { return false; } let background = match self.input_state.boards.active_background() { - crate::input::BoardBackground::Solid(color) => Some(*color), - crate::input::BoardBackground::Transparent => None, + BoardBackground::Solid(color) => Some(*color), + BoardBackground::Transparent => None, }; let text_halo_enabled = self.config.drawing.text_halo_enabled; let board_key = ( @@ -198,7 +204,7 @@ pub(super) struct CanvasLayerInputs { pub(super) scale: i32, pub(super) origin: (f64, f64), pub(super) background: Option, - pub(super) grid: crate::domain::BoardGrid, + pub(super) grid: BoardGrid, pub(super) text_halo_enabled: bool, pub(super) board_key: (usize, usize), pub(super) generation: u64, @@ -207,9 +213,9 @@ pub(super) struct CanvasLayerInputs { impl CanvasLayerCache { pub(super) fn ensure( &mut self, - measurer: &crate::draw::TextMeasurer, - draw_caches: &mut crate::draw::RenderCaches, - shapes: &[crate::draw::DrawnShape], + measurer: &TextMeasurer, + draw_caches: &mut RenderCaches, + shapes: &[DrawnShape], inputs: CanvasLayerInputs, ) -> bool { let CanvasLayerInputs { @@ -303,9 +309,8 @@ impl CanvasLayerCache { bake_ctx.scale(scale as f64, scale as f64); bake_ctx.translate(-(world_x as f64), -(world_y as f64)); - let paper = match background.filter(|_| grid.kind != crate::domain::BoardGridKind::None) - { - Some(color) => match crate::draw::BoardPaper::for_context(color, grid, &bake_ctx) { + let paper = match background.filter(|_| grid.kind != BoardGridKind::None) { + Some(color) => match BoardPaper::for_context(color, grid, &bake_ctx) { Ok(paper) => { if paper.paint(&bake_ctx).is_err() { cache.clear(); @@ -324,7 +329,7 @@ impl CanvasLayerCache { // Erasers clear down to the baked board paper; blur rects have // no backdrop image in this mode (same as the direct render path). let replay_ctx = crate::draw::EraserReplayContext { - pattern: paper.as_ref().map(crate::draw::BoardPaper::pattern), + pattern: paper.as_ref().map(BoardPaper::pattern), surface: None, backdrop_cache_key: None, bg_color: background, diff --git a/src/backend/wayland/state/capture.rs b/src/backend/wayland/state/capture.rs index 0635b8dae..165d85228 100644 --- a/src/backend/wayland/state/capture.rs +++ b/src/backend/wayland/state/capture.rs @@ -1,8 +1,27 @@ -use super::*; +use crate::canvas_export::SpotlightPassSnapshot; +use crate::capture::{CaptureError, ImageRenderJob, RenderedImageDeliveryRequest}; +use crate::input::BoardBackground; + +use super::super::capture::CapturePreflightRequest; +use super::WaylandState; +use super::core::overlay::OverlaySuppression; +use crate::backend::ExitAfterCaptureMode; use crate::backend::wayland::frozen::FrozenCaptureBackend; -use crate::capture::{CaptureRequest, CaptureRequestId, CaptureSubmitError}; +use crate::canvas_export::{ + BoardExportSnapshot, CanvasExportBackdropSnapshot, CanvasExportSnapshot, CanvasExportViewport, + render_canvas_png, +}; +use crate::capture::file::{FileSaveConfig, expand_tilde}; +use crate::capture::types::CaptureType; +use crate::capture::{ + CaptureDestination, CaptureRequest, CaptureRequestId, CaptureSubmitError, ImageFormatMetadata, + ImageOperationKind, +}; +use crate::config::Action; use crate::i18n::{tr, trf}; use crate::input::state::{Toast, ToastPriority}; +use anyhow::Result; +use wayland_client::QueueHandle; mod backdrop; mod barrier; @@ -79,7 +98,7 @@ impl WaylandState { /// saved without the magnification it was asked for. fn report_export_preflight_failure( &mut self, - error: &crate::capture::CaptureError, + error: &CaptureError, operation: ImageOperationKind, toast_source: &'static str, ) { @@ -344,7 +363,7 @@ impl WaylandState { ); return; } - let render: crate::capture::ImageRenderJob = Box::new(move || render_canvas_png(&snapshot)); + let render: ImageRenderJob = Box::new(move || render_canvas_png(&snapshot)); let save_config = if matches!(destination, CaptureDestination::ClipboardOnly) { None @@ -362,7 +381,7 @@ impl WaylandState { self.capture.set_exit_on_success(exit_on_success); self.capture.mark_in_progress(); - let request = crate::capture::RenderedImageDeliveryRequest { + let request = RenderedImageDeliveryRequest { render, destination, save_config, @@ -387,20 +406,16 @@ impl WaylandState { origin_x: origin_x.round() as i32, origin_y: origin_y.round() as i32, }, - spotlight: crate::canvas_export::SpotlightPassSnapshot { + spotlight: SpotlightPassSnapshot { dim_opacity: self.input_state.style.spotlight_dim_opacity, feather: self.input_state.style.spotlight_feather, }, backdrop: match self.input_state.boards.active_background() { - crate::input::BoardBackground::Transparent => { - CanvasExportBackdropSnapshot::Transparent - } - crate::input::BoardBackground::Solid(color) => { - CanvasExportBackdropSnapshot::board_paper( - *color, - self.input_state.boards.active_board().spec.grid, - ) - } + BoardBackground::Transparent => CanvasExportBackdropSnapshot::Transparent, + BoardBackground::Solid(color) => CanvasExportBackdropSnapshot::board_paper( + *color, + self.input_state.boards.active_board().spec.grid, + ), }, board: BoardExportSnapshot { frame: self diff --git a/src/backend/wayland/state/capture/backdrop.rs b/src/backend/wayland/state/capture/backdrop.rs index a1ab806cf..c827f7541 100644 --- a/src/backend/wayland/state/capture/backdrop.rs +++ b/src/backend/wayland/state/capture/backdrop.rs @@ -1,8 +1,12 @@ -use super::super::*; +use smithay_client_toolkit::output::OutputInfo; + +use super::super::WaylandState; use crate::backend::wayland::frozen_geometry::OutputGeometry; +use crate::capture::{DesktopBackdropGeometry, DesktopBackdropOutputGeometry}; +use wayland_client::protocol::wl_output; pub(super) fn desktop_backdrop_output_geometry_from_info( - info: &smithay_client_toolkit::output::OutputInfo, + info: &OutputInfo, ) -> Option { let (logical_x, logical_y) = info.logical_position?; let (logical_width, logical_height) = info.logical_size?; @@ -25,7 +29,7 @@ pub(super) fn desktop_backdrop_output_geometry_from_info( }) } -fn current_mode_size(info: &smithay_client_toolkit::output::OutputInfo) -> Option<(u32, u32)> { +fn current_mode_size(info: &OutputInfo) -> Option<(u32, u32)> { info.modes .iter() .find(|mode| mode.current) diff --git a/src/backend/wayland/state/capture/barrier.rs b/src/backend/wayland/state/capture/barrier.rs index 46faf5289..6f5beca1c 100644 --- a/src/backend/wayland/state/capture/barrier.rs +++ b/src/backend/wayland/state/capture/barrier.rs @@ -1,15 +1,19 @@ -use super::super::*; +use crate::backend::wayland::acquisition::ScreenAcquisitionOutcome; +use crate::backend::wayland::zoom::ZoomState; +use crate::input::InputState; + +use super::super::WaylandState; +use super::super::buffer_damage::FullDamageReason; +use super::super::core::overlay::OverlaySuppression; use crate::backend::wayland::capture_preflight::CapturePreflightError; use crate::i18n::tr; use crate::input::state::{Toast, ToastPriority}; use std::time::{Duration, Instant}; +use wayland_client::QueueHandle; const MAIN_SURFACE_FRAME_TIMEOUT: Duration = Duration::from_secs(1); -fn push_capture_preflight_cancellation_toast( - input_state: &mut crate::input::InputState, - message: &str, -) { +fn push_capture_preflight_cancellation_toast(input_state: &mut InputState, message: &str) { input_state.push_toast(ToastPriority::Critical, "capture", Toast::warning(message)); } @@ -17,14 +21,14 @@ fn push_capture_preflight_cancellation_toast( /// event-loop router owns that report; an uncorrelated standalone failure has /// no terminal, so its caller must retain the immediate capture toast. fn finish_zoom_preflight_cancellation( - zoom: &mut crate::backend::wayland::zoom::ZoomState, - input_state: &mut crate::input::InputState, + zoom: &mut ZoomState, + input_state: &mut InputState, message: &str, ) -> bool { let terminal_will_report = zoom.current_capture_id().is_some(); zoom.finish_preflight_failure( input_state, - CapturePreflightError::Backend(message.to_string()), + &CapturePreflightError::Backend(message.to_string()), ); terminal_will_report } @@ -392,7 +396,7 @@ impl WaylandState { log::warn!("Frozen preflight capture failed: {err}"); if self.frozen.has_acquisition_attempt() { self.frozen.finish_preflight_failure( - CapturePreflightError::from_backend(err), + &CapturePreflightError::from_backend(err), &mut self.input_state, ); } else { @@ -425,7 +429,7 @@ impl WaylandState { log::warn!("Zoom preflight capture failed: {err}"); self.zoom.finish_preflight_failure( &mut self.input_state, - CapturePreflightError::from_backend(err), + &CapturePreflightError::from_backend(err), ); } } @@ -466,7 +470,7 @@ impl WaylandState { let acquisition_terminal = self.frozen.has_acquisition_attempt(); if acquisition_terminal { self.frozen.finish_acquisition( - crate::backend::wayland::acquisition::ScreenAcquisitionOutcome::Failed( + ScreenAcquisitionOutcome::Failed( frozen_message .unwrap_or("Freeze capture could not start.") .to_string(), @@ -498,6 +502,8 @@ impl WaylandState { #[cfg(test)] mod tests { + use crate::backend::wayland::zoom::ZoomCaptureBackend; + use super::*; use crate::backend::wayland::state::acquisition::report_zoom_terminal_to; use crate::backend::wayland::zoom::ZoomWaiterOwner; @@ -735,9 +741,9 @@ mod tests { #[tokio::test] async fn zoom_preflight_failure_has_exactly_one_report_with_or_without_a_waiter() { for owner in [Some(ZoomWaiterOwner::Ocr), None] { - let mut zoom = crate::backend::wayland::zoom::ZoomState::new(None); + let mut zoom = ZoomState::new(None); let mut input_state = make_test_input_state(); - zoom.start_capture(crate::backend::wayland::zoom::ZoomCaptureBackend::WlrScreencopy) + zoom.start_capture(ZoomCaptureBackend::WlrScreencopy) .expect("identified zoom capture"); assert!(finish_zoom_preflight_cancellation( @@ -761,7 +767,7 @@ mod tests { #[test] fn standalone_zoom_preflight_failure_keeps_the_caller_report() { - let mut zoom = crate::backend::wayland::zoom::ZoomState::new(None); + let mut zoom = ZoomState::new(None); let mut input_state = make_test_input_state(); assert!(!finish_zoom_preflight_cancellation( diff --git a/src/backend/wayland/state/capture/pdf.rs b/src/backend/wayland/state/capture/pdf.rs index 115d4dd72..f633a8f19 100644 --- a/src/backend/wayland/state/capture/pdf.rs +++ b/src/backend/wayland/state/capture/pdf.rs @@ -1,5 +1,20 @@ -use super::super::*; -use crate::backend::wayland::capture::CaptureLayoutContext; +use crate::capture::{DocumentRenderJob, RenderedDocumentDeliveryRequest}; +use crate::config::PdfTransparentBackground; + +use super::super::WaylandState; +use super::super::core::overlay::OverlaySuppression; +use crate::backend::wayland::capture::{ + CaptureLayoutContext, CapturePreflightRequest, PendingPdfExport, +}; +use crate::canvas_export::{ + BoardPdfExportSnapshot, CanvasExportBackdropSnapshot, render_board_pdf, +}; +use crate::capture::file::{FileSaveConfig, expand_tilde}; +use crate::capture::{ + CaptureDestination, DesktopBackdropCaptureRequest, DesktopBackdropCaptureResult, + ImageOperationKind, RenderedDocument, +}; +use crate::config::Action; use crate::i18n::tr; use crate::input::state::{Toast, ToastPriority}; @@ -180,7 +195,7 @@ impl WaylandState { // Render on the capture worker, not here: an all-boards export // renders every page of every board plus PDF encoding, which stalled // event dispatch for seconds on multi-board sessions. - let render: crate::capture::DocumentRenderJob = Box::new(move || { + let render: DocumentRenderJob = Box::new(move || { render_board_pdf(&snapshot).map(|bytes| RenderedDocument { bytes, extension: "pdf".to_string(), @@ -191,7 +206,7 @@ impl WaylandState { self.capture.set_exit_on_success(exit_on_success); self.capture.mark_in_progress(); - let request = crate::capture::RenderedDocumentDeliveryRequest { + let request = RenderedDocumentDeliveryRequest { render, destination: CaptureDestination::FileOnly, save_config: Some(save_config), @@ -224,8 +239,7 @@ impl WaylandState { } fn should_capture_desktop_for_pdf_export(&self, action: Action) -> bool { - self.config.export.pdf.transparent_background - == crate::config::PdfTransparentBackground::Desktop + self.config.export.pdf.transparent_background == PdfTransparentBackground::Desktop && self.board_pdf_export_scope_has_transparent_pages(action) } diff --git a/src/backend/wayland/state/capture/retry.rs b/src/backend/wayland/state/capture/retry.rs index 08997308b..0c8b3d5cb 100644 --- a/src/backend/wayland/state/capture/retry.rs +++ b/src/backend/wayland/state/capture/retry.rs @@ -1,7 +1,12 @@ //! Re-admission on the retained backend after a bounded layout settling period. -use super::super::core::overlay::OverlaySuppressionState; -use super::super::*; +use super::super::WaylandState; +use super::super::buffer_damage::FullDamageReason; +use super::super::core::overlay::{OverlaySuppression, OverlaySuppressionState}; use crate::backend::wayland::capture_preflight::CapturePreflightError; +use crate::backend::wayland::frozen::FrozenState; +use crate::backend::wayland::zoom::ZoomState; +use crate::input::InputState; +use anyhow::Result; use std::time::Instant; impl WaylandState { @@ -48,7 +53,7 @@ fn advance_layout_retries( Ok(ready) => restarted |= ready, Err(error) => { log::warn!("Freeze retry preflight failed: {error}"); - frozen.finish_preflight_failure(error, input); + frozen.finish_preflight_failure(&error, input); } } } @@ -60,7 +65,7 @@ fn advance_layout_retries( Ok(ready) => restarted |= ready, Err(error) => { log::warn!("Zoom retry preflight failed: {error}"); - zoom.finish_preflight_failure(input, error); + zoom.finish_preflight_failure(input, &error); } } } @@ -88,14 +93,20 @@ fn restart_suppressed_retry( #[cfg(test)] mod tests { + use crate::backend::wayland::RuntimeWakeSource; + use crate::config::Config; + use crate::input::state::test_support; + use super::*; use crate::backend::wayland::acquisition::{ ScreenAcquisitionId, ScreenAcquisitionOutcome, ScreenAcquisitionOwner, }; use crate::backend::wayland::frozen_geometry::OutputGeometry; + use crate::backend::wayland::state::OverlaySuppressionKeyboardPolicy; use crate::backend::wayland::state::acquisition::AcquisitionRuntime; use crate::backend::wayland::zoom::{ZoomCaptureBackend, ZoomCaptureId, ZoomSourceOutcome}; use std::time::Duration; + use wayland_client::protocol::wl_output; #[derive(Clone, Copy)] enum RetainedRequest { @@ -181,7 +192,7 @@ mod tests { for reason in [OverlaySuppression::Frozen, OverlaySuppression::Zoom] { let mut fixture = HandlerFixture::with_capture_output( - crate::config::Config::default(), + Config::default(), CaptureFixtureBackend::Portal, ); let output = fixture.complete_output_metadata("DP-3"); @@ -289,10 +300,10 @@ mod tests { // Successful admission, lost suppression, switched output, and // metadata that never becomes complete exercise the real owners. for failure in [None, Some("suppression"), Some("output"), Some("metadata")] { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut frozen = FrozenState::new_with_runtime_wake(None, wake.handle()); let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); let geometry = OutputGeometry::update_from( Some((0, 0)), Some((2, 1)), diff --git a/src/backend/wayland/state/clipboard.rs b/src/backend/wayland/state/clipboard.rs index bc82d96a8..e21849421 100644 --- a/src/backend/wayland/state/clipboard.rs +++ b/src/backend/wayland/state/clipboard.rs @@ -1,18 +1,15 @@ //! Wayland-state glue for clipboard publish and paste requests. use super::WaylandState; -use crate::backend::wayland::{ - RuntimeOperationPoll, - clipboard::{ - self, ClipboardPasteCompletion, ClipboardPasteResult, ClipboardPublishCompletion, - FailedLocalSelectionProbe, PasteAction, TransferEffect, TransferPlan, TransferWarning, - transfer, - }, +use crate::backend::wayland::RuntimeOperationPoll; +use crate::backend::wayland::clipboard::{ + self, ClipboardPasteCompletion, ClipboardPasteResult, ClipboardPublishCompletion, + FailedLocalSelectionProbe, PasteAction, TransferEffect, TransferPlan, TransferWarning, + transfer, }; use crate::i18n::{tr, trf}; use crate::input::InputState; -use crate::input::state::{ClipboardFingerprint, ClipboardPasteRequest}; -use crate::input::state::{Toast, ToastPriority}; +use crate::input::state::{ClipboardFingerprint, ClipboardPasteRequest, Toast, ToastPriority}; use std::time::{Duration, Instant}; mod fallback_save; @@ -197,8 +194,8 @@ impl WaylandState { &mut self.input_state, completion.request, generation, - expected, - current, + expected.as_ref(), + current.as_ref(), ); self.apply_paste_action(action); return; @@ -363,7 +360,7 @@ impl WaylandState { &mut self, request: ClipboardPasteRequest, generation: u64, - expected: Option, + expected: Option, ) { log::info!( "Probing system clipboard fingerprint for paste request {} generation {}", @@ -612,8 +609,8 @@ fn continue_after_fingerprint_probe( input_state: &mut InputState, request: ClipboardPasteRequest, generation: u64, - expected: Option, - current: Option, + expected: Option<&ClipboardFingerprint>, + current: Option<&ClipboardFingerprint>, ) -> PasteAction { let local_shapes = input_state .selection_clipboard_snapshot() @@ -641,7 +638,7 @@ fn apply_transfer_effects(input_state: &mut InputState, effects: Vec Option { (!clipboard_paste_active) @@ -652,7 +649,8 @@ fn take_pending_clipboard_paste_if_idle( #[cfg(test)] mod transport_tests { use super::*; - use crate::input::state::{PasteAnchor, test_support::make_test_input_state}; + use crate::input::state::PasteAnchor; + use crate::input::state::test_support::make_test_input_state; use crate::util::Rect; fn request(id: u64) -> ClipboardPasteRequest { diff --git a/src/backend/wayland/state/clipboard/fallback_save.rs b/src/backend/wayland/state/clipboard/fallback_save.rs index 4c7dec01a..b963cce22 100644 --- a/src/backend/wayland/state/clipboard/fallback_save.rs +++ b/src/backend/wayland/state/clipboard/fallback_save.rs @@ -1,3 +1,9 @@ +use std::path::PathBuf; + +use crate::backend::wayland::runtime_operation::RuntimeOperationSubmitError; +use crate::capture::file; +use crate::notification; + use super::WaylandState; use crate::backend::wayland::RuntimeOperationPoll; use crate::backend::wayland::state::clipboard_runtime::ClipboardRuntime; @@ -27,7 +33,7 @@ impl WaylandState { pub(in crate::backend::wayland) fn finish_clipboard_fallback_saves(&mut self) { if let Err(error) = finish_fallback_saves(&mut self.input_state, &mut self.clipboard) { log::error!("Failed to finish accepted image save before exit: {error}"); - crate::notification::send_notification_async( + notification::send_notification_async( &self.tokio_handle, "Failed to Save Image".into(), error, @@ -44,10 +50,7 @@ fn submit_save( ) -> Result<(), String> { if let Err(failure) = clipboard.submit_fallback_save(request, save_image) { let (error, id) = failure.into_parts(); - let message = if matches!( - error, - crate::backend::wayland::runtime_operation::RuntimeOperationSubmitError::Busy { .. } - ) { + let message = if matches!(error, RuntimeOperationSubmitError::Busy { .. }) { "Another image is being saved. Try again in a moment." } else { "Could not start the image save. Try again." @@ -81,16 +84,14 @@ fn finish_fallback_saves( result } -fn save_image( - request: std::sync::Arc, -) -> Result { - crate::capture::file::save_screenshot(&request.image_data, &request.save_config) +fn save_image(request: &ClipboardFallbackSaveRequest) -> Result { + file::save_screenshot(&request.image_data, &request.save_config) .map_err(|error| request.operation.format_error(&error)) } fn apply_completion( input: &mut InputState, - completion: RuntimeOperationPoll>, + completion: RuntimeOperationPoll>, ) -> Result<(), String> { let (id, outcome) = match completion { RuntimeOperationPoll::Idle | RuntimeOperationPoll::Pending { .. } => return Ok(()), @@ -117,19 +118,28 @@ fn apply_completion( #[cfg(test)] mod tests { + use std::path::Path; + use std::sync::mpsc; + use std::sync::mpsc::RecvTimeoutError; + use std::{fs, thread}; + + use crate::input::state::{HelperLaunchRequest, test_support}; + use crate::test_temp; + use super::*; use crate::backend::wayland::state::clipboard_runtime::ClipboardRuntime; use crate::backend::wayland::{RuntimeOperationIdSource, RuntimeWakeSource}; - use crate::capture::{ImageOperationKind, file::FileSaveConfig}; + use crate::capture::ImageOperationKind; + use crate::capture::file::FileSaveConfig; use crate::input::state::{InputEffect, InputEffectDrain, PendingBackendAction}; use std::time::Duration; fn queue_image( input: &mut InputState, - path: &std::path::Path, + path: &Path, bytes: Vec, exit: bool, - ) -> std::sync::Arc { + ) -> Arc { input.set_clipboard_fallback( bytes, FileSaveConfig { @@ -160,9 +170,9 @@ mod tests { #[test] fn teardown_starts_a_save_accepted_in_the_exit_dispatch_batch() { - let temp = crate::test_temp::tempdir().unwrap(); - let mut input = crate::input::state::test_support::make_test_input_state(); - let wake = RuntimeWakeSource::new().unwrap(); + let temp = test_temp::tempdir().unwrap(); + let mut input = test_support::make_test_input_state(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut runtime = ClipboardRuntime::new(RuntimeOperationIdSource::new(), wake.handle()); input.set_clipboard_fallback( vec![4, 5, 6], @@ -176,14 +186,13 @@ mod tests { ); input.save_pending_clipboard_to_file(); assert!(input.has_pending_backend_actions()); - let unrelated = - PendingBackendAction::HelperLaunch(crate::input::state::HelperLaunchRequest::About); + let unrelated = PendingBackendAction::HelperLaunch(HelperLaunchRequest::About); input.set_pending_backend_action(unrelated.clone()); input.request_explicit_exit(); finish_fallback_saves(&mut input, &mut runtime).unwrap(); - let paths: Vec<_> = std::fs::read_dir(temp.path()) + let paths: Vec<_> = fs::read_dir(temp.path()) .unwrap() .map(|entry| entry.unwrap().path()) .collect(); @@ -192,7 +201,7 @@ mod tests { 1, "accepted save must run even when Exit skips the runtime drain" ); - assert_eq!(std::fs::read(&paths[0]).unwrap(), [4, 5, 6]); + assert_eq!(fs::read(&paths[0]).unwrap(), [4, 5, 6]); assert_eq!(input.last_capture_path(), Some(paths[0].as_path())); assert!(matches!( runtime.poll_fallback_save(), @@ -202,14 +211,14 @@ mod tests { assert_eq!(input.take_pending_backend_action(), Some(unrelated)); assert!(!input.has_pending_backend_actions()); finish_fallback_saves(&mut input, &mut runtime).unwrap(); - assert_eq!(std::fs::read_dir(temp.path()).unwrap().count(), 1); + assert_eq!(fs::read_dir(temp.path()).unwrap().count(), 1); } #[test] fn teardown_preserves_queued_image_ownership_when_the_fallback_changes() { - let temp = crate::test_temp::tempdir().unwrap(); - let mut input = crate::input::state::test_support::make_test_input_state(); - let wake = RuntimeWakeSource::new().unwrap(); + let temp = test_temp::tempdir().unwrap(); + let mut input = test_support::make_test_input_state(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut runtime = ClipboardRuntime::new(RuntimeOperationIdSource::new(), wake.handle()); for (name, bytes) in [("first", vec![1, 2]), ("second", vec![3, 4])] { input.set_clipboard_fallback( @@ -228,12 +237,12 @@ mod tests { finish_fallback_saves(&mut input, &mut runtime).unwrap(); for (name, bytes) in [("first", vec![1, 2]), ("second", vec![3, 4])] { - let paths: Vec<_> = std::fs::read_dir(temp.path().join(name)) + let paths: Vec<_> = fs::read_dir(temp.path().join(name)) .unwrap() .map(|entry| entry.unwrap().path()) .collect(); assert_eq!(paths.len(), 1); - assert_eq!(std::fs::read(&paths[0]).unwrap(), bytes); + assert_eq!(fs::read(&paths[0]).unwrap(), bytes); } assert_eq!( input.last_capture_path().unwrap().parent(), @@ -255,17 +264,17 @@ mod tests { (false, "Failed to save canvas export"), (true, "held writer failed"), ] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = if panics { temp.path().to_path_buf() } else { temp.path().join("not-a-directory") }; if !panics { - std::fs::write(&path, b"obstruction").unwrap(); + fs::write(&path, b"obstruction").unwrap(); } - let mut input = crate::input::state::test_support::make_test_input_state(); - let wake = RuntimeWakeSource::new().unwrap(); + let mut input = test_support::make_test_input_state(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut runtime = ClipboardRuntime::new(RuntimeOperationIdSource::new(), wake.handle()); let request = queue_image(&mut input, &path, vec![7, 8], true); runtime @@ -279,7 +288,7 @@ mod tests { assert!(error.contains(expected)); if !panics { - assert_eq!(std::fs::read(&path).unwrap(), b"obstruction"); + assert_eq!(fs::read(&path).unwrap(), b"obstruction"); } assert!(!input.should_exit); assert!(input.last_capture_path().is_none()); @@ -291,11 +300,11 @@ mod tests { #[test] fn teardown_failure_does_not_discard_a_newer_accepted_save() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let blocked = temp.path().join("not-a-directory"); - std::fs::write(&blocked, b"obstruction").unwrap(); - let mut input = crate::input::state::test_support::make_test_input_state(); - let wake = RuntimeWakeSource::new().unwrap(); + fs::write(&blocked, b"obstruction").unwrap(); + let mut input = test_support::make_test_input_state(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut runtime = ClipboardRuntime::new(RuntimeOperationIdSource::new(), wake.handle()); let first = queue_image(&mut input, &blocked, vec![1], true); runtime.submit_fallback_save(first, save_image).unwrap(); @@ -314,10 +323,10 @@ mod tests { let error = finish_fallback_saves(&mut input, &mut runtime).unwrap_err(); assert!(error.contains("Failed to save canvas export")); - assert_eq!(std::fs::read(&blocked).unwrap(), b"obstruction"); + assert_eq!(fs::read(&blocked).unwrap(), b"obstruction"); let saved = input.last_capture_path().unwrap(); assert_eq!(saved.parent(), Some(temp.path().join("newer").as_path())); - assert_eq!(std::fs::read(saved).unwrap(), [2]); + assert_eq!(fs::read(saved).unwrap(), [2]); assert!(!input.should_exit); assert!(matches!( runtime.poll_fallback_save(), @@ -327,14 +336,14 @@ mod tests { #[test] fn teardown_waits_for_an_already_started_image_writer() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let directory = temp.path().to_path_buf(); - let (release, wait) = std::sync::mpsc::channel(); - let (started_tx, started_rx) = std::sync::mpsc::channel(); - let (finished_tx, finished_rx) = std::sync::mpsc::channel(); - let teardown = std::thread::spawn(move || { - let mut input = crate::input::state::test_support::make_test_input_state(); - let wake = RuntimeWakeSource::new().unwrap(); + let (release, wait) = mpsc::channel(); + let (started_tx, started_rx) = mpsc::channel(); + let (finished_tx, finished_rx) = mpsc::channel(); + let teardown = thread::spawn(move || { + let mut input = test_support::make_test_input_state(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut runtime = ClipboardRuntime::new(RuntimeOperationIdSource::new(), wake.handle()); let request = queue_image(&mut input, &directory, vec![8, 9, 10], true); runtime @@ -359,40 +368,38 @@ mod tests { teardown.join().unwrap(); assert!( - matches!(premature, Err(std::sync::mpsc::RecvTimeoutError::Timeout)), + matches!(premature, Err(RecvTimeoutError::Timeout)), "teardown returned while its accepted writer was held" ); result.unwrap(); - let paths: Vec<_> = std::fs::read_dir(temp.path()) + let paths: Vec<_> = fs::read_dir(temp.path()) .unwrap() .map(|entry| entry.unwrap().path()) .collect(); assert_eq!(paths.len(), 1); - assert_eq!(std::fs::read(&paths[0]).unwrap(), [8, 9, 10]); + assert_eq!(fs::read(&paths[0]).unwrap(), [8, 9, 10]); } #[test] fn fallback_worker_saves_real_bytes_and_only_then_requests_exit() { - let temp = crate::test_temp::tempdir().unwrap(); - let mut input = crate::input::state::test_support::make_test_input_state(); - let wake = RuntimeWakeSource::new().unwrap(); + let temp = test_temp::tempdir().unwrap(); + let mut input = test_support::make_test_input_state(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut runtime = ClipboardRuntime::new(RuntimeOperationIdSource::new(), wake.handle()); let request = queue_image(&mut input, temp.path(), vec![1, 2, 3], true); - runtime - .submit_fallback_save(request.clone(), save_image) - .unwrap(); + runtime.submit_fallback_save(request, save_image).unwrap(); assert!(!input.should_exit); assert!(wake.wait_readable(Some(Duration::from_secs(2))).unwrap()); apply_completion(&mut input, runtime.poll_fallback_save()).unwrap(); - assert_eq!(std::fs::read_dir(temp.path()).unwrap().count(), 1); - let path = std::fs::read_dir(temp.path()) + assert_eq!(fs::read_dir(temp.path()).unwrap().count(), 1); + let path = fs::read_dir(temp.path()) .unwrap() .next() .unwrap() .unwrap() .path(); - assert_eq!(std::fs::read(path).unwrap(), [1, 2, 3]); + assert_eq!(fs::read(path).unwrap(), [1, 2, 3]); assert!(input.should_exit); assert!(input.take_explicit_exit_requested()); assert!(!input.take_explicit_exit_requested()); @@ -407,16 +414,14 @@ mod tests { #[test] fn failed_save_retains_bytes_for_retry_and_canvas_error_wording() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let blocked = temp.path().join("not-a-directory"); - std::fs::write(&blocked, b"file").unwrap(); - let mut input = crate::input::state::test_support::make_test_input_state(); - let wake = RuntimeWakeSource::new().unwrap(); + fs::write(&blocked, b"file").unwrap(); + let mut input = test_support::make_test_input_state(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut runtime = ClipboardRuntime::new(RuntimeOperationIdSource::new(), wake.handle()); let request = queue_image(&mut input, &blocked, vec![7, 8, 9], false); - runtime - .submit_fallback_save(request.clone(), save_image) - .unwrap(); + runtime.submit_fallback_save(request, save_image).unwrap(); assert!(wake.wait_readable(Some(Duration::from_secs(2))).unwrap()); assert!(apply_completion(&mut input, runtime.poll_fallback_save()).is_err()); assert!( @@ -434,20 +439,20 @@ mod tests { .to_lowercase() .contains("screenshot") ); - std::fs::remove_file(&blocked).unwrap(); + fs::remove_file(&blocked).unwrap(); input.save_pending_clipboard_to_file(); let retry = take_save_request(&mut input); assert_eq!(&*retry.image_data, &[7, 8, 9]); runtime.submit_fallback_save(retry, save_image).unwrap(); assert!(wake.wait_readable(Some(Duration::from_secs(2))).unwrap()); apply_completion(&mut input, runtime.poll_fallback_save()).unwrap(); - let saved = std::fs::read_dir(&blocked) + let saved = fs::read_dir(&blocked) .unwrap() .next() .unwrap() .unwrap() .path(); - assert_eq!(std::fs::read(saved).unwrap(), [7, 8, 9]); + assert_eq!(fs::read(saved).unwrap(), [7, 8, 9]); input.save_pending_clipboard_to_file(); assert!( !input @@ -459,12 +464,12 @@ mod tests { #[test] fn slow_save_is_single_flight_and_stale_completion_preserves_newer_image() { - let temp = crate::test_temp::tempdir().unwrap(); - let mut input = crate::input::state::test_support::make_test_input_state(); - let wake = RuntimeWakeSource::new().unwrap(); + let temp = test_temp::tempdir().unwrap(); + let mut input = test_support::make_test_input_state(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut runtime = ClipboardRuntime::new(RuntimeOperationIdSource::new(), wake.handle()); let first = queue_image(&mut input, temp.path(), vec![1], true); - let (release, wait) = std::sync::mpsc::channel(); + let (release, wait) = mpsc::channel(); runtime .submit_fallback_save(first, move |request| { wait.recv().unwrap(); diff --git a/src/backend/wayland/state/clipboard/probe_tests.rs b/src/backend/wayland/state/clipboard/probe_tests.rs index 35456b2ef..8b33b242d 100644 --- a/src/backend/wayland/state/clipboard/probe_tests.rs +++ b/src/backend/wayland/state/clipboard/probe_tests.rs @@ -1,5 +1,8 @@ //! The paste decision after a failed private publish, through the same start //! and probe-continuation steps a paste completion uses. +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::*; use crate::config::Action; use crate::draw::Shape; @@ -22,8 +25,8 @@ fn copy_with_failed_publish( x: i32, at_failure: Option, ) -> u64 { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); let id = input_state .boards .active_frame_mut() @@ -78,8 +81,8 @@ fn a_failed_publish_is_pasted_locally_only_while_the_system_clipboard_is_unchang &mut input_state, request, generation, - expected, - Some(fingerprint(1)), + expected.as_ref(), + Some(&fingerprint(1)), ); assert!(matches!(action, PasteAction::UseLocalShapes { .. })); @@ -88,8 +91,8 @@ fn a_failed_publish_is_pasted_locally_only_while_the_system_clipboard_is_unchang &mut input_state, request, generation, - expected, - Some(fingerprint(2)), + expected.as_ref(), + Some(&fingerprint(2)), ); assert!(matches!(action, PasteAction::ReadSystemClipboard { .. })); let local = input_state.selection_clipboard_snapshot(); @@ -108,8 +111,8 @@ fn a_failed_publish_without_a_fingerprint_is_superseded_by_a_readable_clipboard( &mut input_state, request, generation, - expected, - Some(fingerprint(1)), + expected.as_ref(), + Some(&fingerprint(1)), ); assert!(matches!(action, PasteAction::ReadSystemClipboard { .. })); @@ -126,8 +129,13 @@ fn an_unreadable_clipboard_reads_the_system_clipboard_and_keeps_the_fallback() { copy_with_failed_publish(&mut input_state, 10, None); let (request, generation, expected) = start_probe(&mut input_state); - let action = - continue_after_fingerprint_probe(&mut input_state, request, generation, expected, None); + let action = continue_after_fingerprint_probe( + &mut input_state, + request, + generation, + expected.as_ref(), + None, + ); assert!(matches!(action, PasteAction::ReadSystemClipboard { .. })); assert!( @@ -157,8 +165,8 @@ fn a_probe_for_an_older_copy_neither_pastes_nor_supersedes_a_newer_one() { &mut input_state, request, generation, - expected, - Some(fingerprint(1)), + expected.as_ref(), + Some(&fingerprint(1)), ); assert!(matches!(action, PasteAction::ReadSystemClipboard { .. })); assert_eq!( diff --git a/src/backend/wayland/state/clipboard/session_paste.rs b/src/backend/wayland/state/clipboard/session_paste.rs index 9dbad6681..113f09dd8 100644 --- a/src/backend/wayland/state/clipboard/session_paste.rs +++ b/src/backend/wayland/state/clipboard/session_paste.rs @@ -1,3 +1,5 @@ +use std::env; + use super::super::WaylandState; use crate::config::{Action, Config}; use crate::draw::frame::UndoAction; @@ -6,10 +8,9 @@ use crate::env_vars::HOME_ENV; use crate::i18n::{tr, trf}; use crate::input::InputState; use crate::input::boards::BoardState; -use crate::input::state::ClipboardPasteRequest; -use crate::input::state::{Toast, ToastPriority}; +use crate::input::state::{ClipboardPasteRequest, Toast, ToastPriority}; use crate::{notification, session}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::time::Instant; const SESSION_PASTE_WARNING_TOAST_MS: u64 = 20_000; @@ -429,13 +430,14 @@ fn describe_limit(limit: session::SaveLimitExceeded) -> String { } fn config_path_display() -> String { - Config::get_config_path() + Config::config_path() + .as_deref() .map(compact_path) .unwrap_or_else(|_| "~/.config/wayscriber/config.toml".to_string()) } -fn compact_path(path: PathBuf) -> String { - let Some(home) = std::env::var_os(HOME_ENV).map(PathBuf::from) else { +fn compact_path(path: &Path) -> String { + let Some(home) = env::var_os(HOME_ENV).map(PathBuf::from) else { return path.display().to_string(); }; match path.strip_prefix(&home) { diff --git a/src/backend/wayland/state/clipboard/session_paste/tests.rs b/src/backend/wayland/state/clipboard/session_paste/tests.rs index 104e937cc..477eef627 100644 --- a/src/backend/wayland/state/clipboard/session_paste/tests.rs +++ b/src/backend/wayland/state/clipboard/session_paste/tests.rs @@ -1,7 +1,8 @@ use super::*; use crate::draw::Color; use crate::input::BOARD_ID_TRANSPARENT; -use crate::input::state::{PasteAnchor, test_support::make_test_input_state}; +use crate::input::state::PasteAnchor; +use crate::input::state::test_support::make_test_input_state; use crate::session::{CompressionMode, SnapshotPayloadEstimate, SnapshotSaveEstimate}; use crate::util::Rect; use std::path::PathBuf; diff --git a/src/backend/wayland/state/clipboard_runtime.rs b/src/backend/wayland/state/clipboard_runtime.rs index ed334c8b9..f9d88f9b1 100644 --- a/src/backend/wayland/state/clipboard_runtime.rs +++ b/src/backend/wayland/state/clipboard_runtime.rs @@ -1,15 +1,21 @@ use std::collections::VecDeque; +use std::path::PathBuf; +use std::sync::Arc; +use crate::backend::wayland::clipboard::{ + ClipboardPasteCompletion, ClipboardPublishCompletion, transfer, +}; use crate::backend::wayland::{ RuntimeOperationController, RuntimeOperationIdSource, RuntimeOperationPoll, RuntimeOperationSubmitFailure, RuntimeWakeHandle, - clipboard::{ClipboardPasteCompletion, ClipboardPublishCompletion, transfer}, }; use crate::clipboard_text::{ ClipboardTextError, copy_text_via_command, read_clipboard_text_via_command, }; +use crate::i18n; use crate::input::state::{ - ClipboardPasteRequest, TextClipboardRequest, TextPasteEdit, TextPasteTarget, + ClipboardFallbackSaveRequest, ClipboardPasteRequest, TextClipboardRequest, TextPasteEdit, + TextPasteTarget, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -21,8 +27,8 @@ pub(in crate::backend::wayland) enum HexCopyOutcome { impl HexCopyOutcome { pub(in crate::backend::wayland) fn message(&self) -> &'static str { match self { - Self::Copied => crate::i18n::tr("Copied to clipboard"), - Self::Failed => crate::i18n::tr("Failed to copy to clipboard"), + Self::Copied => i18n::tr("Copied to clipboard"), + Self::Failed => i18n::tr("Failed to copy to clipboard"), } } } @@ -36,8 +42,8 @@ pub(in crate::backend::wayland) enum TextCopyOutcome { impl TextCopyOutcome { pub(in crate::backend::wayland) fn message(&self) -> &'static str { match self { - Self::Copied => crate::i18n::tr("Copied to clipboard"), - Self::Failed => crate::i18n::tr("Failed to copy to clipboard"), + Self::Copied => i18n::tr("Copied to clipboard"), + Self::Failed => i18n::tr("Failed to copy to clipboard"), } } } @@ -63,9 +69,9 @@ impl ClipboardPasteSubmitFailure { impl TextPasteOutcome { pub(in crate::backend::wayland) fn message(&self) -> &'static str { match self { - Self::Text(_) => crate::i18n::tr("Pasted from clipboard"), - Self::Empty => crate::i18n::tr("Clipboard empty"), - Self::Failed => crate::i18n::tr("Failed to paste from clipboard"), + Self::Text(_) => i18n::tr("Pasted from clipboard"), + Self::Empty => i18n::tr("Clipboard empty"), + Self::Failed => i18n::tr("Failed to paste from clipboard"), } } } @@ -76,7 +82,7 @@ pub(in crate::backend::wayland) struct ClipboardRuntime { paste: RuntimeOperationController, hex_copy: RuntimeOperationController, pending_hex_copy: Option, - fallback_save: RuntimeOperationController>, + fallback_save: RuntimeOperationController>, text_copy: RuntimeOperationController, pending_text_copy: VecDeque, text_paste: RuntimeOperationController, @@ -100,30 +106,28 @@ impl ClipboardRuntime { pub(in crate::backend::wayland) fn submit_fallback_save( &mut self, - request: std::sync::Arc, - operation: impl FnOnce( - std::sync::Arc, - ) -> Result + request: Arc, + operation: impl FnOnce(&ClipboardFallbackSaveRequest) -> Result + Send + 'static, ) -> Result<(), RuntimeOperationSubmitFailure> { self.fallback_save .try_submit(request.id, "clipboard-fallback-save", move || { - operation(request) + operation(&request) }) .map(drop) } pub(in crate::backend::wayland) fn poll_fallback_save( &mut self, - ) -> RuntimeOperationPoll> { + ) -> RuntimeOperationPoll> { self.fallback_save.poll() } /// Finish an accepted file write before overlay teardown; never use in dispatch. pub(in crate::backend::wayland) fn wait_fallback_save( &mut self, - ) -> RuntimeOperationPoll> { + ) -> RuntimeOperationPoll> { self.fallback_save.wait() } @@ -138,7 +142,7 @@ impl ClipboardRuntime { ) -> Result<(), RuntimeOperationSubmitFailure> { self.publish .try_submit(generation, "clipboard-publish", move || { - transfer::resolve_selection_clipboard_publish(generation, payload_json) + transfer::resolve_selection_clipboard_publish(generation, &payload_json) }) .map(drop) } @@ -369,11 +373,13 @@ fn rebase_text_paste_offset(offset: usize, edit: &TextPasteEdit) -> usize { #[cfg(test)] mod tests { + use crate::input::state::TextCutTarget; + use super::*; use crate::backend::wayland::{RuntimeOperationIdSource, RuntimeWakeSource}; fn runtime() -> ClipboardRuntime { - let wake = RuntimeWakeSource::new().expect("runtime wake source"); + let wake = RuntimeWakeSource::try_new().expect("runtime wake source"); ClipboardRuntime::new(RuntimeOperationIdSource::new(), wake.handle()) } @@ -388,7 +394,7 @@ mod tests { fn cut_request(text: &str, start: usize) -> TextClipboardRequest { TextClipboardRequest { text: text.to_string(), - cut: Some(crate::input::state::TextCutTarget { + cut: Some(TextCutTarget { generation: 7, revision: 3, range: start..start + text.len(), diff --git a/src/backend/wayland/state/color_picker.rs b/src/backend/wayland/state/color_picker.rs index 691f08061..ec70bff84 100644 --- a/src/backend/wayland/state/color_picker.rs +++ b/src/backend/wayland/state/color_picker.rs @@ -1,12 +1,14 @@ //! Clipboard helpers for color hex values. +use std::panic; +use std::time::Instant; + use super::{HexCopyOutcome, WaylandState}; use crate::backend::wayland::RuntimeOperationPoll; use crate::clipboard_text::{ClipboardTextError, read_clipboard_text_via_command}; use crate::draw::Color; use crate::i18n::{tr, trf}; -use crate::input::state::{HexPasteTarget, Toast, ToastPriority}; -use crate::input::state::{color_to_hex, parse_hex_color}; +use crate::input::state::{HexPasteTarget, Toast, ToastPriority, color_to_hex, parse_hex_color}; use std::time::Duration; impl WaylandState { @@ -15,7 +17,7 @@ impl WaylandState { let hex = color_to_hex(color); log::info!("Hex copy requested: {}", hex); self.focus - .suppress_exit_for(std::time::Instant::now(), Duration::from_millis(1500)); + .suppress_exit_for(Instant::now(), Duration::from_millis(1500)); if let Err(err) = self.clipboard.queue_hex_copy(hex) { log::warn!("Failed to start hex clipboard copy: {err}"); @@ -89,6 +91,10 @@ impl WaylandState { } /// Pastes a hex color from the clipboard. + #[expect( + clippy::disallowed_methods, + reason = "defense in depth: a panic while reading the clipboard is reported as a failed paste instead of ending the overlay" + )] pub(in crate::backend::wayland) fn handle_paste_hex_color(&mut self, target: HexPasteTarget) { if !self.input_state.hex_paste_target_is_current(target) { log::debug!("Discarding stale color-picker hex paste request"); @@ -96,8 +102,8 @@ impl WaylandState { } log::info!("Hex paste requested"); self.focus - .suppress_exit_for(std::time::Instant::now(), Duration::from_millis(1500)); - let clipboard = match std::panic::catch_unwind(read_clipboard_text_via_command) { + .suppress_exit_for(Instant::now(), Duration::from_millis(1500)); + let clipboard = match panic::catch_unwind(read_clipboard_text_via_command) { Ok(Ok(text)) => text, Ok(Err(ClipboardTextError::Empty)) => { self.input_state.push_toast( diff --git a/src/backend/wayland/state/core/accessors.rs b/src/backend/wayland/state/core/accessors.rs index 25a29fec1..0785042dd 100644 --- a/src/backend/wayland/state/core/accessors.rs +++ b/src/backend/wayland/state/core/accessors.rs @@ -1,4 +1,7 @@ -use super::super::*; +use crate::config::XdgFocusLossBehavior; + +use super::super::WaylandState; +use crate::session::SessionOptions; use smithay_client_toolkit::shell::WaylandSurface; use std::time::Instant; @@ -131,7 +134,7 @@ impl WaylandState { pub(in crate::backend::wayland) fn xdg_focus_loss_exits_overlay(&self) -> bool { matches!( self.config.ui.xdg_focus_loss_behavior, - crate::config::XdgFocusLossBehavior::Exit + XdgFocusLossBehavior::Exit ) } diff --git a/src/backend/wayland/state/core/init.rs b/src/backend/wayland/state/core/init.rs index 7c35f1bd2..742642c5a 100644 --- a/src/backend/wayland/state/core/init.rs +++ b/src/backend/wayland/state/core/init.rs @@ -1,6 +1,21 @@ +use crate::backend::wayland::runtime_ui_state::ToolbarPositionSnapshot; +use crate::ocr::OcrController; +use crate::ui::theme::Theme; + +#[cfg(feature = "tablet-input")] +use super::super::TabletSettings; use super::super::buffer_damage::BufferDamageTracker; -use super::super::*; +use super::super::helpers::{drag_log, force_inline_toolbars_requested}; +use super::super::{WaylandState, WaylandStateInit, perf}; +use crate::backend::wayland::capture::CaptureState; +use crate::backend::wayland::frozen::FrozenState; +use crate::backend::wayland::session::SessionState; +use crate::backend::wayland::surface::{SurfacePlacement, SurfaceState}; +use crate::backend::wayland::toolbar::ToolbarSurfaceManager; +use crate::backend::wayland::zoom::ZoomState; +use crate::backend::wayland::{RuntimeOperationController, RuntimeOperationIdSource}; use crate::env_vars::FORCE_INLINE_TOOLBARS_ENV; +use log::info; impl WaylandState { pub(in crate::backend::wayland) fn new(init: WaylandStateInit) -> Self { @@ -68,7 +83,7 @@ impl WaylandState { // committed drag is layered on top. Clamping happens on the first // apply against real output geometry, not here, so an override from a // now-disconnected monitor degrades instead of being lost. - let mut positions = crate::backend::wayland::runtime_ui_state::ToolbarPositionSnapshot { + let mut positions = ToolbarPositionSnapshot { top: (config.ui.toolbar.top_offset, config.ui.toolbar.top_offset_y), }; if let Some(runtime_ui) = runtime_ui.as_ref() { @@ -103,7 +118,7 @@ impl WaylandState { runtime_operation_ids, runtime_wake.clone(), ); - let ocr = crate::ocr::OcrController::new(runtime_wake.clone()); + let ocr = OcrController::new(runtime_wake.clone()); let preferences = super::super::preference_stores::PreferenceStores::new( onboarding, palette_recents, @@ -119,7 +134,7 @@ impl WaylandState { toolbar_chrome, toolbar_drag: super::super::toolbar::ToolbarDrag::new(), render: super::super::render::RenderRuntime::new( - crate::ui::theme::Theme::resolve(config.ui.theme.to_theme_mode()), + Theme::resolve(config.ui.theme.to_theme_mode()), ui_text, text_measurer, ), diff --git a/src/backend/wayland/state/core/output.rs b/src/backend/wayland/state/core/output.rs index 969ac84dc..5bf67c3e7 100644 --- a/src/backend/wayland/state/core/output.rs +++ b/src/backend/wayland/state/core/output.rs @@ -1,13 +1,8 @@ -use crate::input::state::{Toast, ToastPriority}; -use log::{info, warn}; -use smithay_client_toolkit::shell::{WaylandSurface, wlr_layer::Anchor}; use std::time::{Duration, Instant}; -use super::super::*; -use crate::{ - backend::wayland::backend::event_loop::session_save, input::state::OutputFocusAction, - notification, session, -}; +use super::super::WaylandState; +use super::super::buffer_damage::FullDamageReason; +use wayland_client::protocol::wl_output; mod focus; mod identity; @@ -74,14 +69,30 @@ enum OutputTransitionStart { ResolveTransition, } -fn output_transition_start( +/// What the session knows when the surface reports a physical output. +#[derive(Debug, Clone, Copy, Default)] +struct OutputTransitionContext { loaded: bool, + /// The output differs from the session's current logical target. target_changed: bool, + /// A pending transition from this epoch already targets this output. matching_pending: bool, + /// A transition from the current target epoch is pending. same_epoch_pending: bool, live_source_resolution_pending: bool, interaction_active: bool, -) -> OutputTransitionStart { +} + +fn output_transition_start(context: OutputTransitionContext) -> OutputTransitionStart { + let OutputTransitionContext { + loaded, + target_changed, + matching_pending, + same_epoch_pending, + live_source_resolution_pending, + interaction_active, + } = context; + let superseding_pending_destination = same_epoch_pending && !matching_pending; if !target_changed && (loaded || superseding_pending_destination || live_source_resolution_pending) @@ -102,16 +113,22 @@ fn output_transition_retry_at(backoff: Duration) -> Instant { Instant::now() + backoff } -fn live_source_reconciliation_ready( +/// What the session knows when it checks for an idle moment to resolve its +/// live source. +#[derive(Debug, Clone, Copy)] +struct LiveSourceReconcileContext { live_source_resolution_pending: bool, output_transition_pending: bool, interaction_active: bool, + /// The persistence worker can accept operations. worker_healthy: bool, -) -> bool { - live_source_resolution_pending - && !output_transition_pending - && !interaction_active - && worker_healthy +} + +fn live_source_reconciliation_ready(context: LiveSourceReconcileContext) -> bool { + context.live_source_resolution_pending + && !context.output_transition_pending + && !context.interaction_active + && context.worker_healthy } #[cfg(test)] diff --git a/src/backend/wayland/state/core/output/focus.rs b/src/backend/wayland/state/core/output/focus.rs index 81ba3ab85..32c904d8f 100644 --- a/src/backend/wayland/state/core/output/focus.rs +++ b/src/backend/wayland/state/core/output/focus.rs @@ -1,5 +1,12 @@ -use super::*; +use crate::backend::wayland::state::WaylandState; +use crate::backend::wayland::state::buffer_damage::FullDamageReason; use crate::i18n::tr; +use crate::input::state::{OutputFocusAction, Toast, ToastPriority}; +use log::{info, warn}; +use smithay_client_toolkit::shell::WaylandSurface; +use smithay_client_toolkit::shell::wlr_layer::Anchor; +use wayland_client::QueueHandle; +use wayland_client::protocol::wl_output; impl WaylandState { pub(in crate::backend::wayland) fn handle_output_focus_action( @@ -148,6 +155,8 @@ impl WaylandState { #[cfg(test)] mod tests { + use crate::config::Config; + use super::*; use crate::backend::wayland::frozen::FrozenImage; use crate::backend::wayland::handlers::test_support::HandlerFixture; @@ -156,7 +165,7 @@ mod tests { #[test] fn explicit_output_switch_invalidates_inactive_zoom_cache_before_same_output_enter() { - let mut config = crate::config::Config::default(); + let mut config = Config::default(); config.ui.xdg_fullscreen = true; config.ui.multi_monitor_enabled = true; let mut fixture = HandlerFixture::with_outputs(config, 3); diff --git a/src/backend/wayland/state/core/output/identity.rs b/src/backend/wayland/state/core/output/identity.rs index 71d8adfb1..051724395 100644 --- a/src/backend/wayland/state/core/output/identity.rs +++ b/src/backend/wayland/state/core/output/identity.rs @@ -1,4 +1,8 @@ -use super::*; +use crate::util; + +use super::OUTPUT_BADGE_MAX_LEN; +use crate::backend::wayland::state::WaylandState; +use wayland_client::protocol::wl_output; impl WaylandState { pub(in crate::backend::wayland) fn preferred_fullscreen_output( @@ -67,10 +71,7 @@ impl WaylandState { let info = self.protocol.output().info(output)?; if let Some(name) = info.name.as_deref().filter(|name| !name.is_empty()) { - return Some(crate::util::truncate_with_ellipsis( - name, - OUTPUT_BADGE_MAX_LEN, - )); + return Some(util::truncate_with_ellipsis(name, OUTPUT_BADGE_MAX_LEN)); } let label = match (info.make.trim(), info.model.trim()) { @@ -80,10 +81,7 @@ impl WaylandState { (make, model) => format!("{make} {model}"), }; - Some(crate::util::truncate_with_ellipsis( - &label, - OUTPUT_BADGE_MAX_LEN, - )) + Some(util::truncate_with_ellipsis(&label, OUTPUT_BADGE_MAX_LEN)) } pub(in crate::backend::wayland) fn refresh_active_output_label(&mut self) { diff --git a/src/backend/wayland/state/core/output/session_ops.rs b/src/backend/wayland/state/core/output/session_ops.rs index 74726521c..9ac74be00 100644 --- a/src/backend/wayland/state/core/output/session_ops.rs +++ b/src/backend/wayland/state/core/output/session_ops.rs @@ -1,8 +1,15 @@ -use super::*; +use std::env; +use std::path::{Path, PathBuf}; + +use crate::backend::wayland::backend::event_loop::session_save; use crate::backend::wayland::session::{ ExpandedTooLarge, OutputSessionLoad, commit_output_load, load_output_session, }; +use crate::backend::wayland::state::WaylandState; use crate::i18n::{tr, trf}; +use crate::input::state::{Toast, ToastPriority}; +use crate::{env_vars, notification, session}; +use std::time::Duration; impl WaylandState { /// Loads and commits `staged` as the session for the output identified as @@ -74,13 +81,11 @@ impl WaylandState { } /// The remembered session this run continues, while no session has loaded. - pub(in crate::backend::wayland) fn unloaded_remembered_session( - &self, - ) -> Option { + pub(in crate::backend::wayland) fn unloaded_remembered_session(&self) -> Option { self.session_home .remembered() .filter(|_| !self.session.is_loaded()) - .map(std::path::Path::to_path_buf) + .map(Path::to_path_buf) } /// After a launch-time session load, announce ink restored onto the @@ -97,8 +102,7 @@ impl WaylandState { return; } - let daemon_toggle = - std::env::var_os(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV).is_some(); + let daemon_toggle = env::var_os(env_vars::OVERLAY_CHILD_GENERATION_ENV).is_some(); let step = launch_restore_step( daemon_toggle, self.session.has_loaded_board_data(), diff --git a/src/backend/wayland/state/core/output/tests.rs b/src/backend/wayland/state/core/output/tests.rs index fcddf2f7c..24168ed09 100644 --- a/src/backend/wayland/state/core/output/tests.rs +++ b/src/backend/wayland/state/core/output/tests.rs @@ -1,18 +1,26 @@ +use crate::draw::TextMeasurer; +use crate::input::InputState; + use super::{ - OutputTransitionStart, live_source_reconciliation_ready, output_transition_retry_at, - output_transition_start, -}; -use crate::backend::wayland::session::replace_output_session_snapshot; -use crate::{ - backend::wayland::session::SessionState, - draw::{Color, Frame, Shape}, - input::state::test_support::make_test_input_state, - session::{BoardPagesSnapshot, BoardSnapshot, SessionOptions, SessionSnapshot}, + LiveSourceReconcileContext, OutputTransitionContext, OutputTransitionStart, + live_source_reconciliation_ready, output_transition_retry_at, output_transition_start, }; +use crate::backend::wayland::session::{SessionState, replace_output_session_snapshot}; +use crate::draw::{Color, Frame, Shape}; +use crate::input::state::test_support::make_test_input_state; +use crate::session::{BoardPagesSnapshot, BoardSnapshot, SessionOptions, SessionSnapshot}; use std::path::PathBuf; use std::time::{Duration, Instant}; -fn add_test_line(input: &mut crate::input::InputState) { +/// A pending live-source resolution with nothing in its way. +const IDLE_PENDING_RESOLUTION: LiveSourceReconcileContext = LiveSourceReconcileContext { + live_source_resolution_pending: true, + output_transition_pending: false, + interaction_active: false, + worker_healthy: true, +}; + +fn add_test_line(input: &mut InputState) { input.boards.active_frame_mut().add_shape(Shape::Line { x1: 0, y1: 0, @@ -35,13 +43,8 @@ fn empty_output_load_replaces_source_board_contents() { add_test_line(&mut input); assert_eq!(input.boards.active_frame().shapes.len(), 1); - replace_output_session_snapshot( - &mut input, - &crate::draw::TextMeasurer::default(), - None, - &options, - ) - .expect("empty output replacement"); + replace_output_session_snapshot(&mut input, &TextMeasurer::default(), None, &options) + .expect("empty output replacement"); assert!(input.boards.active_frame().shapes.is_empty()); } @@ -68,7 +71,7 @@ fn partial_output_load_clears_boards_omitted_from_snapshot() { replace_output_session_snapshot( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), Some(snapshot), &options, ) @@ -101,7 +104,7 @@ fn failed_output_replacement_preserves_source_board_contents() { let err = replace_output_session_snapshot( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), Some(snapshot), &options, ) @@ -114,7 +117,11 @@ fn failed_output_replacement_preserves_source_board_contents() { #[test] fn configure_retry_keeps_matching_epoch_bound_transition() { assert_eq!( - output_transition_start(false, false, true, true, false, false), + output_transition_start(OutputTransitionContext { + matching_pending: true, + same_epoch_pending: true, + ..OutputTransitionContext::default() + }), OutputTransitionStart::KeepPending ); } @@ -122,11 +129,20 @@ fn configure_retry_keeps_matching_epoch_bound_transition() { #[test] fn initial_and_loaded_transitions_defer_for_active_interaction() { assert_eq!( - output_transition_start(false, true, false, false, false, true), + output_transition_start(OutputTransitionContext { + target_changed: true, + interaction_active: true, + ..OutputTransitionContext::default() + }), OutputTransitionStart::DeferForInteraction ); assert_eq!( - output_transition_start(true, true, false, false, false, true), + output_transition_start(OutputTransitionContext { + loaded: true, + target_changed: true, + interaction_active: true, + ..OutputTransitionContext::default() + }), OutputTransitionStart::DeferForInteraction ); } @@ -134,11 +150,18 @@ fn initial_and_loaded_transitions_defer_for_active_interaction() { #[test] fn transition_start_distinguishes_initial_load_and_loaded_switch() { assert_eq!( - output_transition_start(false, true, false, false, false, false), + output_transition_start(OutputTransitionContext { + target_changed: true, + ..OutputTransitionContext::default() + }), OutputTransitionStart::LoadInitial ); assert_eq!( - output_transition_start(true, true, false, false, false, false), + output_transition_start(OutputTransitionContext { + loaded: true, + target_changed: true, + ..OutputTransitionContext::default() + }), OutputTransitionStart::ResolveTransition ); } @@ -168,14 +191,13 @@ fn unloaded_dirty_source_with_nonmatching_pending_transition_resolves_before_loa assert!(session.is_dirty()); assert!(!matching_pending); assert_eq!( - output_transition_start( - session.is_loaded(), - changed, + output_transition_start(OutputTransitionContext { + loaded: session.is_loaded(), + target_changed: changed, matching_pending, same_epoch_pending, - false, - false, - ), + ..OutputTransitionContext::default() + }), OutputTransitionStart::ResolveTransition ); assert!(session.is_dirty()); @@ -196,7 +218,11 @@ fn active_stroke_return_keeps_an_unloaded_dirty_source_protected() { ); let committed_epoch = committed.target_epoch(); assert_eq!( - output_transition_start(false, false, false, true, false, true), + output_transition_start(OutputTransitionContext { + same_epoch_pending: true, + interaction_active: true, + ..OutputTransitionContext::default() + }), OutputTransitionStart::IgnoreCurrentTarget ); assert!( @@ -207,7 +233,11 @@ fn active_stroke_return_keeps_an_unloaded_dirty_source_protected() { assert!(!committed.is_loaded()); assert!(committed.resolve_live_source_resolution(false, true)); assert_eq!( - output_transition_start(false, false, false, false, true, true), + output_transition_start(OutputTransitionContext { + live_source_resolution_pending: true, + interaction_active: true, + ..OutputTransitionContext::default() + }), OutputTransitionStart::IgnoreCurrentTarget ); @@ -217,7 +247,10 @@ fn active_stroke_return_keeps_an_unloaded_dirty_source_protected() { assert!(committed.resolve_live_source_resolution(false, false)); assert_eq!(committed.target_epoch(), committed_epoch); assert_eq!( - output_transition_start(true, false, false, false, false, false), + output_transition_start(OutputTransitionContext { + loaded: true, + ..OutputTransitionContext::default() + }), OutputTransitionStart::IgnoreCurrentTarget ); @@ -248,18 +281,38 @@ fn active_stroke_return_keeps_an_unloaded_dirty_source_protected() { assert!(!canceled.is_dirty()); assert_eq!(canceled.target_epoch(), canceled_epoch); assert_eq!( - output_transition_start(false, false, false, false, false, false), + output_transition_start(OutputTransitionContext::default()), OutputTransitionStart::LoadInitial ); } #[test] fn live_source_reconciliation_runs_only_when_idle_without_a_destination() { - assert!(live_source_reconciliation_ready(true, false, false, true)); - assert!(!live_source_reconciliation_ready(false, false, false, true)); - assert!(!live_source_reconciliation_ready(true, true, false, true)); - assert!(!live_source_reconciliation_ready(true, false, true, true)); - assert!(!live_source_reconciliation_ready(true, false, false, false)); + assert!(live_source_reconciliation_ready(IDLE_PENDING_RESOLUTION)); + assert!(!live_source_reconciliation_ready( + LiveSourceReconcileContext { + live_source_resolution_pending: false, + ..IDLE_PENDING_RESOLUTION + } + )); + assert!(!live_source_reconciliation_ready( + LiveSourceReconcileContext { + output_transition_pending: true, + ..IDLE_PENDING_RESOLUTION + } + )); + assert!(!live_source_reconciliation_ready( + LiveSourceReconcileContext { + interaction_active: true, + ..IDLE_PENDING_RESOLUTION + } + )); + assert!(!live_source_reconciliation_ready( + LiveSourceReconcileContext { + worker_healthy: false, + ..IDLE_PENDING_RESOLUTION + } + )); } #[test] @@ -270,12 +323,12 @@ fn clean_unloaded_cancellation_arms_immediate_source_resolution() { assert!(session.cancel_output_transition_for_live_source().is_some()); assert!(session.has_pending_live_source_resolution()); - assert!(live_source_reconciliation_ready(true, false, false, true)); + assert!(live_source_reconciliation_ready(IDLE_PENDING_RESOLUTION)); assert!(!session.resolve_live_source_resolution(false, false)); assert!(!session.is_loaded()); assert!(!session.has_pending_live_source_resolution()); assert_eq!( - output_transition_start(false, false, false, false, false, false), + output_transition_start(OutputTransitionContext::default()), OutputTransitionStart::LoadInitial ); } diff --git a/src/backend/wayland/state/core/output/transition.rs b/src/backend/wayland/state/core/output/transition.rs index 7834296f7..dc165b88b 100644 --- a/src/backend/wayland/state/core/output/transition.rs +++ b/src/backend/wayland/state/core/output/transition.rs @@ -1,8 +1,17 @@ -use super::*; +use super::{ + LiveSourceReconcileContext, OutputTransitionContext, OutputTransitionStart, + live_source_reconciliation_ready, output_transition_retry_at, output_transition_start, +}; +use crate::backend::wayland::backend::event_loop::session_save; use crate::backend::wayland::session::{ SessionCommand, SourceWriteRefused, interaction_defer_interval, }; +use crate::backend::wayland::state::WaylandState; use crate::i18n::trf; +use crate::input::state::{Toast, ToastPriority}; +use crate::session; +use log::{info, warn}; +use std::time::Instant; impl WaylandState { pub(in crate::backend::wayland) fn begin_session_output_transition( @@ -31,14 +40,14 @@ impl WaylandState { let live_source_resolution_pending = self .session .resolve_live_source_resolution(input_dirty, interaction_active); - let start = output_transition_start( - self.session.is_loaded(), - changed, + let start = output_transition_start(OutputTransitionContext { + loaded: self.session.is_loaded(), + target_changed: changed, matching_pending, same_epoch_pending, live_source_resolution_pending, interaction_active, - ); + }); let retry_at = Instant::now() + interaction_defer_interval(); match start { @@ -109,7 +118,7 @@ impl WaylandState { OutputTransitionStart::LoadInitial | OutputTransitionStart::ResolveTransition => { if let Err(err) = self.run_output_transition( staged_options.clone(), - physical_output_identity.clone(), + physical_output_identity.as_deref(), reason, ) { warn!("Failed to complete session transition for {reason}: {err:#}"); @@ -157,7 +166,7 @@ impl WaylandState { }; if let Err(err) = self.run_output_transition( pending.staged_options.clone(), - pending.physical_output_identity.clone(), + pending.physical_output_identity.as_deref(), "deferred output transition", ) { let retry_at = output_transition_retry_at(self.output_transition_failure_backoff()); @@ -204,12 +213,12 @@ impl WaylandState { } let interaction_active = self.session_transaction.is_some() || session_save::should_defer_for_interaction(self); - if !live_source_reconciliation_ready( - true, - self.session.pending_output_transition().is_some(), + if !live_source_reconciliation_ready(LiveSourceReconcileContext { + live_source_resolution_pending: true, + output_transition_pending: self.session.pending_output_transition().is_some(), interaction_active, - self.persistence.is_healthy(), - ) { + worker_healthy: self.persistence.is_healthy(), + }) { return false; } @@ -241,7 +250,7 @@ impl WaylandState { fn run_output_transition( &mut self, staged_options: session::SessionOptions, - physical_output_identity: Option, + physical_output_identity: Option<&str>, reason: &str, ) -> anyhow::Result<()> { if self.session_transaction.is_some() || session_save::should_defer_for_interaction(self) { @@ -263,14 +272,14 @@ impl WaylandState { .session .pending_output_transition() .is_some_and(|pending| { - pending.physical_output_identity == physical_output_identity + pending.physical_output_identity.as_deref() == physical_output_identity && pending.staged_options.session_file_path() == staged_options.session_file_path() }) { self.session.stage_output_transition( staged_options, - physical_output_identity.clone(), + physical_output_identity.map(str::to_string), Instant::now(), ); } @@ -282,7 +291,7 @@ impl WaylandState { let remembered = self.unloaded_remembered_session(); let home = self .session_home - .options_for_output(physical_output_identity.as_deref()) + .options_for_output(physical_output_identity) .map(Box::new); let leaves_placeholder = physical_output_identity.is_some() && self.session_options().is_some_and(|current| { diff --git a/src/backend/wayland/state/core/overlay.rs b/src/backend/wayland/state/core/overlay.rs index 5f06c4a02..eb68d68da 100644 --- a/src/backend/wayland/state/core/overlay.rs +++ b/src/backend/wayland/state/core/overlay.rs @@ -1,5 +1,10 @@ +use crate::input::state::RegionSelectUiState; + +use super::super::WaylandState; +use super::super::buffer_damage::FullDamageReason; use super::super::capture::OverlayCaptureBarrier; -use super::super::*; +use crate::backend::wayland::overlay_passthrough::set_surface_clickthrough; +use anyhow::Result; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub(in crate::backend::wayland) enum OverlaySuppression { @@ -244,7 +249,7 @@ impl WaylandState { } } -fn capture_picker_chrome_suppressed_for(region: crate::input::state::RegionSelectUiState) -> bool { +fn capture_picker_chrome_suppressed_for(region: RegionSelectUiState) -> bool { region.is_engaged() && region.purpose().is_some_and(|purpose| purpose.is_capture()) } diff --git a/src/backend/wayland/state/core/session.rs b/src/backend/wayland/state/core/session.rs index 035db728b..4ac993f6e 100644 --- a/src/backend/wayland/state/core/session.rs +++ b/src/backend/wayland/state/core/session.rs @@ -1,13 +1,15 @@ use anyhow::Result; -use super::super::*; +use crate::input::state::{Toast, ToastPriority}; + +use super::super::WaylandState; use crate::backend::wayland::backend::event_loop::session_save::{ handle_autosave_failure, handle_persistence_transport_failure, report_autosave_success, }; +use crate::backend::wayland::session::driver::{self, SessionCommandRuntime}; use crate::backend::wayland::session::{ PersistenceController, SaveCompletion, SessionCommand, SessionCommandReport, SessionCommandTransaction, SessionTransaction, - driver::{self, SessionCommandRuntime}, }; use crate::i18n::tr; use crate::session::ToolStateSnapshot; @@ -39,7 +41,7 @@ impl SessionCommandRuntime for WaylandState { } fn session_command_queued(&mut self) { - self.input_state.push_toast(crate::input::state::ToastPriority::Action, "session.command", crate::input::state::Toast::info(tr("Session command queued for the visible session. If the session changes, retry it on the intended session."))); + self.input_state.push_toast(ToastPriority::Action, "session.command", Toast::info(tr("Session command queued for the visible session. If the session changes, retry it on the intended session."))); } fn session_target_committed(&mut self) { @@ -59,7 +61,7 @@ impl SessionCommandRuntime for WaylandState { failures: Vec<(SessionCommand, anyhow::Error)>, closing: bool, ) { - WaylandState::fail_queued_session_commands(self, failures, closing); + WaylandState::fail_queued_session_commands(self, &failures, closing); } fn session_transport_failed(&mut self, error: &anyhow::Error) { @@ -67,7 +69,7 @@ impl SessionCommandRuntime for WaylandState { } fn autosave_succeeded(&mut self, save: SaveCompletion, execution_time: Duration) { - report_autosave_success(self, save, execution_time); + report_autosave_success(self, &save, execution_time); } fn autosave_failed(&mut self, error: &anyhow::Error) { diff --git a/src/backend/wayland/state/core/session_home.rs b/src/backend/wayland/state/core/session_home.rs index 87c2f99bb..0978ae01f 100644 --- a/src/backend/wayland/state/core/session_home.rs +++ b/src/backend/wayland/state/core/session_home.rs @@ -2,7 +2,7 @@ use std::path::Path; use log::{info, warn}; -use super::super::*; +use super::super::WaylandState; use crate::backend::wayland::session::session_target; use crate::daemon::protocol_v2::publish_session_from_environment; use crate::i18n::trf; diff --git a/src/backend/wayland/state/desktop_open.rs b/src/backend/wayland/state/desktop_open.rs index 0f75eb8a8..5a412b08a 100644 --- a/src/backend/wayland/state/desktop_open.rs +++ b/src/backend/wayland/state/desktop_open.rs @@ -4,7 +4,9 @@ //! worker, wakes the Wayland loop, and requests overlay exit only after the //! opener has been handed off successfully. -use std::time::Duration; +use std::time::{Duration, Instant}; + +use crate::desktop_open; use super::{RuntimeOperationController, WaylandState}; use crate::backend::wayland::{RuntimeOperationPoll, RuntimeOperationSubmitFailure}; @@ -44,10 +46,10 @@ fn handoff_exit_intent(completion: &DesktopOpenCompletion) -> HandoffExitIntent impl WaylandState { pub(in crate::backend::wayland) fn handle_desktop_open(&mut self, request: DesktopOpenRequest) { if let Err(failure) = - queue_desktop_open(&mut self.desktop_open, request, crate::desktop_open::open) + queue_desktop_open(&mut self.desktop_open, request, desktop_open::open) { let (error, request) = failure.into_parts(); - self.report_desktop_open_failure(request, error.to_string()); + self.report_desktop_open_failure(&request, &error.to_string()); } } @@ -70,7 +72,7 @@ impl WaylandState { ); } DesktopOpenCompletion::Failed { request, reason } => { - self.report_desktop_open_failure(request, reason); + self.report_desktop_open_failure(&request, &reason); } } } @@ -79,7 +81,7 @@ impl WaylandState { self.desktop_open.is_active() } - fn report_desktop_open_failure(&mut self, request: DesktopOpenRequest, reason: String) { + fn report_desktop_open_failure(&mut self, request: &DesktopOpenRequest, reason: &str) { log::warn!( "Failed to open {} at {}: {}", request.target_name(), @@ -89,7 +91,7 @@ impl WaylandState { // If an opener partially launched an application before failing, keep // this failure visible instead of immediately applying focus-loss exit. self.focus - .suppress_exit_for(std::time::Instant::now(), Duration::from_millis(1500)); + .suppress_exit_for(Instant::now(), Duration::from_millis(1500)); self.input_state.push_toast( ToastPriority::Critical, "launcher", @@ -150,24 +152,25 @@ fn classify_result( #[cfg(test)] mod tests { use std::sync::mpsc; + use std::thread; use std::time::{Duration, Instant}; use super::*; - use crate::backend::wayland::{ - RuntimeOperationIdSource, RuntimeWakeSource, - handlers::keyboard::{XdgFocusLeaveAction, xdg_focus_leave_action}, + use crate::backend::wayland::handlers::keyboard::{ + XdgFocusLeaveAction, XdgFocusLeaveContext, xdg_focus_leave_action, }; + use crate::backend::wayland::{RuntimeOperationIdSource, RuntimeWakeSource}; #[test] fn dispatch_returns_before_helper_completion_and_exit_waits_for_success() { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut controller = RuntimeOperationController::new(RuntimeOperationIdSource::new(), wake.handle()); let request = DesktopOpenRequest::CaptureFolder("/tmp/capture".into()); let (release_tx, release_rx) = mpsc::channel(); let fallback_release = release_tx.clone(); - std::thread::spawn(move || { - std::thread::sleep(Duration::from_millis(500)); + thread::spawn(move || { + thread::sleep(Duration::from_millis(500)); let _ = fallback_release.send(()); }); @@ -183,7 +186,12 @@ mod tests { ); assert!(controller.is_active()); assert_eq!( - xdg_focus_leave_action(true, controller.is_active(), false, true), + xdg_focus_leave_action(XdgFocusLeaveContext { + is_xdg_window: true, + desktop_open_in_progress: controller.is_active(), + focus_exit_suppressed: false, + focus_loss_exits_overlay: true, + }), XdgFocusLeaveAction::AwaitDesktopOpen, ); assert!(matches!( @@ -204,7 +212,7 @@ mod tests { Instant::now() < deadline, "desktop-open completion was not published" ); - std::thread::yield_now(); + thread::yield_now(); } DesktopOpenCompletion::HandedOff(completed) => { assert_eq!(completed, request); diff --git a/src/backend/wayland/state/eyedropper.rs b/src/backend/wayland/state/eyedropper.rs index dc8a5f347..1f7be1a38 100644 --- a/src/backend/wayland/state/eyedropper.rs +++ b/src/backend/wayland/state/eyedropper.rs @@ -1,15 +1,16 @@ use crate::backend::wayland::acquisition::{ScreenAcquisitionOutcome, ScreenAcquisitionOwner}; use crate::backend::wayland::frozen::FrozenImage; use crate::backend::wayland::zoom::ZoomWaiterOwner; +use crate::config::Action; use crate::draw::Color; -use crate::input::state::EyedropperCaptureSource; -use crate::input::state::{Toast, ToastPriority}; +use crate::input::state::{EyedropperCaptureSource, Toast, ToastPriority}; +use crate::ui; use super::WaylandState; use super::acquisition::report_screen_source_activation_rejected_to; use super::screen_image::{ - DisplayedScreenImage, ScreenSourceEntry, current_screen_source_token, displayed_screen_image, - image_point_for_screen_point, screen_source_entry, screen_source_token, + DisplayedScreenImage, ScreenSourceConditions, ScreenSourceEntry, current_screen_source_token, + displayed_screen_image, image_point_for_screen_point, screen_source_entry, screen_source_token, }; use crate::i18n::tr; @@ -71,13 +72,13 @@ impl WaylandState { // during the wait would commit that stroke's peak pressure to the tool. self.retire_stylus_contact(); - let decision = screen_source_entry( - self.background_image_source().is_some(), - self.input_state.board_is_transparent(), - self.zoom.is_engaged(), - self.zoom.active, - self.frozen.enabled(), - ); + let decision = screen_source_entry(ScreenSourceConditions { + has_source: self.background_image_source().is_some(), + board_is_transparent: self.input_state.board_is_transparent(), + zoom_engaged: self.zoom.is_engaged(), + zoom_active: self.zoom.active, + frozen_enabled: self.frozen.enabled(), + }); match decision { ScreenSourceEntry::Activate => { if !self.activate_eyedropper_sampler(None) { @@ -114,20 +115,14 @@ impl WaylandState { Toast::info(tr( "Screen eyedropper isn't available while zoomed on a solid board.", )) - .action( - tr("Switch to transparent"), - crate::config::Action::ReturnToTransparent, - ), + .action(tr("Switch to transparent"), Action::ReturnToTransparent), ); } ScreenSourceEntry::RefuseSolidBoard => { let toast = Toast::info(tr( "Screen eyedropper requires a transparent board or an active screen freeze.", )) - .action( - tr("Switch to transparent"), - crate::config::Action::ReturnToTransparent, - ); + .action(tr("Switch to transparent"), Action::ReturnToTransparent); self.input_state .push_toast(ToastPriority::Action, "eyedropper", toast); } @@ -257,11 +252,11 @@ impl WaylandState { return; }; let (image_x, image_y) = self.eyedropper_image_coords(&source, pointer_x, pointer_y); - let layout = crate::ui::compute_eyedropper_loupe_layout( + let layout = ui::compute_eyedropper_loupe_layout( (pointer_x, pointer_y), (screen_width, screen_height), ); - crate::ui::render_eyedropper_loupe(ctx, layout, (image_x, image_y), |x, y| { + ui::render_eyedropper_loupe(ctx, layout, (image_x, image_y), |x, y| { sample_at(source.image, x, y) }); } @@ -334,7 +329,13 @@ mod tests { #[test] fn entry_waits_for_pending_zoom_instead_of_starting_frozen_capture() { assert_eq!( - screen_source_entry(false, true, true, false, true), + screen_source_entry(ScreenSourceConditions { + has_source: false, + board_is_transparent: true, + zoom_engaged: true, + zoom_active: false, + frozen_enabled: true, + }), ScreenSourceEntry::WaitForZoom ); } @@ -342,11 +343,23 @@ mod tests { #[test] fn entry_distinguishes_solid_board_zoom_refusal() { assert_eq!( - screen_source_entry(false, false, true, true, true), + screen_source_entry(ScreenSourceConditions { + has_source: false, + board_is_transparent: false, + zoom_engaged: true, + zoom_active: true, + frozen_enabled: true, + }), ScreenSourceEntry::RefuseWhileZoomedOnSolidBoard ); assert_eq!( - screen_source_entry(false, false, false, false, true), + screen_source_entry(ScreenSourceConditions { + has_source: false, + board_is_transparent: false, + zoom_engaged: false, + zoom_active: false, + frozen_enabled: true, + }), ScreenSourceEntry::RefuseSolidBoard ); } @@ -354,7 +367,13 @@ mod tests { #[test] fn entry_uses_existing_source_before_board_policy() { assert_eq!( - screen_source_entry(true, false, false, false, true), + screen_source_entry(ScreenSourceConditions { + has_source: true, + board_is_transparent: false, + zoom_engaged: false, + zoom_active: false, + frozen_enabled: true, + }), ScreenSourceEntry::Activate ); } @@ -362,11 +381,23 @@ mod tests { #[test] fn entry_reports_missing_zoom_image_separately_from_missing_capture_support() { assert_eq!( - screen_source_entry(false, true, true, true, true), + screen_source_entry(ScreenSourceConditions { + has_source: false, + board_is_transparent: true, + zoom_engaged: true, + zoom_active: true, + frozen_enabled: true, + }), ScreenSourceEntry::ZoomImageUnavailable ); assert_eq!( - screen_source_entry(false, true, false, false, false), + screen_source_entry(ScreenSourceConditions { + has_source: false, + board_is_transparent: true, + zoom_engaged: false, + zoom_active: false, + frozen_enabled: false, + }), ScreenSourceEntry::CaptureUnavailable ); } diff --git a/src/backend/wayland/state/focus.rs b/src/backend/wayland/state/focus.rs index 319c8ecb0..28e486a36 100644 --- a/src/backend/wayland/state/focus.rs +++ b/src/backend/wayland/state/focus.rs @@ -1,10 +1,9 @@ +use std::mem; use std::time::{Duration, Instant}; use smithay_client_toolkit::shell::wlr_layer::KeyboardInteractivity; -use wayland_client::{ - Proxy, - protocol::{wl_keyboard, wl_seat}, -}; +use wayland_client::Proxy; +use wayland_client::protocol::{wl_keyboard, wl_seat}; use super::WaylandState; use super::seat_devices::SeatDevices; @@ -266,7 +265,7 @@ impl FocusState { } pub(in crate::backend::wayland) fn take_xdg_explicit_close_requested(&mut self) -> bool { - std::mem::take(&mut self.xdg_explicit_close_requested) + mem::take(&mut self.xdg_explicit_close_requested) } pub(in crate::backend::wayland) fn activation_token_to_apply(&self) -> Option { diff --git a/src/backend/wayland/state/font_catalog.rs b/src/backend/wayland/state/font_catalog.rs index 33e00a864..d46e1f315 100644 --- a/src/backend/wayland/state/font_catalog.rs +++ b/src/backend/wayland/state/font_catalog.rs @@ -6,6 +6,7 @@ use crate::backend::wayland::{ RuntimeOperationController, RuntimeOperationIdSource, RuntimeOperationPoll, RuntimeOperationSubmitFailure, RuntimeWakeHandle, }; +use crate::draw; use super::WaylandState; @@ -42,7 +43,7 @@ impl FontCatalogPrewarm { self.controller .try_submit((), "wayscriber-font-catalog", || { let started = Instant::now(); - crate::draw::prewarm_system_font_catalog(); + draw::prewarm_system_font_catalog(); started.elapsed() }) .map(|_| self.started = true) @@ -65,7 +66,7 @@ impl WaylandState { pub(in crate::backend::wayland) fn start_font_catalog_prewarm(&mut self) { if let Err(failure) = self.font_catalog.start( self.input_state.font_picker_load_failed(), - crate::draw::system_font_catalog_is_ready(), + draw::system_font_catalog_is_ready(), ) { let (error, ()) = failure.into_parts(); log::warn!("Failed to start system font catalog prewarm: {error}"); @@ -104,7 +105,7 @@ mod tests { use crate::backend::wayland::RuntimeWakeSource; fn prewarm() -> FontCatalogPrewarm { - let wake = RuntimeWakeSource::new().expect("runtime wake source"); + let wake = RuntimeWakeSource::try_new().expect("runtime wake source"); FontCatalogPrewarm::new(RuntimeOperationIdSource::new(), wake.handle()) } diff --git a/src/backend/wayland/state/gtk_toolbar.rs b/src/backend/wayland/state/gtk_toolbar.rs index c343e6e7a..4568d6580 100644 --- a/src/backend/wayland/state/gtk_toolbar.rs +++ b/src/backend/wayland/state/gtk_toolbar.rs @@ -2,8 +2,16 @@ //! feedback draining, and state pushes. See `crate::toolbar_gtk` for the //! threading model. +use std::time::Instant; + +use smithay_client_toolkit::shell::wlr_layer::Layer; use wayland_client::{Connection, QueueHandle}; +use crate::backend::wayland::RuntimeWakeHandle; +use crate::config::ChordModifiers; +use crate::input::InputState; +use crate::toolbar_gtk::GtkToolbarKind; + use super::WaylandState; use crate::backend::wayland::handlers::keyboard::ForwardedKey; use crate::toolbar_gtk::select::{ @@ -11,7 +19,7 @@ use crate::toolbar_gtk::select::{ }; use crate::toolbar_gtk::{GtkToolbarBridge, GtkToolbarFeedback, GtkToolbarUpdate}; -fn gtk_toolbar_feedback_blocked(input_state: &crate::input::InputState) -> bool { +fn gtk_toolbar_feedback_blocked(input_state: &InputState) -> bool { input_state.command_palette_is_engaged() || (input_state.region_is_engaged() && input_state @@ -38,15 +46,14 @@ impl WaylandState { /// Spawns the GTK toolbar thread when the resolved frontend is GTK. pub(in crate::backend::wayland) fn spawn_gtk_toolbar_if_selected( &mut self, - runtime_wake: crate::backend::wayland::RuntimeWakeHandle, + runtime_wake: RuntimeWakeHandle, ) { let request = requested_backend(&self.config); let preconditions = GtkPreconditions { feature_compiled: cfg!(feature = "toolbar-gtk"), layer_shell: self.protocol.layer_shell().is_some(), force_inline: super::force_inline_toolbars_requested(&self.config), - main_surface_uses_overlay_layer: self.surface.placement().layer() - == smithay_client_toolkit::shell::wlr_layer::Layer::Overlay, + main_surface_uses_overlay_layer: self.surface.placement().layer() == Layer::Overlay, }; match resolve_frontend(request, preconditions) { ToolbarFrontend::Gtk => { @@ -102,8 +109,7 @@ impl WaylandState { phase, .. } if phase.is_end() - && self.toolbar_drag.gtk_preview_kind() - == Some(crate::toolbar_gtk::GtkToolbarKind::Top) => + && self.toolbar_drag.gtk_preview_kind() == Some(GtkToolbarKind::Top) => { self.toolbar_drag.set_gtk_rebase(None); let offset = self.toolbar_chrome.top_offset(); @@ -139,7 +145,15 @@ impl WaylandState { logo, } => { if !self.input_state.modal_owns_pointer_shortcuts() { - self.try_dispatch_gdk_pointer_shortcut(button, ctrl, shift, alt, logo); + self.try_dispatch_gdk_pointer_shortcut( + button, + ChordModifiers { + ctrl, + shift, + alt, + logo, + }, + ); } } GtkToolbarFeedback::Key { @@ -162,11 +176,10 @@ impl WaylandState { ); } GtkToolbarFeedback::EscapeDismissed => { - self.input_state - .note_escape_dismissal(std::time::Instant::now()); + self.input_state.note_escape_dismissal(Instant::now()); } GtkToolbarFeedback::KeyboardReleased => { - self.reclaim_keyboard_after_toolbar_release(std::time::Instant::now()); + self.reclaim_keyboard_after_toolbar_release(Instant::now()); } GtkToolbarFeedback::TopHover { hovered } => { self.toolbar_chrome.set_gtk_top_hover(hovered); @@ -257,6 +270,8 @@ impl WaylandState { #[cfg(test)] mod modal_tests { + use crate::draw::TextMeasurer; + use super::*; use crate::config::Action; use crate::input::state::test_support::make_test_input_state; @@ -278,16 +293,12 @@ mod modal_tests { use crate::input::state::RegionPurposeTag; let mut input_state = make_test_input_state(); - input_state.activate_region_with( - &crate::draw::TextMeasurer::default(), - RegionPurposeTag::Ocr, - 1, - ); + input_state.activate_region_with(&TextMeasurer::default(), RegionPurposeTag::Ocr, 1); assert!(!gtk_toolbar_feedback_blocked(&input_state)); input_state.cancel_region_ui_only(); input_state.activate_region_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), RegionPurposeTag::CaptureDeliver, 2, ); diff --git a/src/backend/wayland/state/helper_launch.rs b/src/backend/wayland/state/helper_launch.rs index 4f7e57d7c..3493516e0 100644 --- a/src/backend/wayland/state/helper_launch.rs +++ b/src/backend/wayland/state/helper_launch.rs @@ -3,8 +3,12 @@ //! Input records semantic intent only. The backend resolves executables and //! talks to the process broker because both are runtime side effects. +use std::env; use std::ffi::{OsStr, OsString}; +use crate::process_broker::{BrokerChild, BrokerErrorKind, HelperKind, HelperLifetime}; +use crate::{about_window, process_broker}; + use super::WaylandState; use crate::configurator_destination::{ConfiguratorDestination, configurator_launch_arguments}; use crate::env_vars::CONFIGURATOR_ENV; @@ -12,13 +16,13 @@ use crate::i18n::tr; use crate::input::state::{HelperLaunchRequest, Toast, ToastPriority}; fn spawn_detached( - kind: crate::process_broker::HelperKind, + kind: HelperKind, program: &OsStr, arguments: &[OsString], -) -> anyhow::Result { - crate::process_broker::current()?.try_spawn( +) -> anyhow::Result { + process_broker::current()?.try_spawn( kind, - crate::process_broker::HelperLifetime::DetachedAfterExec, + HelperLifetime::DetachedAfterExec, program, arguments, Vec::new(), @@ -26,7 +30,7 @@ fn spawn_detached( } fn launch_deferred_by_busy_broker(error: &anyhow::Error) -> bool { - crate::process_broker::error_kind(error) == Some(crate::process_broker::BrokerErrorKind::Busy) + process_broker::error_kind(error) == Some(BrokerErrorKind::Busy) } fn launch_failure_message(error: &anyhow::Error, failed: &'static str) -> &'static str { @@ -45,13 +49,13 @@ impl WaylandState { match request { HelperLaunchRequest::About => self.launch_about_helper(), HelperLaunchRequest::Configurator(destination) => { - self.launch_configurator_helper(destination); + self.launch_configurator_helper(destination.as_ref()); } } } fn launch_about_helper(&mut self) { - let executable = match std::env::current_exe() { + let executable = match env::current_exe() { Ok(path) => path, Err(error) => { log::error!("Failed to resolve the Wayscriber executable for About: {error}"); @@ -65,9 +69,9 @@ impl WaylandState { }; match spawn_detached( - crate::process_broker::HelperKind::About, + HelperKind::About, executable.as_os_str(), - &crate::about_window::launch_arguments(), + &about_window::launch_arguments(), ) { Ok(child) => { log::info!("Launched About window (pid {})", child.id()); @@ -87,16 +91,12 @@ impl WaylandState { } } - fn launch_configurator_helper(&mut self, destination: Option) { - let binary = std::env::var(CONFIGURATOR_ENV) - .unwrap_or_else(|_| "wayscriber-configurator".to_string()); - let arguments = configurator_launch_arguments(destination.as_ref()); + fn launch_configurator_helper(&mut self, destination: Option<&ConfiguratorDestination>) { + let binary = + env::var(CONFIGURATOR_ENV).unwrap_or_else(|_| "wayscriber-configurator".to_string()); + let arguments = configurator_launch_arguments(destination); - match spawn_detached( - crate::process_broker::HelperKind::Configurator, - OsStr::new(&binary), - &arguments, - ) { + match spawn_detached(HelperKind::Configurator, OsStr::new(&binary), &arguments) { Ok(child) => { log::info!( "Launched wayscriber-configurator (binary: {binary}, pid: {})", diff --git a/src/backend/wayland/state/helpers.rs b/src/backend/wayland/state/helpers.rs index 094920182..a3f66e633 100644 --- a/src/backend/wayland/state/helpers.rs +++ b/src/backend/wayland/state/helpers.rs @@ -1,13 +1,17 @@ -use std::{sync::OnceLock, time::Duration}; +use std::env; +use std::sync::OnceLock; +use std::time::Duration; -use wayland_client::{Proxy, protocol::wl_surface}; +use wayland_client::Proxy; +use wayland_client::protocol::wl_surface; +use crate::config::Config; use crate::env_vars::{ DEBUG_DAMAGE_ENV, DEBUG_TOOLBAR_DRAG_ENV, FORCE_INLINE_TOOLBARS_ENV, TOOLBAR_DRAG_HANDOFF_MS_ENV, TOOLBAR_DRAG_PREVIEW_ENV, TOOLBAR_DRAG_THROTTLE_MS_ENV, TOOLBAR_POINTER_LOCK_ENV, }; -use crate::{config::Config, util::Rect}; +use crate::util::Rect; pub(in crate::backend::wayland) fn scale_damage_regions( regions: Vec, @@ -56,9 +60,7 @@ pub(super) fn parse_debug_damage_env(raw: &str) -> bool { pub(in crate::backend::wayland) fn debug_damage_logging_enabled() -> bool { static ENABLED: OnceLock = OnceLock::new(); - *ENABLED.get_or_init(|| { - parse_debug_damage_env(&std::env::var(DEBUG_DAMAGE_ENV).unwrap_or_default()) - }) + *ENABLED.get_or_init(|| parse_debug_damage_env(&env::var(DEBUG_DAMAGE_ENV).unwrap_or_default())) } pub(in crate::backend::wayland) fn surface_id(surface: &wl_surface::WlSurface) -> u32 { @@ -67,30 +69,29 @@ pub(in crate::backend::wayland) fn surface_id(surface: &wl_surface::WlSurface) - pub(in crate::backend::wayland) fn debug_toolbar_drag_logging_enabled() -> bool { static ENABLED: OnceLock = OnceLock::new(); - *ENABLED.get_or_init(|| { - parse_boolish_env(&std::env::var(DEBUG_TOOLBAR_DRAG_ENV).unwrap_or_default()) - }) + *ENABLED + .get_or_init(|| parse_boolish_env(&env::var(DEBUG_TOOLBAR_DRAG_ENV).unwrap_or_default())) } pub(in crate::backend::wayland) fn toolbar_pointer_lock_enabled() -> bool { static ENABLED: OnceLock = OnceLock::new(); *ENABLED.get_or_init(|| { // Default ON: without pointer lock, layer-shell toolbar drags jitter/flicker as surfaces move. - parse_boolish_env(&std::env::var(TOOLBAR_POINTER_LOCK_ENV).unwrap_or_else(|_| "1".into())) + parse_boolish_env(&env::var(TOOLBAR_POINTER_LOCK_ENV).unwrap_or_else(|_| "1".into())) }) } pub(in crate::backend::wayland) fn toolbar_drag_preview_enabled() -> bool { static ENABLED: OnceLock = OnceLock::new(); *ENABLED.get_or_init(|| { - parse_boolish_env(&std::env::var(TOOLBAR_DRAG_PREVIEW_ENV).unwrap_or_else(|_| "1".into())) + parse_boolish_env(&env::var(TOOLBAR_DRAG_PREVIEW_ENV).unwrap_or_else(|_| "1".into())) }) } pub(in crate::backend::wayland) fn toolbar_drag_throttle_interval() -> Option { static VALUE: OnceLock> = OnceLock::new(); *VALUE.get_or_init(|| { - let raw = std::env::var(TOOLBAR_DRAG_THROTTLE_MS_ENV).unwrap_or_else(|_| "12".into()); + let raw = env::var(TOOLBAR_DRAG_THROTTLE_MS_ENV).unwrap_or_else(|_| "12".into()); let trimmed = raw.trim(); if trimmed.is_empty() { return Some(Duration::from_millis(12)); @@ -109,7 +110,7 @@ pub(in crate::backend::wayland) fn toolbar_drag_throttle_interval() -> Option Duration { static VALUE: OnceLock = OnceLock::new(); *VALUE.get_or_init(|| { - let raw = std::env::var(TOOLBAR_DRAG_HANDOFF_MS_ENV).unwrap_or_else(|_| "250".into()); + let raw = env::var(TOOLBAR_DRAG_HANDOFF_MS_ENV).unwrap_or_else(|_| "250".into()); let Ok(ms) = raw.trim().parse::() else { return Duration::from_millis(250); }; @@ -128,9 +129,8 @@ pub(in crate::backend::wayland) fn drag_log>(message: impl FnOnce( fn force_inline_env_enabled() -> bool { static ENABLED: OnceLock = OnceLock::new(); - *ENABLED.get_or_init(|| { - parse_boolish_env(&std::env::var(FORCE_INLINE_TOOLBARS_ENV).unwrap_or_default()) - }) + *ENABLED + .get_or_init(|| parse_boolish_env(&env::var(FORCE_INLINE_TOOLBARS_ENV).unwrap_or_default())) } pub(super) fn force_inline_toolbars_requested_with_env( diff --git a/src/backend/wayland/state/input_actions.rs b/src/backend/wayland/state/input_actions.rs index 9ae81e08c..dfcc1f0ac 100644 --- a/src/backend/wayland/state/input_actions.rs +++ b/src/backend/wayland/state/input_actions.rs @@ -1,11 +1,9 @@ +use crate::input::state::ZoomAnchor; + use super::WaylandState; -use crate::{ - config::Action, - input::{ - InputState, Key, - state::{InputEffect, InputEffectDrain, InputTextResources}, - }, -}; +use crate::config::Action; +use crate::input::state::{InputEffect, InputEffectDrain, InputTextResources}; +use crate::input::{InputState, Key}; /// The input HUD's live state, before and after an input update. /// @@ -55,7 +53,7 @@ impl WaylandState { pub(in crate::backend::wayland) fn dispatch_input_action_anchored( &mut self, action: Action, - anchor: crate::input::state::ZoomAnchor, + anchor: ZoomAnchor, ) { self.apply_input_update(|input_state, resources| { input_state.handle_action_anchored(resources, action, anchor) diff --git a/src/backend/wayland/state/input_hud.rs b/src/backend/wayland/state/input_hud.rs index 0b8938551..5fc0d08c3 100644 --- a/src/backend/wayland/state/input_hud.rs +++ b/src/backend/wayland/state/input_hud.rs @@ -7,6 +7,16 @@ //! place that reconciles the two, so the effective source is always a function //! of `{HUD enabled} x {configured mode} x {probe result}`. +#[cfg(feature = "input-monitor")] +use crate::backend::wayland::input_monitor::InputMonitor; +#[cfg(feature = "input-monitor")] +use crate::backend::wayland::input_monitor::SystemInputEvent; +#[cfg(feature = "input-monitor")] +use crate::backend::wayland::input_monitor::SystemInputFailure; +use crate::backend::wayland::{RuntimeWakeHandle, input_monitor}; + +use std::mem; + use super::WaylandState; use crate::config::InputHudMode; use crate::i18n::tr; @@ -48,16 +58,23 @@ enum MonitorStart { /// System-reader lifecycle and reconciliation latches for the input HUD. pub(in crate::backend::wayland) struct InputHudRuntime { #[cfg(feature = "input-monitor")] - monitor_wake: crate::backend::wayland::RuntimeWakeHandle, + monitor_wake: RuntimeWakeHandle, #[cfg(feature = "input-monitor")] - monitor: Option, + monitor: Option, system_warned: bool, announce_pending: bool, last_request: Option<(bool, InputHudMode)>, } impl InputHudRuntime { - pub(super) fn new(monitor_wake: crate::backend::wayland::RuntimeWakeHandle) -> Self { + #[cfg_attr( + not(feature = "input-monitor"), + expect( + clippy::needless_pass_by_value, + reason = "the runtime keeps the wake handle only when `input-monitor` is enabled" + ) + )] + pub(super) fn new(monitor_wake: RuntimeWakeHandle) -> Self { #[cfg(not(feature = "input-monitor"))] let _ = monitor_wake; Self { @@ -84,7 +101,7 @@ impl InputHudRuntime { } fn take_announce(&mut self) -> bool { - std::mem::take(&mut self.announce_pending) + mem::take(&mut self.announce_pending) } fn clear_announce(&mut self) { @@ -112,18 +129,14 @@ impl InputHudRuntime { MonitorStart::Pending }; } - match crate::backend::wayland::input_monitor::InputMonitor::start(self.monitor_wake.clone()) - { + match InputMonitor::start(self.monitor_wake.clone()) { Ok(monitor) => { self.monitor = Some(monitor); log::info!("Input HUD: waiting for system-wide capture to come up"); MonitorStart::Pending } Err(err) => MonitorStart::Failed( - crate::backend::wayland::input_monitor::SystemInputFailure::StartFailed( - err.to_string(), - ) - .user_message(), + SystemInputFailure::StartFailed(err.to_string()).user_message(), ), } } @@ -144,10 +157,10 @@ impl InputHudRuntime { } #[cfg(feature = "input-monitor")] - fn drain(&mut self) -> Vec { + fn drain(&mut self) -> Vec { self.monitor .as_mut() - .map(crate::backend::wayland::input_monitor::InputMonitor::drain) + .map(InputMonitor::drain) .unwrap_or_default() } @@ -225,7 +238,7 @@ impl WaylandState { let wanted = self.input_state.input_hud_enabled().then(|| { resolve_input_hud_source( self.input_state.input_hud_configured_mode(), - crate::backend::wayland::input_monitor::system_input_available(), + input_monitor::system_input_available(), ) }); @@ -444,10 +457,12 @@ impl WaylandState { #[cfg(test)] mod tests { + use crate::backend::wayland::RuntimeWakeSource; + use super::*; fn runtime() -> InputHudRuntime { - let wake = crate::backend::wayland::RuntimeWakeSource::new() + let wake = RuntimeWakeSource::try_new() .expect("runtime wake source") .handle(); InputHudRuntime::new(wake) diff --git a/src/backend/wayland/state/key_repeat.rs b/src/backend/wayland/state/key_repeat.rs index 62f8ad2d3..d90a158b3 100644 --- a/src/backend/wayland/state/key_repeat.rs +++ b/src/backend/wayland/state/key_repeat.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; use std::time::{Duration, Instant}; -use wayland_client::{Proxy, protocol::wl_keyboard}; +use wayland_client::Proxy; +use wayland_client::protocol::wl_keyboard; use crate::backend::wayland::handlers::keyboard::{KEY_REPEAT_INITIAL_DELAY, KEY_REPEAT_INTERVAL}; use crate::input::Key; diff --git a/src/backend/wayland/state/keybindings.rs b/src/backend/wayland/state/keybindings.rs index 470fab9a2..b2aa9b42a 100644 --- a/src/backend/wayland/state/keybindings.rs +++ b/src/backend/wayland/state/keybindings.rs @@ -51,19 +51,6 @@ //! that got neither edit, or over one that was never written because it already //! said this, sends the user looking for something that is not there. -use super::super::config_edits::{ - ConfigEdit, ConfigEditWorker, KeybindingEditWrite, ProjectedShortcut, -}; -use super::WaylandState; -use crate::config::{ - Action, ConfigEditNotReadBack, ConfigEditOutcome, ConfigEditWrite, KeybindingsConfig, Shortcut, - ShortcutClaimedOnDisk, action_label, -}; -use crate::input::state::{ - InputState, KeybindingEditOperation, KeybindingEditRequest, Toast, ToastPriority, -}; -use std::collections::HashMap; - mod implementation; pub(in crate::backend::wayland) use implementation::queue_keybinding_edit; diff --git a/src/backend/wayland/state/keybindings/implementation.rs b/src/backend/wayland/state/keybindings/implementation.rs index 342256a27..dc1407408 100644 --- a/src/backend/wayland/state/keybindings/implementation.rs +++ b/src/backend/wayland/state/keybindings/implementation.rs @@ -1,5 +1,16 @@ -use super::*; +use super::super::WaylandState; +use crate::backend::wayland::config_edits::{ + ConfigEdit, ConfigEditWorker, KeybindingEditWrite, ProjectedShortcut, +}; +use crate::config::{ + Action, ConfigEditNotReadBack, ConfigEditOutcome, ConfigEditWrite, KeybindingsConfig, Shortcut, + ShortcutClaimedOnDisk, action_label, +}; use crate::i18n::{tr, trf}; +use crate::input::state::{ + InputState, KeybindingEditOperation, KeybindingEditRequest, Toast, ToastPriority, +}; +use std::collections::HashMap; /// Why a shortcut edit was refused. Both arms leave the keymap untouched. #[derive(Debug)] diff --git a/src/backend/wayland/state/keybindings/implementation/tests.rs b/src/backend/wayland/state/keybindings/implementation/tests.rs index cb6b2cf7a..59dc29294 100644 --- a/src/backend/wayland/state/keybindings/implementation/tests.rs +++ b/src/backend/wayland/state/keybindings/implementation/tests.rs @@ -1,8 +1,7 @@ use super::*; -use crate::config::Config; use crate::config::io::{is_stale_source_error, persist_keybinding_edit_at}; -use crate::config::persist_keybinding_edit; use crate::config::test_helpers::with_temp_config_home; +use crate::config::{Config, persist_keybinding_edit}; use std::fs; use std::path::{Path, PathBuf}; diff --git a/src/backend/wayland/state/keybindings/implementation/tests/persistence.rs b/src/backend/wayland/state/keybindings/implementation/tests/persistence.rs index 8d2288443..63db8a1f7 100644 --- a/src/backend/wayland/state/keybindings/implementation/tests/persistence.rs +++ b/src/backend/wayland/state/keybindings/implementation/tests/persistence.rs @@ -1,3 +1,6 @@ +use crate::config::ConfigDocument; +use crate::{config, test_temp}; + use super::*; /// The persistence fixture: hand-authored text the write must respect. @@ -52,7 +55,7 @@ fn write_fixture(dir: &Path) -> PathBuf { /// otherwise byte-identical, comments and all. #[test] fn a_shortcut_write_changes_exactly_its_own_key() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); persist_keybinding_edit_at( @@ -76,7 +79,7 @@ fn a_shortcut_write_changes_exactly_its_own_key() { /// Unbind and reset write through the same one-key path. #[test] fn unbinding_and_resetting_also_change_exactly_their_own_key() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); persist_keybinding_edit_at(&path, Action::SelectPenTool, &[]) @@ -112,7 +115,7 @@ fn unbinding_and_resetting_also_change_exactly_their_own_key() { #[test] fn a_shortcut_write_leaves_a_timestamped_backup() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); let outcome = persist_keybinding_edit_at( @@ -134,7 +137,7 @@ fn a_shortcut_write_leaves_a_timestamped_backup() { /// an action the file omitted has to come back explicit once it is written. #[test] fn a_written_shortcut_reloads_as_explicitly_authored() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); assert!( !AUTHORED_FILE.contains("select_step_marker_tool"), @@ -148,8 +151,7 @@ fn a_written_shortcut_reloads_as_explicitly_authored() { ) .expect("the write should succeed"); - let reloaded = crate::config::ConfigDocument::load_from_path(&path) - .expect("the written config should reload"); + let reloaded = ConfigDocument::load_from_path(&path).expect("the written config should reload"); assert!( reloaded .keybinding_authorship() @@ -171,7 +173,7 @@ fn a_written_shortcut_reloads_as_explicitly_authored() { /// happened. Most actions are in this state: the file simply omits them. #[test] fn resetting_an_omitted_action_already_at_its_default_writes_nothing() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); assert!( !AUTHORED_FILE.contains("select_marker_tool"), @@ -211,7 +213,7 @@ fn resetting_an_omitted_action_already_at_its_default_writes_nothing() { /// build changing the shipped value. #[test] fn resetting_an_authored_shortcut_writes_the_default_and_reports_it() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); let default = KeybindingsConfig::default() .bindings_for_action(Action::SelectPenTool) @@ -243,7 +245,7 @@ fn resetting_an_authored_shortcut_writes_the_default_and_reports_it() { /// the caller reported a save failure over a file it had just changed. #[test] fn a_chord_claimed_on_disk_since_the_edit_is_refused_without_writing() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = config_in(temp.path()); fs::write(&path, CLAIMED_ON_DISK_FILE).expect("the fixture should be written"); assert!( @@ -294,7 +296,7 @@ fn a_chord_claimed_on_disk_since_the_edit_is_refused_without_writing() { /// worker does with two of them, not how they got past the palette. #[test] fn an_overlapping_edit_onto_the_chord_the_first_took_is_refused_by_the_file() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); let running = KeybindingsConfig::default(); let contested = "Ctrl+Alt+Shift+X"; @@ -341,7 +343,7 @@ fn an_overlapping_edit_onto_the_chord_the_first_took_is_refused_by_the_file() { /// must not pin it into the file, in either direction. #[test] fn an_edit_leaves_the_omitted_default_that_stood_down_alone() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = config_in(temp.path()); let authored = "[keybindings]\ntoggle_input_hud = [\"Ctrl+Z\"]\n"; fs::write(&path, authored).expect("the fixture should be written"); @@ -366,7 +368,7 @@ fn an_edit_leaves_the_omitted_default_that_stood_down_alone() { fn a_read_only_config_fails_the_write_and_leaves_the_file_alone() { use std::os::unix::fs::PermissionsExt; - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); // The file mode alone would not stop it: the write is an atomic // replace, so it is the directory that has to refuse the new entry @@ -385,6 +387,7 @@ fn a_read_only_config_fails_the_write_and_leaves_the_file_alone() { fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir"); fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file"); + // SAFETY: geteuid has no preconditions and cannot fail. if unsafe { libc::geteuid() } == 0 { // Root ignores these modes; the scenario cannot be staged. return; @@ -406,7 +409,7 @@ fn a_read_only_config_fails_the_write_and_leaves_the_file_alone() { /// with the damage on screen — never as a side effect of a rebind. #[test] fn an_unparseable_config_is_refused_rather_than_rebuilt_from_defaults() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = config_in(temp.path()); fs::write(&path, "[keybindings\nundo = broken").expect("the fixture should be written"); @@ -431,10 +434,10 @@ fn an_unparseable_config_is_refused_rather_than_rebuilt_from_defaults() { /// so this stages that save rather than trusting the wording. #[test] fn a_stale_document_is_recognised_and_retried() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); - let document = crate::config::ConfigDocument::load_from_path(&path).expect("load"); + let document = ConfigDocument::load_from_path(&path).expect("load"); fs::write(&path, format!("{AUTHORED_FILE}\n# a second writer\n")).expect("outside write"); let error = document .save_with_backup(document.config().clone()) @@ -464,13 +467,13 @@ fn a_stale_document_is_recognised_and_retried() { #[test] fn a_fresh_load_returns_the_written_shortcut() { with_temp_config_home(|config_root| { - let config_dir = config_root.join(crate::config::PRIMARY_CONFIG_DIR); + let config_dir = config_root.join(config::PRIMARY_CONFIG_DIR); fs::create_dir_all(&config_dir).expect("test config directory"); let path = config_dir.join("config.toml"); fs::write(&path, AUTHORED_SHORTCUTS).expect("test config should be written"); // Through the environment-resolved entry point the overlay calls, - // so the wiring from `Config::get_config_path` is covered too. + // so the wiring from `Config::config_path` is covered too. persist_keybinding_edit(Action::SelectPenTool, &["Ctrl+Alt+Shift+K".to_string()]) .expect("the write should succeed"); diff --git a/src/backend/wayland/state/ocr.rs b/src/backend/wayland/state/ocr.rs index 7775caf43..ce98a6f90 100644 --- a/src/backend/wayland/state/ocr.rs +++ b/src/backend/wayland/state/ocr.rs @@ -5,13 +5,20 @@ //! OCR releases, and a user-owned freeze or zoom survives untouched. Nothing //! here mutates annotations, history, boards, sessions, or the active tool. +use std::time::{Duration, Instant}; + use crate::backend::wayland::acquisition::{ScreenAcquisitionOutcome, ScreenAcquisitionOwner}; +use crate::backend::wayland::state::screen_image; use crate::backend::wayland::zoom::ZoomWaiterOwner; -use crate::input::state::OcrScanOutcome; +use crate::config::Action; use crate::input::state::{ - RegionInputSource, RegionPurposeTag, ScreenCaptureSource, Toast, ToastPriority, + OcrScanOutcome, RegionInputSource, RegionPurposeTag, ScreenCaptureSource, Toast, ToastPriority, +}; +use crate::ocr::{ + OcrFailure, OcrLanguages, OcrPoll, OcrRequest, OcrRequestId, OcrSubmitError, OcrSuccess, }; -use crate::ocr::{OcrFailure, OcrLanguages, OcrPoll, OcrRequest, OcrSubmitError, OcrSuccess}; +use crate::screen_pixels::{ImagePixelRect, PackedArgb32}; +use crate::ui; use super::WaylandState; use super::acquisition::report_screen_source_activation_rejected_to; @@ -19,8 +26,8 @@ use super::region_capture::{ ActiveScreenRegion, FreezeOwnership, RegionOwnerLoss, RegionSelectionFinalize, }; use super::screen_image::{ - CropError, DisplayedScreenImage, ScreenSourceEntry, copy_image_rect, displayed_screen_image, - screen_source_entry, + CropError, DisplayedScreenImage, ScreenSourceConditions, ScreenSourceEntry, copy_image_rect, + displayed_screen_image, screen_source_entry, }; use crate::i18n::tr; @@ -95,13 +102,13 @@ impl WaylandState { self.retire_stylus_contact(); let generation = self.region_capture.next_generation(); - match screen_source_entry( - self.ocr_screen_source().is_some(), - self.input_state.board_is_transparent(), - self.zoom.is_engaged(), - self.zoom.active, - self.frozen.enabled(), - ) { + match screen_source_entry(ScreenSourceConditions { + has_source: self.ocr_screen_source().is_some(), + board_is_transparent: self.input_state.board_is_transparent(), + zoom_engaged: self.zoom.is_engaged(), + zoom_active: self.zoom.active, + frozen_enabled: self.frozen.enabled(), + }) { ScreenSourceEntry::Activate => { if !self.activate_ocr_selector(generation, FreezeOwnership::PreExisting) { self.clear_screen_region_ui_only(); @@ -136,10 +143,8 @@ impl WaylandState { self.input_state.push_toast( ToastPriority::Action, TOAST_SOURCE, - Toast::info(tr("OCR needs a visible screen image.")).action( - "Switch to transparent", - crate::config::Action::ReturnToTransparent, - ), + Toast::info(tr("OCR needs a visible screen image.")) + .action("Switch to transparent", Action::ReturnToTransparent), ); } ScreenSourceEntry::CaptureUnavailable => { @@ -287,7 +292,10 @@ impl WaylandState { RegionOwnerLoss::Cancel(RegionPurposeTag::Ocr) => self.cancel_ocr(), RegionOwnerLoss::Cancel(RegionPurposeTag::Measure) => self.cancel_measure_mode(), RegionOwnerLoss::Cancel(purpose) => { - debug_assert!(purpose.is_capture()); + debug_assert!( + purpose.is_capture(), + "OCR and measure regions are cancelled above, got {purpose:?}" + ); self.cancel_region_capture() } } @@ -347,10 +355,7 @@ impl WaylandState { } /// Recognize an already-chosen rectangle — the whole image, from `Ctrl+A`. - pub(in crate::backend::wayland) fn submit_whole_image_ocr( - &mut self, - rect: crate::screen_pixels::ImagePixelRect, - ) { + pub(in crate::backend::wayland) fn submit_whole_image_ocr(&mut self, rect: ImagePixelRect) { self.submit_ocr_for_rect(rect); } @@ -358,15 +363,13 @@ impl WaylandState { /// area the sweep will cover, crop while the source is still held, release /// it, submit, and start the overlay. Both entry points share it so a fix /// to one cannot leave the other behind. - fn submit_ocr_for_rect(&mut self, rect: crate::screen_pixels::ImagePixelRect) { + fn submit_ocr_for_rect(&mut self, rect: ImagePixelRect) { // Mapped before the region is released: the token is what turns the // authoritative image rectangle into the surface pixels the sweep is // painted over. let scan_region = match self.region_capture.active() { Some(ActiveScreenRegion::Ready { source, .. }) => { - crate::backend::wayland::state::screen_image::screen_rect_for_image_rect( - &source, rect, - ) + screen_image::screen_rect_for_image_rect(&source, rect) } _ => None, }; @@ -390,15 +393,11 @@ impl WaylandState { return; }; if let Some(region) = scan_region { - self.input_state - .begin_ocr_scan(id, region, std::time::Instant::now()); + self.input_state.begin_ocr_scan(id, region, Instant::now()); } } - fn crop_ocr_selection( - &self, - rect: crate::screen_pixels::ImagePixelRect, - ) -> Result { + fn crop_ocr_selection(&self, rect: ImagePixelRect) -> Result { let Some(source) = self.ocr_screen_source() else { return Err(tr( "The screen image for text recognition is no longer available.", @@ -412,10 +411,7 @@ impl WaylandState { /// Hand the crop to the worker, reporting the accepted request. `None` /// means nothing is running and no completion will arrive. - fn submit_ocr_request( - &mut self, - pixels: crate::screen_pixels::PackedArgb32, - ) -> Option { + fn submit_ocr_request(&mut self, pixels: PackedArgb32) -> Option { let languages = OcrLanguages::from_validated(self.config.capture.resolved_ocr_languages()); log::debug!( "OCR submitting {}x{} crop for languages {}", @@ -427,10 +423,8 @@ impl WaylandState { Ok(id) => { // wl-copy needs the overlay to stay alive long enough to serve // the selection it publishes. - self.focus.suppress_exit_for( - std::time::Instant::now(), - std::time::Duration::from_millis(1500), - ); + self.focus + .suppress_exit_for(Instant::now(), Duration::from_millis(1500)); log::debug!("OCR request {id} started"); Some(id) } @@ -474,7 +468,7 @@ impl WaylandState { character_count, replaced_invalid_utf8, }, - std::time::Instant::now(), + Instant::now(), ); let message = if replaced_invalid_utf8 { tr("Text copied (some characters were unreadable)") @@ -491,11 +485,8 @@ impl WaylandState { id, outcome: Ok(OcrSuccess::NoTextFound), } => { - self.input_state.settle_ocr_scan( - id, - OcrScanOutcome::NoTextFound, - std::time::Instant::now(), - ); + self.input_state + .settle_ocr_scan(id, OcrScanOutcome::NoTextFound, Instant::now()); self.input_state.push_toast( ToastPriority::Info, TOAST_SOURCE, @@ -507,11 +498,8 @@ impl WaylandState { outcome: Err(failure), } => { log::warn!("OCR request {id} failed: {failure:?}"); - self.input_state.settle_ocr_scan( - id, - OcrScanOutcome::Failed, - std::time::Instant::now(), - ); + self.input_state + .settle_ocr_scan(id, OcrScanOutcome::Failed, Instant::now()); let toast = match failure { OcrFailure::EngineMissing | OcrFailure::LanguageMissing { .. } => { Toast::warning(failure.message()) @@ -523,11 +511,8 @@ impl WaylandState { } OcrPoll::WorkerLost { id, reason } => { log::error!("OCR request {id} lost its worker: {reason}"); - self.input_state.settle_ocr_scan( - id, - OcrScanOutcome::Failed, - std::time::Instant::now(), - ); + self.input_state + .settle_ocr_scan(id, OcrScanOutcome::Failed, Instant::now()); self.input_state.push_toast( ToastPriority::Critical, TOAST_SOURCE, @@ -581,11 +566,11 @@ impl WaylandState { // reuses the same strip, honours the same `show_legend` setting, and // dismisses on the first drag through the shared selector state. if self.config.capture.region.show_legend && !self.region_picker_legend_dismissed() { - crate::ui::render_region_legend( + ui::render_region_legend( self.render.ui_text(), ctx, (screen_width, screen_height), - crate::ui::OCR_LEGEND_TEXT, + ui::OCR_LEGEND_TEXT, ); } let _ = ctx.restore(); diff --git a/src/backend/wayland/state/onboarding.rs b/src/backend/wayland/state/onboarding.rs index 605a97bd1..ed7b3e683 100644 --- a/src/backend/wayland/state/onboarding.rs +++ b/src/backend/wayland/state/onboarding.rs @@ -1,11 +1,15 @@ +use crate::config; +use crate::config::TopDisplayMode; use crate::config::keybindings::Action; use crate::domain::OnboardingTip; use crate::i18n::{tr, trf}; +use crate::input::InputState; use crate::input::state::{Toast, ToastCommand, ToastPriority}; -use crate::onboarding::DEFERRED_HINT_REPEAT_MAX; +use crate::onboarding::{DEFERRED_HINT_REPEAT_MAX, OnboardingSaveError}; use std::time::{Duration, Instant}; -use super::*; +use super::WaylandState; +use anyhow::Result; mod card; mod first_run; @@ -32,11 +36,11 @@ enum ContextualHint { struct TipAcknowledgementOutcome { follow_up: Option, - persistence_error: Option, + persistence_error: Option, } fn acknowledge_tip_command( - result: Result<(), crate::onboarding::OnboardingSaveError>, + result: Result<(), OnboardingSaveError>, follow_up: Option, ) -> TipAcknowledgementOutcome { TipAcknowledgementOutcome { @@ -48,7 +52,7 @@ fn acknowledge_tip_command( /// The visible status-HUD entry that can open the board picker. The hint must /// not advertise the pill when both configurable Board/Page segments are /// absent: the remaining color/tool/help segments perform different actions. -fn status_bar_board_picker_entry(input: &crate::input::InputState) -> Option<&'static str> { +fn status_bar_board_picker_entry(input: &InputState) -> Option<&'static str> { if !input.ui_visibility.show_status_bar || !input.ui_visibility.status_bar_interactive || input.boards.board_count() <= 1 @@ -67,10 +71,10 @@ fn status_bar_board_picker_entry(input: &crate::input::InputState) -> Option<&'s } /// Whether the top-strip overflow that owns the Canvas popover is reachable. -fn canvas_popover_hint_relevant(input: &crate::input::InputState) -> bool { +fn canvas_popover_hint_relevant(input: &InputState) -> bool { input.toolbar_top_visible() && !input.toolbar_top_minimized() - && input.toolbar_top_display_mode() == crate::config::TopDisplayMode::Full + && input.toolbar_top_display_mode() == TopDisplayMode::Full } fn automatic_tip_toast(message: impl Into, tip: OnboardingTip) -> Toast { @@ -271,7 +275,7 @@ impl WaylandState { let message = format!( "Tip: press {shortcut} for {}.", - crate::config::action_label(action) + config::action_label(action) ); let outcome = self.input_state.push_toast( ToastPriority::Hint, @@ -521,10 +525,7 @@ impl WaylandState { } } - fn show_onboarding_persistence_warning( - &mut self, - error: &crate::onboarding::OnboardingSaveError, - ) { + fn show_onboarding_persistence_warning(&mut self, error: &OnboardingSaveError) { self.input_state.push_toast( ToastPriority::Critical, "onboarding.persistence", diff --git a/src/backend/wayland/state/onboarding/card.rs b/src/backend/wayland/state/onboarding/card.rs index 616bcb80a..b17d3d8a6 100644 --- a/src/backend/wayland/state/onboarding/card.rs +++ b/src/backend/wayland/state/onboarding/card.rs @@ -7,6 +7,7 @@ //! button when the release lands on the same button. use crate::backend::wayland::state::WaylandState; +use crate::draw::DirtyFullReason; use crate::ui::{OnboardingCardAction, OnboardingCardLayout, OnboardingCardPress}; /// The rectangles painted this frame, the hovered button, and the stylus @@ -135,7 +136,7 @@ impl WaylandState { // Hover only recolors a button, and the card repaints as a whole. self.input_state .dirty_tracker - .mark_full_for(crate::draw::DirtyFullReason::FirstRunOnboarding); + .mark_full_for(DirtyFullReason::FirstRunOnboarding); self.input_state.needs_redraw = true; } diff --git a/src/backend/wayland/state/onboarding/first_run.rs b/src/backend/wayland/state/onboarding/first_run.rs index 558ac25c5..2fef25b75 100644 --- a/src/backend/wayland/state/onboarding/first_run.rs +++ b/src/backend/wayland/state/onboarding/first_run.rs @@ -1,9 +1,14 @@ +use std::fmt::Display; +use std::mem; + use crate::backend::wayland::state::WaylandState; -use crate::config::{RadialMenuMouseBinding, keybindings::Action}; +use crate::config::RadialMenuMouseBinding; +use crate::config::keybindings::Action; +use crate::daemon::setup; use crate::draw::DirtyFullReason; use crate::i18n::{tr, trf}; -use crate::input::state::{Toast, ToastPriority}; -use crate::input::{DrawingState, Key, state::PendingOnboardingUsage}; +use crate::input::state::{PendingOnboardingUsage, Toast, ToastPriority}; +use crate::input::{DrawingState, Key}; use crate::onboarding::{FirstRunStep, OnboardingState}; use crate::ui::OnboardingCardAction; @@ -58,7 +63,7 @@ impl WaylandState { } if enable { - match crate::daemon::setup::setup_background_mode() { + match setup::setup_background_mode() { Ok(summary) => { mark_background_mode_prompt( self.preferences.onboarding_mut().state_mut(), @@ -207,18 +212,18 @@ impl WaylandState { { return false; } - !first_run_card_hidden_by_ui_state( - self.input_state.presenter_mode_active(), - self.input_state.command_palette.is_open(), - self.input_state.help_overlay.is_visible(), - self.input_state.is_radial_menu_open(), - self.input_state.is_context_menu_open(), - self.zoom.is_engaged(), - ) + !first_run_card_hidden_by_ui_state(FirstRunCardUiState { + presenter_mode: self.input_state.presenter_mode_active(), + command_palette_open: self.input_state.command_palette.is_open(), + show_help: self.input_state.help_overlay.is_visible(), + radial_menu_open: self.input_state.is_radial_menu_open(), + context_menu_open: self.input_state.is_context_menu_open(), + zoom_engaged: self.zoom.is_engaged(), + }) } pub(super) fn apply_first_run_progress(&mut self) { - let usage = std::mem::take(&mut self.input_state.pending_onboarding_usage); + let usage = mem::take(&mut self.input_state.pending_onboarding_usage); let environment = FirstRunEnvironment { context_enabled: self.input_state.context_menu_enabled(), radial_binding: self.input_state.radial_menu.mouse_binding(), @@ -272,7 +277,7 @@ impl WaylandState { } } -fn background_mode_setup_failed_message(err: &dyn std::fmt::Display) -> String { +fn background_mode_setup_failed_message(err: &dyn Display) -> String { trf( "Background mode setup failed: {}. You can set this up later in Background Mode settings.", &[err], @@ -355,14 +360,7 @@ pub(super) fn advance_first_run_steps( FirstRunStep::QuickAccess } FirstRunStep::QuickAccess => { - if !quick_access_completed( - state, - environment.context_enabled, - environment.radial_binding, - environment.radial_available, - environment.context_keyboard_available, - environment.toolbar_visible, - ) { + if !quick_access_completed(state, environment) { break; } state.quick_access_requires_toolbar = false; @@ -440,26 +438,22 @@ pub(super) fn color_thickness_completed(state: &OnboardingState) -> bool { pub(super) fn quick_access_completed( state: &OnboardingState, - context_enabled: bool, - radial_binding: RadialMenuMouseBinding, - radial_available: bool, - context_keyboard_available: bool, - toolbar_visible: bool, + environment: FirstRunEnvironment, ) -> bool { let mut done = true; - if radial_available { + if environment.radial_available { done &= state.used_radial_menu; } done &= quick_access_context_done( state, - context_enabled, - radial_binding, - radial_available, - context_keyboard_available, + environment.context_enabled, + environment.radial_binding, + environment.radial_available, + environment.context_keyboard_available, ); if state.quick_access_requires_toolbar { - done &= toolbar_visible || state.used_toolbar_toggle; + done &= environment.toolbar_visible || state.used_toolbar_toggle; } done @@ -572,18 +566,22 @@ pub(super) fn first_run_holds_toolbar(state: &OnboardingState) -> bool { state.first_run_active() || state.sessions_seen <= 1 } -pub(super) fn first_run_card_hidden_by_ui_state( - presenter_mode: bool, - command_palette_open: bool, - show_help: bool, - radial_menu_open: bool, - context_menu_open: bool, - zoom_engaged: bool, -) -> bool { - presenter_mode - || command_palette_open - || show_help - || radial_menu_open - || context_menu_open - || zoom_engaged +/// Overlay UI that takes the screen from the first-run card while it shows. +#[derive(Debug, Clone, Copy, Default)] +pub(super) struct FirstRunCardUiState { + pub(super) presenter_mode: bool, + pub(super) command_palette_open: bool, + pub(super) show_help: bool, + pub(super) radial_menu_open: bool, + pub(super) context_menu_open: bool, + pub(super) zoom_engaged: bool, +} + +pub(super) fn first_run_card_hidden_by_ui_state(ui: FirstRunCardUiState) -> bool { + ui.presenter_mode + || ui.command_palette_open + || ui.show_help + || ui.radial_menu_open + || ui.context_menu_open + || ui.zoom_engaged } diff --git a/src/backend/wayland/state/onboarding/first_run_card.rs b/src/backend/wayland/state/onboarding/first_run_card.rs index f35922b7f..e91452c0f 100644 --- a/src/backend/wayland/state/onboarding/first_run_card.rs +++ b/src/backend/wayland/state/onboarding/first_run_card.rs @@ -6,7 +6,8 @@ //! written beside it. use crate::backend::wayland::state::WaylandState; -use crate::config::{RadialMenuMouseBinding, keybindings::Action}; +use crate::config::RadialMenuMouseBinding; +use crate::config::keybindings::Action; use crate::i18n::{tr, trf}; use crate::onboarding::{FirstRunStep, OnboardingState}; use crate::ui::{ diff --git a/src/backend/wayland/state/onboarding/tests.rs b/src/backend/wayland/state/onboarding/tests.rs index 5570db12c..1cc581462 100644 --- a/src/backend/wayland/state/onboarding/tests.rs +++ b/src/backend/wayland/state/onboarding/tests.rs @@ -1,8 +1,12 @@ +use crate::config::TopDisplayMode; +use crate::input::state::test_support; +use crate::onboarding::OnboardingSaveError; + use super::first_run::{ - FirstRunAdvance, FirstRunEnvironment, ReplayStart, advance_first_run_steps, - apply_persisted_usage_signals, background_mode_prompt_active, color_thickness_completed, - first_run_card_hidden_by_ui_state, first_run_card_key_action, first_run_holds_toolbar, - first_run_skip_allowed, quick_access_completed, replay_start, + FirstRunAdvance, FirstRunCardUiState, FirstRunEnvironment, ReplayStart, + advance_first_run_steps, apply_persisted_usage_signals, background_mode_prompt_active, + color_thickness_completed, first_run_card_hidden_by_ui_state, first_run_card_key_action, + first_run_holds_toolbar, first_run_skip_allowed, quick_access_completed, replay_start, }; use super::first_run_card::{first_run_step_eyebrow, toolbar_exit_body}; use super::{ @@ -11,8 +15,8 @@ use super::{ }; use crate::config::RadialMenuMouseBinding; use crate::domain::{Action, OnboardingTip}; -use crate::input::state::{CompositorCapabilities, ToastCommand}; -use crate::input::{Key, state::PendingOnboardingUsage}; +use crate::input::Key; +use crate::input::state::{CompositorCapabilities, PendingOnboardingUsage, ToastCommand}; use crate::onboarding::{DEFERRED_HINT_REPEAT_MAX, FirstRunStep, OnboardingState}; use crate::ui::OnboardingCardAction; use std::time::{Duration, Instant}; @@ -28,18 +32,36 @@ fn first_run_skip_requires_active_onboarding_and_visible_card() { #[test] fn first_run_card_hides_for_each_modal_state() { let modal_cases = [ - (true, false, false, false, false, false), // presenter - (false, true, false, false, false, false), // palette - (false, false, true, false, false, false), // help - (false, false, false, true, false, false), // radial - (false, false, false, false, true, false), // context menu - (false, false, false, false, false, true), // zoom + FirstRunCardUiState { + presenter_mode: true, + ..FirstRunCardUiState::default() + }, + FirstRunCardUiState { + command_palette_open: true, + ..FirstRunCardUiState::default() + }, + FirstRunCardUiState { + show_help: true, + ..FirstRunCardUiState::default() + }, + FirstRunCardUiState { + radial_menu_open: true, + ..FirstRunCardUiState::default() + }, + FirstRunCardUiState { + context_menu_open: true, + ..FirstRunCardUiState::default() + }, + FirstRunCardUiState { + zoom_engaged: true, + ..FirstRunCardUiState::default() + }, ]; for case in modal_cases { assert!( - first_run_card_hidden_by_ui_state(case.0, case.1, case.2, case.3, case.4, case.5), - "expected modal case to hide onboarding card" + first_run_card_hidden_by_ui_state(case), + "expected modal case to hide onboarding card: {case:?}" ); } } @@ -47,7 +69,7 @@ fn first_run_card_hides_for_each_modal_state() { #[test] fn first_run_card_remains_visible_without_modal_states() { assert!(!first_run_card_hidden_by_ui_state( - false, false, false, false, false, false + FirstRunCardUiState::default() )); } @@ -86,7 +108,7 @@ fn automatic_tip_controls_acknowledge_the_exact_tip_before_optional_settings() { #[test] fn tip_settings_navigation_survives_an_acknowledgement_write_failure() { let outcome = acknowledge_tip_command( - Err(crate::onboarding::OnboardingSaveError::Unavailable), + Err(OnboardingSaveError::Unavailable), Some(Action::OpenConfiguratorOnboardingHints), ); @@ -224,12 +246,9 @@ fn shortcut_coach_suppressed_once_learned_or_capped() { #[test] fn canvas_hint_requires_reachable_full_top_strip() { - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); input.test_set_toolbar_visibility_state(true, true, input.toolbar_top_pinned()); - input.test_set_toolbar_display_state( - crate::config::TopDisplayMode::Full, - input.toolbar_top_minimized(), - ); + input.test_set_toolbar_display_state(TopDisplayMode::Full, input.toolbar_top_minimized()); input.test_set_toolbar_display_state(input.toolbar_top_display_mode(), false); assert!(canvas_popover_hint_relevant(&input)); @@ -239,16 +258,13 @@ fn canvas_hint_requires_reachable_full_top_strip() { "the minimized restore tab has no Canvas overflow entry" ); input.test_set_toolbar_display_state(input.toolbar_top_display_mode(), false); - input.test_set_toolbar_display_state( - crate::config::TopDisplayMode::Micro, - input.toolbar_top_minimized(), - ); + input.test_set_toolbar_display_state(TopDisplayMode::Micro, input.toolbar_top_minimized()); assert!(!canvas_popover_hint_relevant(&input)); } #[test] fn status_bar_hint_requires_a_visible_board_picker_segment() { - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); assert!(input.boards.create_board(), "test needs multiple boards"); input.ui_visibility.show_status_bar = true; input.ui_visibility.status_bar_interactive = true; @@ -351,11 +367,13 @@ fn quick_access_completes_when_radial_unavailable_and_context_disabled() { let state = OnboardingState::default(); assert!(quick_access_completed( &state, - false, - RadialMenuMouseBinding::Middle, - false, - false, - true, + FirstRunEnvironment { + context_enabled: false, + radial_binding: RadialMenuMouseBinding::Middle, + radial_available: false, + context_keyboard_available: false, + toolbar_visible: true, + }, )); } @@ -367,11 +385,13 @@ fn quick_access_waives_context_when_radial_uses_right_click_without_context_shor }; assert!(quick_access_completed( &state, - true, - RadialMenuMouseBinding::Right, - true, - false, - true, + FirstRunEnvironment { + context_enabled: true, + radial_binding: RadialMenuMouseBinding::Right, + radial_available: true, + context_keyboard_available: false, + toolbar_visible: true, + }, )); } @@ -381,23 +401,16 @@ fn quick_access_blocks_when_toolbar_required_and_still_hidden() { quick_access_requires_toolbar: true, ..OnboardingState::default() }; - assert!(!quick_access_completed( - &state, - false, - RadialMenuMouseBinding::Middle, - false, - false, - false, - )); + let hidden_toolbar = FirstRunEnvironment { + context_enabled: false, + radial_binding: RadialMenuMouseBinding::Middle, + radial_available: false, + context_keyboard_available: false, + toolbar_visible: false, + }; + assert!(!quick_access_completed(&state, hidden_toolbar)); state.used_toolbar_toggle = true; - assert!(quick_access_completed( - &state, - false, - RadialMenuMouseBinding::Middle, - false, - false, - false, - )); + assert!(quick_access_completed(&state, hidden_toolbar)); } fn limited_caps() -> CompositorCapabilities { @@ -412,7 +425,7 @@ fn limited_caps() -> CompositorCapabilities { #[test] fn capability_warning_shows_once_per_session() { - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); let caps = limited_caps(); assert!( @@ -424,7 +437,7 @@ fn capability_warning_shows_once_per_session() { #[test] fn capability_warning_returns_when_capabilities_change() { - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); let caps = limited_caps(); let first = input.note_capability_toast(caps); @@ -437,7 +450,7 @@ fn capability_warning_returns_when_capabilities_change() { #[test] fn capability_warning_skipped_when_everything_available() { - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); let caps = CompositorCapabilities { layer_shell: true, screencopy: true, diff --git a/src/backend/wayland/state/pdf_export.rs b/src/backend/wayland/state/pdf_export.rs index 5eae65327..9fe866f45 100644 --- a/src/backend/wayland/state/pdf_export.rs +++ b/src/backend/wayland/state/pdf_export.rs @@ -3,7 +3,9 @@ use crate::canvas_export::{ CanvasPageExportSnapshot, PdfPageExportSnapshot, PdfPageMetadata, SpotlightPassSnapshot, resolve_pdf_page_layout, }; -use crate::config::{Action, PdfFitMode}; +use crate::capture::CaptureError; +use crate::config::{Action, PdfExportConfig, PdfFitMode}; +use crate::domain::BoardGrid; use crate::draw::{Frame, TextMeasurer}; use crate::input::BoardBackground; use crate::input::boards::BoardState; @@ -14,7 +16,7 @@ impl WaylandState { pub(in crate::backend::wayland) fn board_pdf_export_snapshot( &self, action: Action, - ) -> Result { + ) -> Result { self.board_pdf_export_snapshot_inner(action, None) } @@ -22,7 +24,7 @@ impl WaylandState { &self, action: Action, desktop_backdrop: CanvasExportBackdropSnapshot, - ) -> Result { + ) -> Result { self.board_pdf_export_snapshot_inner(action, Some(desktop_backdrop)) } @@ -30,7 +32,7 @@ impl WaylandState { &self, action: Action, desktop_backdrop: Option, - ) -> Result { + ) -> Result { let scope = pdf_export_scope_for_action(action); build_board_pdf_export_snapshot(BoardPdfExportBuildContext { measurer: self.render.text_measurer(), @@ -68,15 +70,15 @@ enum PdfExportScope { AllBoards, } -struct BoardPdfExportBuildContext<'a> { - measurer: &'a TextMeasurer, +struct BoardPdfExportBuildContext<'state> { + measurer: &'state TextMeasurer, logical_width: u32, logical_height: u32, - boards: &'a [BoardState], + boards: &'state [BoardState], active_board_index: usize, pan_enabled: bool, scope: PdfExportScope, - config: &'a crate::config::PdfExportConfig, + config: &'state PdfExportConfig, desktop_backdrop: Option, text_halo_enabled: bool, spotlight: SpotlightPassSnapshot, @@ -84,7 +86,7 @@ struct BoardPdfExportBuildContext<'a> { fn build_board_pdf_export_snapshot( context: BoardPdfExportBuildContext<'_>, -) -> Result { +) -> Result { let BoardPdfExportBuildContext { measurer, logical_width, @@ -243,7 +245,7 @@ fn pdf_export_scope_has_transparent_pages( fn backdrop_from_background( background: &BoardBackground, - grid: crate::domain::BoardGrid, + grid: BoardGrid, desktop_backdrop: Option<&CanvasExportBackdropSnapshot>, ) -> CanvasExportBackdropSnapshot { match background { diff --git a/src/backend/wayland/state/pdf_export/tests.rs b/src/backend/wayland/state/pdf_export/tests.rs index cdafa84e5..8613a41e2 100644 --- a/src/backend/wayland/state/pdf_export/tests.rs +++ b/src/backend/wayland/state/pdf_export/tests.rs @@ -1,3 +1,5 @@ +use crate::draw::BoardPages; + use super::*; use crate::config::{PdfExportConfig, PdfFitMode}; use crate::draw::{RED, Shape, WHITE}; @@ -15,7 +17,7 @@ fn board(id: &str, name: &str, background: BoardBackground, pages: Vec) - persist: true, pinned: false, }); - board.pages = crate::draw::BoardPages::from_pages(pages, 0); + board.pages = BoardPages::from_pages(pages, 0); board } diff --git a/src/backend/wayland/state/perf.rs b/src/backend/wayland/state/perf.rs index 195de498c..f62502812 100644 --- a/src/backend/wayland/state/perf.rs +++ b/src/backend/wayland/state/perf.rs @@ -1,15 +1,11 @@ -use std::{ - collections::{BTreeMap, VecDeque}, - fmt, - time::{Duration, Instant}, -}; +use std::collections::{BTreeMap, VecDeque}; +use std::time::{Duration, Instant}; +use std::{env, fmt}; use log::info; -use crate::{ - env_vars::PERF_LOG_ENV, - input::{DrawingState, Tool}, -}; +use crate::env_vars::PERF_LOG_ENV; +use crate::input::{DrawingState, Tool}; use super::{FullDamageReason, WaylandState}; @@ -24,19 +20,14 @@ pub(in crate::backend::wayland) use damage_diagnostics::{ PerfDamageDiagnostics, PerfFrameDamageContext, damage_covers_logical_surface, }; use damage_diagnostics::{ - damage_area_pct, damage_covers_surface, effective_full_damage_reason, - format_effective_full_damage_reason, format_pct_hundredths, full_damage_source, - largest_region_area_pct_hundredths, + damage_area_pct, damage_covers_surface, largest_region_area_pct_hundredths, }; #[cfg(test)] use render_breakdown::PerfRenderStageDurations; pub(in crate::backend::wayland) use render_breakdown::{ PerfRenderBreakdown, PerfRenderProfileKind, }; -use render_breakdown::{ - PerfRenderBreakdownAccumulator, PerfRenderBreakdownSummary, log_render_stage_frame, - log_render_stage_summary, -}; +use render_breakdown::{PerfRenderBreakdownAccumulator, PerfRenderBreakdownSummary}; const MAX_PENDING_INPUT_SAMPLES: usize = 4096; const MAX_RECENT_LATENCIES: usize = 2048; @@ -323,7 +314,7 @@ impl WaylandState { pub(in crate::backend::wayland) fn commit_perf_frame( &mut self, - damage: PerfFrameDamageContext<'_>, + damage: &PerfFrameDamageContext<'_>, commit_at: Instant, ) { if !self.perf.enabled() { @@ -415,7 +406,7 @@ fn log_frame_pacing_summary(summary: &PerfFramePacingSummary, final_summary: boo } fn perf_log_enabled_from_env() -> bool { - std::env::var(PERF_LOG_ENV) + env::var(PERF_LOG_ENV) .map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "on" | "ON")) .unwrap_or(false) } diff --git a/src/backend/wayland/state/perf_modules/damage_diagnostics.rs b/src/backend/wayland/state/perf_modules/damage_diagnostics.rs index 06c490f2e..c7ac3b7f3 100644 --- a/src/backend/wayland/state/perf_modules/damage_diagnostics.rs +++ b/src/backend/wayland/state/perf_modules/damage_diagnostics.rs @@ -14,8 +14,8 @@ pub(in crate::backend::wayland) struct PerfDamageDiagnostics { pub(in crate::backend::wayland) largest_region_area_pct_hundredths: u32, } -pub(in crate::backend::wayland) struct PerfFrameDamageContext<'a> { - pub(in crate::backend::wayland) damage_screen: &'a [Rect], +pub(in crate::backend::wayland) struct PerfFrameDamageContext<'damage> { + pub(in crate::backend::wayland) damage_screen: &'damage [Rect], pub(in crate::backend::wayland) logical_width: u32, pub(in crate::backend::wayland) logical_height: u32, pub(in crate::backend::wayland) damage_rects: usize, diff --git a/src/backend/wayland/state/perf_modules/metrics.rs b/src/backend/wayland/state/perf_modules/metrics.rs index 76854232e..ed66bde54 100644 --- a/src/backend/wayland/state/perf_modules/metrics.rs +++ b/src/backend/wayland/state/perf_modules/metrics.rs @@ -1,4 +1,27 @@ -use super::*; +use super::super::FullDamageReason; +use super::damage_diagnostics::{ + effective_full_damage_reason, format_effective_full_damage_reason, format_pct_hundredths, + full_damage_source, +}; +use super::render_breakdown::{ + PerfRenderBreakdown, PerfRenderBreakdownAccumulator, PerfRenderBreakdownSummary, + log_render_stage_frame, log_render_stage_summary, +}; +use super::{ + MAX_PENDING_INPUT_SAMPLES, MAX_RECENT_LATENCIES, MAX_RECENT_RENDER_DURATIONS, PerfCommitReport, + PerfFinalSummaryReport, PerfFrameContext, PerfFramePacingReport, PerfFramePacingSummary, + PerfInputSample, PerfInputSource, PerfMetrics, PerfRenderSkipReason, PerfSlowFrame, + PerfSlowRenderFrame, PerfSummary, SLOW_INPUT_TO_COMMIT, SLOW_RENDER_FALLBACK, + SUMMARY_FRAME_INTERVAL, SUMMARY_INTERVAL, dominant_full_damage_reason, duration_ms, + format_force_full_reason, format_full_damage_reasons, format_optional_ms, format_pct, + frame_budget_duration, log_frame_pacing_summary, log_input_summary, percentile_nearest_rank, + perf_log_enabled_from_env, +}; +use crate::env_vars::PERF_LOG_ENV; +use crate::input::Tool; +use log::info; +use std::collections::{BTreeMap, VecDeque}; +use std::time::{Duration, Instant}; impl PerfMetrics { pub(in crate::backend::wayland::state) fn from_env() -> Self { diff --git a/src/backend/wayland/state/perf_modules/metrics/tests.rs b/src/backend/wayland/state/perf_modules/metrics/tests.rs index 6caeeb019..73cd49d2a 100644 --- a/src/backend/wayland/state/perf_modules/metrics/tests.rs +++ b/src/backend/wayland/state/perf_modules/metrics/tests.rs @@ -1,4 +1,8 @@ use super::*; +use crate::backend::wayland::state::perf::{ + PerfDamageDiagnostics, PerfRenderProfileKind, PerfRenderStageDurations, damage_area_pct, + damage_covers_surface, +}; use crate::util::Rect; fn record_sample(metrics: &mut PerfMetrics, received_at: Instant) { diff --git a/src/backend/wayland/state/perf_modules/render_breakdown.rs b/src/backend/wayland/state/perf_modules/render_breakdown.rs index f628e4e28..c15a93489 100644 --- a/src/backend/wayland/state/perf_modules/render_breakdown.rs +++ b/src/backend/wayland/state/perf_modules/render_breakdown.rs @@ -2,6 +2,8 @@ use std::time::Duration; use log::info; +use crate::draw::SpotlightSnapshotStrategy; + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(in crate::backend::wayland) enum PerfRenderProfileKind { #[default] @@ -61,8 +63,7 @@ pub(in crate::backend::wayland) struct PerfRenderBreakdown { pub(in crate::backend::wayland) canvas_layer_cache_used: bool, pub(in crate::backend::wayland) spotlight_regions: usize, pub(in crate::backend::wayland) spotlight_copied_pixels: u64, - pub(in crate::backend::wayland) spotlight_strategy: - Option, + pub(in crate::backend::wayland) spotlight_strategy: Option, } #[cfg_attr(not(test), allow(dead_code))] @@ -315,12 +316,10 @@ fn dominant_render_stage(stages: &PerfRenderStageDurations) -> (&'static str, Du .unwrap_or(("none", Duration::ZERO)) } -fn spotlight_strategy_label( - strategy: Option, -) -> &'static str { +fn spotlight_strategy_label(strategy: Option) -> &'static str { match strategy { - Some(crate::draw::SpotlightSnapshotStrategy::Regional) => "regional", - Some(crate::draw::SpotlightSnapshotStrategy::FullSurface) => "full_surface", + Some(SpotlightSnapshotStrategy::Regional) => "regional", + Some(SpotlightSnapshotStrategy::FullSurface) => "full_surface", None => "none", } } diff --git a/src/backend/wayland/state/pointer_runtime.rs b/src/backend/wayland/state/pointer_runtime.rs index 042a216e0..29b539dd2 100644 --- a/src/backend/wayland/state/pointer_runtime.rs +++ b/src/backend/wayland/state/pointer_runtime.rs @@ -1,21 +1,20 @@ +use std::mem; + +use crate::input::state::InputTextResources; +use crate::input::{InputState, MouseButton}; + use super::contact_owner::{ContactMotion, ContactOwner, held_motion}; use contacts::PointerContacts; use log::warn; use smithay_client_toolkit::seat::pointer::{CursorIcon, PointerData, ThemedPointer}; -use wayland_client::{ - Connection, Proxy, - protocol::{wl_pointer, wl_seat, wl_surface, wl_touch}, -}; -use wayland_protocols::wp::{ - pointer_constraints::zv1::client::zwp_locked_pointer_v1::ZwpLockedPointerV1, - relative_pointer::zv1::client::zwp_relative_pointer_v1::ZwpRelativePointerV1, -}; +use wayland_client::protocol::{wl_pointer, wl_seat, wl_surface, wl_touch}; +use wayland_client::{Connection, Proxy}; +use wayland_protocols::wp::pointer_constraints::zv1::client::zwp_locked_pointer_v1::ZwpLockedPointerV1; +use wayland_protocols::wp::relative_pointer::zv1::client::zwp_relative_pointer_v1::ZwpRelativePointerV1; use super::seat_devices::SeatDevices; -use crate::{ - input::state::{RegionInputSource, ToastPress}, - ui::{OnboardingCardPress, ZoomChipPress}, -}; +use crate::input::state::{RegionInputSource, ToastPress}; +use crate::ui::{OnboardingCardPress, ZoomChipPress}; mod chrome; mod contacts; @@ -91,7 +90,7 @@ impl ReleaseSuppression { } fn take(&mut self, source: RegionInputSource) -> bool { - self.slot_mut(source).is_some_and(std::mem::take) + self.slot_mut(source).is_some_and(mem::take) } } @@ -138,10 +137,10 @@ impl PointerRuntime { pub(in crate::backend::wayland) fn route_canvas_press( &mut self, - input: &mut crate::input::InputState, - resources: crate::input::state::InputTextResources<'_>, + input: &mut InputState, + resources: InputTextResources<'_>, button: u32, - mouse_button: crate::input::MouseButton, + mouse_button: MouseButton, (sx, sy): (i32, i32), (wx, wy): (f64, f64), ) { @@ -153,14 +152,14 @@ impl PointerRuntime { pub(in crate::backend::wayland) fn take_contact( &mut self, button: u32, - input: &crate::input::InputState, + input: &InputState, ) -> Option<(ContactOwner, bool)> { self.contacts.take(button, input) } pub(in crate::backend::wayland) fn reconcile_contacts( &mut self, - input: &crate::input::InputState, + input: &InputState, zoom_pan: bool, toolbar_drag: bool, ) { diff --git a/src/backend/wayland/state/pointer_runtime/chrome.rs b/src/backend/wayland/state/pointer_runtime/chrome.rs index 4509ae432..99f9e7e61 100644 --- a/src/backend/wayland/state/pointer_runtime/chrome.rs +++ b/src/backend/wayland/state/pointer_runtime/chrome.rs @@ -1,4 +1,7 @@ -use super::*; +use std::mem; + +use crate::input::state::ToastPress; +use crate::ui::{OnboardingCardPress, ZoomChipPress}; #[derive(Debug, Clone, Copy, Default)] pub(super) struct PendingChromePress { @@ -44,7 +47,7 @@ impl PendingChromePress { } pub(super) fn take_status_hud(&mut self) -> bool { - std::mem::take(&mut self.status_hud) + mem::take(&mut self.status_hud) } pub(super) fn arm_zoom_chip(&mut self, press: ZoomChipPress) -> bool { @@ -56,7 +59,7 @@ impl PendingChromePress { } pub(super) fn take_zoom_chip(&mut self) -> ZoomChipPress { - std::mem::replace(&mut self.zoom_chip, ZoomChipPress::None) + mem::replace(&mut self.zoom_chip, ZoomChipPress::None) } pub(super) fn arm_onboarding_card(&mut self, press: OnboardingCardPress) -> bool { diff --git a/src/backend/wayland/state/pointer_runtime/contacts.rs b/src/backend/wayland/state/pointer_runtime/contacts.rs index bf1241abc..11343f5f7 100644 --- a/src/backend/wayland/state/pointer_runtime/contacts.rs +++ b/src/backend/wayland/state/pointer_runtime/contacts.rs @@ -1,6 +1,7 @@ //! Motion and release ownership are independent and bound by the real press route. use crate::backend::wayland::state::ContactOwner; -use crate::input::{InputState, state::interaction::RoutingOutcome}; +use crate::input::InputState; +use crate::input::state::interaction::RoutingOutcome; #[derive(Debug)] struct HeldContact { @@ -87,11 +88,13 @@ impl PointerContacts { #[cfg(test)] mod tests { + use crate::input::state::test_support; + use super::*; #[test] fn unrelated_releases_and_rebinds_preserve_the_first_held_contact() { let mut contacts = PointerContacts::default(); - let input = crate::input::state::test_support::make_test_input_state(); + let input = test_support::make_test_input_state(); let canvas = ContactOwner::Canvas; contacts.bind(1, canvas); contacts.bind(2, ContactOwner::InlineToolbar); @@ -116,20 +119,20 @@ mod tests { mod dispatch_regressions { use crate::backend::wayland::state::pointer_runtime::PointerRuntime; use crate::backend::wayland::state::{ContactMotion, ContactOwner}; + use crate::config::{Config, RadialMenuMouseBinding}; use crate::draw::TextMeasurer; - use crate::input::{ - MouseButton, - state::{InputTextResources, test_support::make_test_input_state}, - }; + use crate::input::state::InputTextResources; + use crate::input::state::test_support::make_test_input_state; + use crate::input::{InputState, MouseButton}; use crate::ui_text::UiTextEngine; #[test] fn inert_canvas_presses_cannot_inherit_another_devices_canvas_gesture() { for mouse_button in [MouseButton::Right, MouseButton::Middle] { let mut pointer = PointerRuntime::new(); - let mut config = crate::config::Config::default(); - config.ui.radial_menu_mouse_binding = crate::config::RadialMenuMouseBinding::Disabled; - let mut input = crate::input::InputState::from_config(&config); + let mut config = Config::default(); + config.ui.radial_menu_mouse_binding = RadialMenuMouseBinding::Disabled; + let mut input = InputState::from_config(&config); let measurer = TextMeasurer::default(); let engine = UiTextEngine::default(); let resources = || InputTextResources { @@ -225,10 +228,9 @@ mod popup_dispatch_regressions { use crate::backend::wayland::state::pointer_runtime::PointerRuntime; use crate::backend::wayland::state::{ContactMotion, ContactOwner}; use crate::draw::TextMeasurer; - use crate::input::{ - MouseButton, - state::{InputTextResources, test_support::make_test_input_state}, - }; + use crate::input::MouseButton; + use crate::input::state::InputTextResources; + use crate::input::state::test_support::make_test_input_state; use crate::ui_text::UiTextEngine; #[test] diff --git a/src/backend/wayland/state/pointer_runtime/tests.rs b/src/backend/wayland/state/pointer_runtime/tests.rs index a565dac8c..037e01af5 100644 --- a/src/backend/wayland/state/pointer_runtime/tests.rs +++ b/src/backend/wayland/state/pointer_runtime/tests.rs @@ -1,8 +1,10 @@ +use crate::draw::TextMeasurer; +use crate::input::state::test_support; +use crate::ui_text::UiTextEngine; + use super::{PendingChromePress, PointerRuntime, TouchState, TouchTarget}; -use crate::{ - input::state::{RegionInputSource, ToastPress}, - ui::{OnboardingCardPress, ZoomChipPress}, -}; +use crate::input::state::{RegionInputSource, ToastPress}; +use crate::ui::{OnboardingCardPress, ZoomChipPress}; use smithay_client_toolkit::seat::pointer::CursorIcon; #[test] @@ -151,16 +153,17 @@ fn assert_interleaved_release_keeps_stroke( consumed_source: RegionInputSource, drawing_source: RegionInputSource, ) { - use crate::input::{MouseButton, state::DrawingState}; + use crate::input::MouseButton; + use crate::input::state::DrawingState; - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, }; let mut runtime = PointerRuntime::new(); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); // The modal press was consumed, but that device remains held while the // other device starts drawing. Both canvas press handlers reset chrome. diff --git a/src/backend/wayland/state/preference_stores.rs b/src/backend/wayland/state/preference_stores.rs index bb1ab7999..be59dc4a3 100644 --- a/src/backend/wayland/state/preference_stores.rs +++ b/src/backend/wayland/state/preference_stores.rs @@ -1,13 +1,11 @@ -use crate::{ - backend::wayland::{ - RuntimeWakeHandle, - config_edits::ConfigEditWorker, - runtime_ui_state::{ToolbarRuntimeState, UnavailablePersistencePreviews}, - }, - onboarding::OnboardingStore, - palette_recents::PaletteRecentsWriter, - ui::toolbar::RuntimeUiPersistenceSnapshot, +use crate::backend::wayland::RuntimeWakeHandle; +use crate::backend::wayland::config_edits::ConfigEditWorker; +use crate::backend::wayland::runtime_ui_state::{ + ToolbarRuntimeState, UnavailablePersistencePreviews, }; +use crate::onboarding::OnboardingStore; +use crate::palette_recents::PaletteRecentsWriter; +use crate::ui::toolbar::RuntimeUiPersistenceSnapshot; /// Runtime state for persisted UI preferences and degraded-mode previews. pub(in crate::backend::wayland) struct RuntimeUiSlot { diff --git a/src/backend/wayland/state/protocol_globals.rs b/src/backend/wayland/state/protocol_globals.rs index 64a46386e..d50e9e0a9 100644 --- a/src/backend/wayland/state/protocol_globals.rs +++ b/src/backend/wayland/state/protocol_globals.rs @@ -1,15 +1,13 @@ -use smithay_client_toolkit::{ - activation::ActivationState, - compositor::CompositorState, - output::OutputState, - registry::RegistryState, - seat::{ - SeatState, pointer_constraints::PointerConstraintsState, - relative_pointer::RelativePointerState, - }, - shell::{wlr_layer::LayerShell, xdg::XdgShell}, - shm::Shm, -}; +use smithay_client_toolkit::activation::ActivationState; +use smithay_client_toolkit::compositor::CompositorState; +use smithay_client_toolkit::output::OutputState; +use smithay_client_toolkit::registry::RegistryState; +use smithay_client_toolkit::seat::SeatState; +use smithay_client_toolkit::seat::pointer_constraints::PointerConstraintsState; +use smithay_client_toolkit::seat::relative_pointer::RelativePointerState; +use smithay_client_toolkit::shell::wlr_layer::LayerShell; +use smithay_client_toolkit::shell::xdg::XdgShell; +use smithay_client_toolkit::shm::Shm; pub(in crate::backend::wayland) struct ProtocolGlobalsSeed { pub registry: RegistryState, diff --git a/src/backend/wayland/state/region_capture.rs b/src/backend/wayland/state/region_capture.rs index d44694a44..de3138774 100644 --- a/src/backend/wayland/state/region_capture.rs +++ b/src/backend/wayland/state/region_capture.rs @@ -1,9 +1,11 @@ -use crate::backend::wayland::acquisition::ScreenAcquisitionId; +#[cfg(test)] use crate::input::SelectionHandle; +#[cfg(test)] use crate::input::state::{ RegionInputSource, RegionPurposeTag, RegionSelectUiState, RegionSelection, ScreenCaptureSource, }; -use crate::screen_pixels::{ImagePixelRect, ImagePoint, clamp_edge}; +#[cfg(test)] +use crate::screen_pixels::{ImagePixelRect, ImagePoint}; mod active_state; mod board; @@ -23,32 +25,36 @@ mod selection_state; mod source_guard; mod window_snap; +#[cfg(test)] use active_state::RegionInteractionPhase; pub(super) use active_state::{ActiveScreenRegion, FreezeOwnership}; pub(in crate::backend::wayland) use board::{ board_bounds_for_world_rect, world_rect_for_composed_region, world_rect_for_image_rect_exact, }; -pub(in crate::backend::wayland) use cut_preview::CutPreviewOutcome; pub(in crate::backend::wayland) use cut_review::RegionReviewPress; -pub(in crate::backend::wayland) use cut_review::{CutPreviewKey, RegionReviewEdits}; +#[cfg(test)] pub(super) use events::finalize_region_selection_with_review_edits; -use events::*; -pub(in crate::backend::wayland) use geometry::{RegionPickerMeasurement, RegionSelectionGeometry}; +#[cfg(test)] +use events::{ + begin_region_selection_event, initial_square_modifier, region_owner_lost_event, + sync_region_square_modifier_event, update_region_selection_event, +}; +pub(in crate::backend::wayland) use geometry::RegionPickerMeasurement; pub(in crate::backend::wayland) use intent::{RegionCaptureIntent, RegionPickerOptions}; -use measure::*; +#[cfg(test)] +use measure::{MeasureModeTransition, measure_anchor, measure_edge, measure_mode_transition}; pub(in crate::backend::wayland) use measure::{RegionOwnerLoss, RegionSelectionFinalize}; -use review_state::ReviewResizeGrip; #[cfg(test)] use review_state::resized_review_rect; pub(super) use source_guard::owned_generation_is_current; -use source_guard::*; +#[cfg(test)] +use source_guard::{ + active_eyedropper_source_changed, active_region_source_changed, screen_region_invariant, +}; pub(in crate::backend::wayland) use window_snap::WindowSnapDirection; -pub(super) use window_snap::{WindowSnapQuery, WindowSnapSession}; -use super::WaylandState; -use super::screen_image::{ - ScreenSourceToken, current_screen_source_token, image_point_for_screen_point, screen_source_is, -}; +#[cfg(test)] +use super::screen_image::ScreenSourceToken; #[cfg(test)] mod tests; diff --git a/src/backend/wayland/state/region_capture/active_state.rs b/src/backend/wayland/state/region_capture/active_state.rs index 356dc5ec8..f39b97ed1 100644 --- a/src/backend/wayland/state/region_capture/active_state.rs +++ b/src/backend/wayland/state/region_capture/active_state.rs @@ -1,4 +1,15 @@ -use super::*; +use crate::backend::wayland::state::screen_image; + +use super::super::screen_image::{ScreenSourceToken, image_point_for_screen_point}; +use super::geometry; +use super::geometry::{RegionPickerMeasurement, RegionSelectionGeometry}; +use super::measure::{RegionSelectionFinalize, measure_anchor}; +use super::review_state::ReviewResizeGrip; +use crate::backend::wayland::acquisition::ScreenAcquisitionId; +use crate::input::state::{ + RegionInputSource, RegionPurposeTag, RegionSelectUiState, RegionSelection, ScreenCaptureSource, +}; +use crate::screen_pixels::{ImagePixelRect, ImagePoint}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(in crate::backend::wayland::state) enum FreezeOwnership { @@ -301,9 +312,7 @@ impl ActiveScreenRegion { return None; }; let image_rect = self.stored_review_rect()?; - let display = crate::backend::wayland::state::screen_image::screen_rect_for_image_rect( - &source, image_rect, - )?; + let display = screen_image::screen_rect_for_image_rect(&source, image_rect)?; Some(RegionSelectionGeometry::review( purpose, image_rect, diff --git a/src/backend/wayland/state/region_capture/board.rs b/src/backend/wayland/state/region_capture/board.rs index e68118c2b..c4f537a59 100644 --- a/src/backend/wayland/state/region_capture/board.rs +++ b/src/backend/wayland/state/region_capture/board.rs @@ -1,5 +1,6 @@ +use crate::canvas_export::CanvasExportRect; +use crate::screen_pixels::ImagePixelRect; use crate::util::Rect; -use crate::{canvas_export::CanvasExportRect, screen_pixels::ImagePixelRect}; use super::super::screen_image::ScreenSourceToken; @@ -128,6 +129,8 @@ pub(in crate::backend::wayland) fn world_rect_for_composed_region( #[cfg(test)] mod tests { + use wayland_client::protocol::wl_output::Transform; + use super::*; use crate::backend::wayland::state::screen_image::ScreenImageKind; @@ -141,7 +144,7 @@ mod tests { stride: 3200, surface: (800, 600), output_scale: 1, - output_transform: wayland_client::protocol::wl_output::Transform::Normal, + output_transform: Transform::Normal, zoom_transformed, zoom_scale: if zoom_transformed { 2.0 } else { 1.0 }, zoom_view_offset: if zoom_transformed { @@ -334,7 +337,7 @@ mod tests { #[test] fn every_placement_stays_within_a_board_pixel_of_the_exact_crop() { let steps = 0..40; - for origin_step in steps.clone() { + for origin_step in steps { for extent_step in 1..40 { let x = -3.0 + f64::from(origin_step) * 0.17; let width = f64::from(extent_step) * 0.13; diff --git a/src/backend/wayland/state/region_capture/cut_preview/apply.rs b/src/backend/wayland/state/region_capture/cut_preview/apply.rs index 9d6e4beed..c84a77cac 100644 --- a/src/backend/wayland/state/region_capture/cut_preview/apply.rs +++ b/src/backend/wayland/state/region_capture/cut_preview/apply.rs @@ -1,3 +1,6 @@ +use crate::capture; +use crate::input::state::RegionSelection; + use super::super::cut_review::{ CutPreviewKey, PreviewApply, RegionCutBase, RegionCutPreview, RegionReviewEdits, }; @@ -13,10 +16,7 @@ fn preview_matches_review(edits: &RegionReviewEdits, key: &CutPreviewKey) -> boo pub(super) fn apply_cut_preview_outcome( edits: &mut Option, outcome: CutPreviewOutcome, - display_for: impl FnOnce( - &RegionReviewEdits, - &[CutBand], - ) -> Option, + display_for: impl FnOnce(&RegionReviewEdits, &[CutBand]) -> Option, ) -> PreviewApply { let Some(edits) = edits.as_mut() else { return PreviewApply::Ignored; @@ -87,7 +87,7 @@ pub(super) fn apply_cut_preview_outcome( } fn output_size_from_key(key: &CutPreviewKey) -> Option<(u32, u32)> { - crate::capture::output_size( + capture::output_size( ( key.fingerprint.source_rect().width(), key.fingerprint.source_rect().height(), @@ -106,10 +106,7 @@ pub(super) struct CutPreviewVisibleEffect { pub(super) fn visible_effect_for_cut_preview( edits: &mut Option, outcome: CutPreviewOutcome, - display_for: impl FnOnce( - &RegionReviewEdits, - &[CutBand], - ) -> Option, + display_for: impl FnOnce(&RegionReviewEdits, &[CutBand]) -> Option, ) -> CutPreviewVisibleEffect { let desired = edits .as_ref() diff --git a/src/backend/wayland/state/region_capture/cut_preview/scheduler.rs b/src/backend/wayland/state/region_capture/cut_preview/scheduler.rs index 09d9267f6..92a331e04 100644 --- a/src/backend/wayland/state/region_capture/cut_preview/scheduler.rs +++ b/src/backend/wayland/state/region_capture/cut_preview/scheduler.rs @@ -1,3 +1,6 @@ +use crate::capture; +use crate::input::InputState; + use super::super::cut_review::{CutPreviewKey, RegionReviewEdits, native_extent_display}; use super::apply::{CutPreviewVisibleEffect, visible_effect_for_cut_preview}; use super::job::{CutPreviewInput, CutPreviewJob, CutPreviewOutcome, run_cut_preview}; @@ -49,10 +52,7 @@ pub(super) fn cut_preview_from_poll( } } -pub(super) fn present_cut_preview_effect( - input: &mut crate::input::InputState, - effect: CutPreviewVisibleEffect, -) { +pub(super) fn present_cut_preview_effect(input: &mut InputState, effect: CutPreviewVisibleEffect) { if effect.toast_current_failure { input.push_toast( ToastPriority::Info, @@ -164,7 +164,7 @@ impl WaylandState { native_extent_display( &edits.correlation.source, edits.source_rect, - crate::capture::output_size( + capture::output_size( (edits.source_rect.width(), edits.source_rect.height()), cuts, ) diff --git a/src/backend/wayland/state/region_capture/cut_preview/snapshot.rs b/src/backend/wayland/state/region_capture/cut_preview/snapshot.rs index 85bf92c1a..6b7e1dda2 100644 --- a/src/backend/wayland/state/region_capture/cut_preview/snapshot.rs +++ b/src/backend/wayland/state/region_capture/cut_preview/snapshot.rs @@ -1,3 +1,9 @@ +use std::sync::Arc; + +use crate::backend::wayland::state::screen_image::ScreenSourceToken; +use crate::canvas_export::SpotlightPassSnapshot; +use crate::screen_pixels::ScreenImage; + use super::super::ActiveScreenRegion; use super::super::cut_review::{ RegionAnnotatedRenderContext, RegionRenderFingerprint, RegionReviewCorrelation, @@ -67,7 +73,7 @@ impl WaylandState { canvas_content_generation: self.input_state.canvas_content_generation(), board_view_offset: self.board_view_offset(), text_halo_enabled: self.config.drawing.text_halo_enabled, - spotlight: crate::canvas_export::SpotlightPassSnapshot { + spotlight: SpotlightPassSnapshot { dim_opacity: self.input_state.style.spotlight_dim_opacity, feather: self.input_state.style.spotlight_feather, }, @@ -96,8 +102,8 @@ impl WaylandState { fn retain_current_capture_image( &self, - token: &crate::backend::wayland::state::screen_image::ScreenSourceToken, - ) -> Result, CaptureError> { + token: &ScreenSourceToken, + ) -> Result, CaptureError> { let Some(source) = displayed_screen_image( &self.zoom, &self.frozen, @@ -126,7 +132,7 @@ impl WaylandState { fn region_pixel_source( &self, fingerprint: &RegionRenderFingerprint, - shared_image: std::sync::Arc, + shared_image: Arc, ) -> Result { match fingerprint { RegionRenderFingerprint::Raw { source_rect, .. } => Ok(RegionPixelSource::Raw { diff --git a/src/backend/wayland/state/region_capture/cut_preview/tests.rs b/src/backend/wayland/state/region_capture/cut_preview/tests.rs index 4619f87d1..06c233c66 100644 --- a/src/backend/wayland/state/region_capture/cut_preview/tests.rs +++ b/src/backend/wayland/state/region_capture/cut_preview/tests.rs @@ -1,3 +1,17 @@ +use crate::backend::wayland::RuntimeWakeSource; +use crate::backend::wayland::runtime_operation::{ + RuntimeOperationController, RuntimeOperationId, RuntimeOperationIdSource, + RuntimeOperationSubmitError, +}; +use crate::backend::wayland::state::screen_image::ScreenSourceToken; +use crate::canvas_export::SpotlightPassSnapshot; +use crate::input::InputState; +use crate::input::state::{RegionPurposeTag, test_support}; + +use std::sync::mpsc; +use std::time::{Duration, Instant}; +use std::{io, thread}; + use super::super::ActiveScreenRegion; use super::super::cut_review::{ CutPreviewKey, PreviewApply, RegionAnnotatedRenderContext, RegionCutBase, RegionCutPreview, @@ -13,8 +27,7 @@ use super::snapshot::{ }; use crate::backend::wayland::runtime_operation::RuntimeOperationPoll; use crate::backend::wayland::state::screen_image::ScreenImageKind; -use crate::capture::CutAxis; -use crate::capture::{CaptureError, CutBand}; +use crate::capture::{CaptureError, CutAxis, CutBand}; use crate::input::state::RegionSelection; use crate::screen_pixels::{ImagePixelRect, PackedArgb32}; use std::sync::Arc; @@ -22,8 +35,8 @@ use wayland_client::protocol::wl_output::Transform; type ContextMutation = (&'static str, fn(&mut RegionAnnotatedRenderContext)); -fn token() -> crate::backend::wayland::state::screen_image::ScreenSourceToken { - crate::backend::wayland::state::screen_image::ScreenSourceToken { +fn token() -> ScreenSourceToken { + ScreenSourceToken { output_id: 1, output_layout_generation: 1, kind: ScreenImageKind::Frozen, @@ -50,7 +63,7 @@ fn key(revision: u64, generation: u64) -> CutPreviewKey { source_rect: rect, }, revision, - cuts: vec![CutBand::new(CutAxis::Columns, 1, 2).unwrap()], + cuts: vec![CutBand::try_new(CutAxis::Columns, 1, 2).unwrap()], } } @@ -66,7 +79,7 @@ fn annotated_key(revision: u64, generation: u64) -> CutPreviewKey { canvas_content_generation: 4, board_view_offset: (5.0, 6.0), text_halo_enabled: true, - spotlight: crate::canvas_export::SpotlightPassSnapshot { + spotlight: SpotlightPassSnapshot { dim_opacity: 0.7, feather: 0.2, }, @@ -317,7 +330,7 @@ fn stale_failure_does_not_touch_the_new_review() { assert!(edits.unwrap().failed_revision.is_none()); } -fn ready(purpose: crate::input::state::RegionPurposeTag) -> ActiveScreenRegion { +fn ready(purpose: RegionPurposeTag) -> ActiveScreenRegion { ActiveScreenRegion::Ready { purpose, generation: 7, @@ -449,20 +462,17 @@ fn review_edits(desired: CutPreviewKey) -> Option { } fn poll_until_terminal( - controller: &mut crate::backend::wayland::runtime_operation::RuntimeOperationController< - CutPreviewKey, - CutPreviewOutcome, - >, + controller: &mut RuntimeOperationController, ) -> RuntimeOperationPoll { - let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); + let deadline = Instant::now() + Duration::from_secs(2); loop { match controller.poll() { RuntimeOperationPoll::Pending { .. } => { assert!( - std::time::Instant::now() < deadline, + Instant::now() < deadline, "cut preview worker did not finish" ); - std::thread::yield_now(); + thread::yield_now(); } poll => return poll, } @@ -471,7 +481,7 @@ fn poll_until_terminal( fn apply_poll( edits: &mut Option, - input: &mut crate::input::InputState, + input: &mut InputState, poll: RuntimeOperationPoll, ) { if let Some(outcome) = cut_preview_from_poll(poll) { @@ -480,22 +490,16 @@ fn apply_poll( } } -fn preview_controller() -> crate::backend::wayland::runtime_operation::RuntimeOperationController< - CutPreviewKey, - CutPreviewOutcome, -> { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); - crate::backend::wayland::runtime_operation::RuntimeOperationController::new( - crate::backend::wayland::runtime_operation::RuntimeOperationIdSource::new(), - wake.handle(), - ) +fn preview_controller() -> RuntimeOperationController { + let wake = RuntimeWakeSource::try_new().unwrap(); + RuntimeOperationController::new(RuntimeOperationIdSource::new(), wake.handle()) } #[test] fn worker_error_toasts_once_and_marks_dirty_for_the_current_revision() { let desired = key(3, 1); let mut edits = review_edits(desired.clone()); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); input.needs_redraw = false; let _ = input.dirty_tracker.take_region_report(2, 1); @@ -547,10 +551,10 @@ fn worker_error_toasts_once_and_marks_dirty_for_the_current_revision() { fn worker_panic_and_disconnect_fail_the_current_preview() { let desired = key(4, 1); let mut edits = review_edits(desired.clone()); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); let mut controller = preview_controller(); controller - .try_submit(desired.clone(), "test-cut-preview-panic", || { + .try_submit(desired, "test-cut-preview-panic", || { panic!("expected cut preview panic") }) .unwrap(); @@ -560,11 +564,11 @@ fn worker_panic_and_disconnect_fail_the_current_preview() { let desired = key(5, 1); let mut edits = review_edits(desired.clone()); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); let mut controller = preview_controller(); controller .try_submit_with_spawner_for_test( - desired.clone(), + desired, || panic!("must not run"), |job| { drop(job); @@ -581,19 +585,19 @@ fn worker_panic_and_disconnect_fail_the_current_preview() { fn submit_failure_is_visible_and_busy_is_not() { let desired = key(6, 1); let mut edits = review_edits(desired.clone()); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); let mut controller = preview_controller(); let failure = controller .try_submit_with_spawner_for_test( - desired.clone(), + desired, || panic!("must not run"), - |_job| Err(std::io::Error::other("injected spawn failure")), + |_job| Err(io::Error::other("injected spawn failure")), ) .unwrap_err(); let (error, failed_key) = failure.into_parts(); assert!(matches!( error, - crate::backend::wayland::runtime_operation::RuntimeOperationSubmitError::SpawnFailed { .. } + RuntimeOperationSubmitError::SpawnFailed { .. } )); present_cut_preview_effect( &mut input, @@ -611,9 +615,9 @@ fn submit_failure_is_visible_and_busy_is_not() { let desired = key(7, 1); let edits = review_edits(desired.clone()); - let input = crate::input::state::test_support::make_test_input_state(); + let input = test_support::make_test_input_state(); let mut controller = preview_controller(); - let (release_tx, release_rx) = std::sync::mpsc::channel(); + let (release_tx, release_rx) = mpsc::channel(); let blocked_key = desired.clone(); controller .try_submit(desired.clone(), "test-cut-preview-busy", move || { @@ -625,16 +629,13 @@ fn submit_failure_is_visible_and_busy_is_not() { }) .unwrap(); let busy = controller - .try_submit(desired.clone(), "test-cut-preview-busy-2", || { + .try_submit(desired, "test-cut-preview-busy-2", || { panic!("must not run") }) .unwrap_err() .into_parts() .0; - assert!(matches!( - busy, - crate::backend::wayland::runtime_operation::RuntimeOperationSubmitError::Busy { .. } - )); + assert!(matches!(busy, RuntimeOperationSubmitError::Busy { .. })); assert!(edits.as_ref().unwrap().failed_revision.is_none()); assert_eq!(input.test_toast_count(), 0); release_tx.send(()).unwrap(); @@ -645,12 +646,12 @@ fn submit_failure_is_visible_and_busy_is_not() { fn identity_mismatch_and_stale_failure_follow_current_or_silent_rules() { let desired = key(8, 1); let mut edits = review_edits(desired.clone()); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); apply_poll( &mut edits, &mut input, RuntimeOperationPoll::ProducerFailed { - id: crate::backend::wayland::runtime_operation::RuntimeOperationId::from_test(3), + id: RuntimeOperationId::from_test(3), context: desired.clone(), reason: "runtime operation worker reported transport identity 4, expected 3" .to_string(), @@ -661,14 +662,14 @@ fn identity_mismatch_and_stale_failure_follow_current_or_silent_rules() { assert!(input.needs_redraw); let mut next = review_edits(key(9, 2)); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); input.needs_redraw = false; let _ = input.dirty_tracker.take_region_report(2, 1); apply_poll( &mut next, &mut input, RuntimeOperationPoll::ProducerFailed { - id: crate::backend::wayland::runtime_operation::RuntimeOperationId::from_test(3), + id: RuntimeOperationId::from_test(3), context: desired, reason: "old picker".to_string(), }, @@ -692,7 +693,7 @@ fn busy_controller_runs_only_the_newest_desired_key_after_terminal_poll() { let newest = key(12, 1); let mut edits = review_edits(first.clone()); let mut controller = preview_controller(); - let (release_tx, release_rx) = std::sync::mpsc::channel(); + let (release_tx, release_rx) = mpsc::channel(); let first_job_key = first.clone(); controller .try_submit(first, "test-cut-preview-queued-first", move || { @@ -762,7 +763,7 @@ fn reset_while_worker_is_active_releases_buffers_and_ignores_completion() { let worker_base_weak = Arc::downgrade(&worker_base); let worker_preview = Arc::new(PackedArgb32::new(1, 1, 4, vec![1; 4]).unwrap()); let worker_preview_weak = Arc::downgrade(&worker_preview); - let (release_tx, release_rx) = std::sync::mpsc::channel(); + let (release_tx, release_rx) = mpsc::channel(); let mut controller = preview_controller(); let worker_key = active_key.clone(); controller @@ -815,7 +816,7 @@ fn review_exit_and_reopen_releases_buffers_and_rejects_old_completion() { let worker_base_weak = Arc::downgrade(&worker_base); let worker_preview = Arc::new(PackedArgb32::new(1, 1, 4, vec![1; 4]).unwrap()); let worker_preview_weak = Arc::downgrade(&worker_preview); - let (release_tx, release_rx) = std::sync::mpsc::channel(); + let (release_tx, release_rx) = mpsc::channel(); let mut controller = preview_controller(); let worker_key = old_key.clone(); controller @@ -874,7 +875,7 @@ fn render_source_jobs_paint_annotations_before_applying_key_cuts_on_the_worker() generation: 7, source: source_token, }; - let spotlight = crate::canvas_export::SpotlightPassSnapshot { + let spotlight = SpotlightPassSnapshot { dim_opacity: 0.0, feather: 0.0, }; @@ -898,7 +899,7 @@ fn render_source_jobs_paint_annotations_before_applying_key_cuts_on_the_worker() source_rect: selection, } }; - desired.cuts = vec![CutBand::new(CutAxis::Columns, 4, 8).unwrap()]; + desired.cuts = vec![CutBand::try_new(CutAxis::Columns, 4, 8).unwrap()]; let source = if annotated { let mut frame = Frame::new(); frame.add_shape(Shape::Rect { @@ -931,9 +932,7 @@ fn render_source_jobs_paint_annotations_before_applying_key_cuts_on_the_worker() key: desired.clone(), input: CutPreviewInput::RenderSource(source), }; - let outcome = std::thread::spawn(move || run_cut_preview(job)) - .join() - .unwrap(); + let outcome = thread::spawn(move || run_cut_preview(job)).join().unwrap(); let CutPreviewOutcome::Success { key, base, diff --git a/src/backend/wayland/state/region_capture/cut_review/actions.rs b/src/backend/wayland/state/region_capture/cut_review/actions.rs index 1014f277a..b5ac47925 100644 --- a/src/backend/wayland/state/region_capture/cut_review/actions.rs +++ b/src/backend/wayland/state/region_capture/cut_review/actions.rs @@ -1,3 +1,8 @@ +use crate::i18n; +use crate::input::state::{RegionPurposeTag, Toast, ToastPriority}; +use crate::screen_pixels::PackedArgb32; +use crate::ui::RegionAction; + use super::geometry::{cut_band_display, display_contains, output_display_for, quantized_cut}; use super::model::{CutCommit, CutMode, RegionReviewCorrelation, RegionReviewEdits}; use crate::backend::wayland::state::WaylandState; @@ -12,7 +17,7 @@ pub(in crate::backend::wayland::state::region_capture) fn review_edits_for_activ rect: ImagePixelRect, ) -> Option { let Some(ActiveScreenRegion::Ready { - purpose: crate::input::state::RegionPurposeTag::CaptureInteractive, + purpose: RegionPurposeTag::CaptureInteractive, generation, source, .. @@ -83,20 +88,17 @@ impl WaylandState { pub(in crate::backend::wayland) fn apply_region_review_edit( &mut self, - action: crate::ui::RegionAction, + action: RegionAction, ) -> bool { match action { - crate::ui::RegionAction::CutBand => self.toggle_region_cut_mode(), - crate::ui::RegionAction::UndoCut => self.undo_region_cut(), - crate::ui::RegionAction::RedoCut => self.redo_region_cut(), - crate::ui::RegionAction::ResetCuts => self.reset_region_cuts(), - crate::ui::RegionAction::ToggleIncludeDrawings => { - self.toggle_region_picker_include_drawings() + RegionAction::CutBand => self.toggle_region_cut_mode(), + RegionAction::UndoCut => self.undo_region_cut(), + RegionAction::RedoCut => self.redo_region_cut(), + RegionAction::ResetCuts => self.reset_region_cuts(), + RegionAction::ToggleIncludeDrawings => self.toggle_region_picker_include_drawings(), + RegionAction::Copy | RegionAction::Save | RegionAction::Both | RegionAction::Board => { + false } - crate::ui::RegionAction::Copy - | crate::ui::RegionAction::Save - | crate::ui::RegionAction::Both - | crate::ui::RegionAction::Board => false, } } @@ -252,9 +254,9 @@ impl WaylandState { } CutCommit::RejectedFullAxis => { self.input_state.push_toast( - crate::input::state::ToastPriority::Info, + ToastPriority::Info, "capture", - crate::input::state::Toast::warning(crate::i18n::tr( + Toast::warning(i18n::tr( "That cut would remove the entire remaining image.", )), ); @@ -320,9 +322,7 @@ impl WaylandState { } } - pub(in crate::backend::wayland) fn region_cut_preview_pixels( - &self, - ) -> Option<&crate::screen_pixels::PackedArgb32> { + pub(in crate::backend::wayland) fn region_cut_preview_pixels(&self) -> Option<&PackedArgb32> { self.region_capture .review_edits() .and_then(|edits| edits.ready_preview.as_ref()) @@ -338,7 +338,11 @@ impl WaylandState { let display = self.region_cut_displayed_selection()?; let output = edits.output_size()?; let band = quantized_cut(axis, display, output, drag.start, drag.current)?; - debug_assert_eq!(band.axis(), axis); + debug_assert_eq!( + band.axis(), + axis, + "a quantized cut keeps the axis of the drag that made it" + ); cut_band_display(display, output, axis, band.start(), band.end()).map(|band| (axis, band)) } diff --git a/src/backend/wayland/state/region_capture/cut_review/model.rs b/src/backend/wayland/state/region_capture/cut_review/model.rs index ee8fac60d..c66f90eaa 100644 --- a/src/backend/wayland/state/region_capture/cut_review/model.rs +++ b/src/backend/wayland/state/region_capture/cut_review/model.rs @@ -1,7 +1,10 @@ +use std::sync::Arc; + use crate::backend::wayland::state::screen_image::ScreenSourceToken; +use crate::canvas_export::SpotlightPassSnapshot; use crate::capture::{CutAxis, CutBand}; use crate::input::state::{RegionInputSource, RegionSelection}; -use crate::screen_pixels::ImagePixelRect; +use crate::screen_pixels::{ImagePixelRect, PackedArgb32}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(in crate::backend::wayland::state::region_capture) enum PreviewApply { @@ -40,8 +43,7 @@ pub(in crate::backend::wayland::state::region_capture) struct RegionAnnotatedRen pub(in crate::backend::wayland::state::region_capture) canvas_content_generation: u64, pub(in crate::backend::wayland::state::region_capture) board_view_offset: (f64, f64), pub(in crate::backend::wayland::state::region_capture) text_halo_enabled: bool, - pub(in crate::backend::wayland::state::region_capture) spotlight: - crate::canvas_export::SpotlightPassSnapshot, + pub(in crate::backend::wayland::state::region_capture) spotlight: SpotlightPassSnapshot, } #[derive(Debug, Clone, PartialEq)] @@ -87,16 +89,14 @@ pub(in crate::backend::wayland) struct CutPreviewKey { #[derive(Debug, Clone)] pub(in crate::backend::wayland) struct RegionCutPreview { pub(in crate::backend::wayland::state::region_capture) key: CutPreviewKey, - pub(in crate::backend::wayland::state::region_capture) pixels: - std::sync::Arc, + pub(in crate::backend::wayland::state::region_capture) pixels: Arc, pub(in crate::backend::wayland::state::region_capture) display: RegionSelection, } #[derive(Debug, Clone)] pub(in crate::backend::wayland) struct RegionCutBase { pub(in crate::backend::wayland::state::region_capture) fingerprint: RegionRenderFingerprint, - pub(in crate::backend::wayland::state::region_capture) pixels: - std::sync::Arc, + pub(in crate::backend::wayland::state::region_capture) pixels: Arc, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/backend/wayland/state/region_capture/cut_review/tests.rs b/src/backend/wayland/state/region_capture/cut_review/tests.rs index 802bae139..dff10992b 100644 --- a/src/backend/wayland/state/region_capture/cut_review/tests.rs +++ b/src/backend/wayland/state/region_capture/cut_review/tests.rs @@ -1,11 +1,15 @@ +use std::sync::Arc; + +use crate::backend::wayland::state::region_capture; +use crate::canvas_export::CanvasExportRect; +use crate::screen_pixels::PackedArgb32; + use super::actions::apply_cut_history_change; use super::geometry::{dominant_cut_axis, logical_to_output_point, output_display_for}; use super::model::{CutCommit, CutMode}; use super::*; -use crate::backend::wayland::state::screen_image::ScreenImageKind; -use crate::backend::wayland::state::screen_image::ScreenSourceToken; -use crate::capture::CutAxis; -use crate::capture::CutBand; +use crate::backend::wayland::state::screen_image::{ScreenImageKind, ScreenSourceToken}; +use crate::capture::{CutAxis, CutBand}; use crate::input::state::{RegionInputSource, RegionSelection}; use crate::screen_pixels::ImagePixelRect; use wayland_client::protocol::wl_output::Transform; @@ -159,7 +163,7 @@ fn undo_redo_and_new_commit_clear_redo() { let fingerprint = fingerprint(edits.source_rect); edits .cuts - .push(CutBand::new(CutAxis::Columns, 1, 2).unwrap()); + .push(CutBand::try_new(CutAxis::Columns, 1, 2).unwrap()); edits.revision = 1; assert!(edits.undo(fingerprint.clone())); assert!(edits.cuts.is_empty()); @@ -188,15 +192,13 @@ fn undo_and_redo_abandon_an_in_flight_drag() { let fingerprint = fingerprint(edits.source_rect); edits .cuts - .push(CutBand::new(CutAxis::Columns, 1, 2).unwrap()); + .push(CutBand::try_new(CutAxis::Columns, 1, 2).unwrap()); edits.revision = 1; edits.set_desired_from(fingerprint.clone()); let desired = edits.desired_preview.clone().unwrap(); edits.ready_preview = Some(RegionCutPreview { key: desired, - pixels: std::sync::Arc::new( - crate::screen_pixels::PackedArgb32::new(7, 8, 28, vec![0; 28 * 8]).unwrap(), - ), + pixels: Arc::new(PackedArgb32::new(7, 8, 28, vec![0; 28 * 8]).unwrap()), display: display(), }); edits.toggle_mode(); @@ -241,15 +243,13 @@ fn edits_with_current_preview() -> RegionReviewEdits { let fingerprint = fingerprint(edits.source_rect); edits .cuts - .push(CutBand::new(CutAxis::Columns, 1, 2).unwrap()); + .push(CutBand::try_new(CutAxis::Columns, 1, 2).unwrap()); edits.revision = 1; edits.set_desired_from(fingerprint); let desired = edits.desired_preview.clone().unwrap(); edits.ready_preview = Some(RegionCutPreview { key: desired, - pixels: std::sync::Arc::new( - crate::screen_pixels::PackedArgb32::new(7, 8, 28, vec![0; 28 * 8]).unwrap(), - ), + pixels: Arc::new(PackedArgb32::new(7, 8, 28, vec![0; 28 * 8]).unwrap()), display: display(), }); edits @@ -308,7 +308,7 @@ fn toggling_cut_mode_off_during_a_drag_returns_the_owner() { #[test] fn revision_exhaustion_leaves_reset_and_invalidate_untouched() { let mut edits = edits(); - let cut = CutBand::new(CutAxis::Columns, 1, 2).unwrap(); + let cut = CutBand::try_new(CutAxis::Columns, 1, 2).unwrap(); edits.cuts.push(cut); edits.revision = u64::MAX; edits.failed_revision = Some(u64::MAX); @@ -329,7 +329,7 @@ fn undoing_the_last_cut_unlocks_the_crop() { let fingerprint = fingerprint(edits.source_rect); edits .cuts - .push(CutBand::new(CutAxis::Columns, 1, 2).unwrap()); + .push(CutBand::try_new(CutAxis::Columns, 1, 2).unwrap()); edits.revision = 1; assert!(edits.crop_locked()); assert!(edits.undo(fingerprint)); @@ -342,7 +342,7 @@ fn a_failed_current_preview_stays_failed_until_the_revision_changes() { let desired = CutPreviewKey { fingerprint: fingerprint(edits.source_rect), revision: 3, - cuts: vec![CutBand::new(CutAxis::Columns, 1, 2).unwrap()], + cuts: vec![CutBand::try_new(CutAxis::Columns, 1, 2).unwrap()], }; edits.cuts = desired.cuts.clone(); edits.revision = 3; @@ -361,10 +361,12 @@ fn a_failed_current_preview_stays_failed_until_the_revision_changes() { #[test] fn reset_clears_history_and_unlocks_the_crop() { let mut edits = edits(); - edits.cuts.push(CutBand::new(CutAxis::Rows, 1, 2).unwrap()); + edits + .cuts + .push(CutBand::try_new(CutAxis::Rows, 1, 2).unwrap()); edits .redo - .push(CutBand::new(CutAxis::Columns, 1, 2).unwrap()); + .push(CutBand::try_new(CutAxis::Columns, 1, 2).unwrap()); edits.mode = CutMode::Armed; assert!(edits.reset()); assert!(edits.cuts.is_empty()); @@ -380,7 +382,7 @@ fn source_rect_cannot_change_while_cuts_exist() { assert!(edits.set_source_rect(next)); edits .cuts - .push(CutBand::new(CutAxis::Columns, 1, 2).unwrap()); + .push(CutBand::try_new(CutAxis::Columns, 1, 2).unwrap()); assert!(!edits.set_source_rect(ImagePixelRect::new(0, 0, 4, 4, (8, 8)).unwrap())); assert_eq!(edits.source_rect, next); } @@ -391,7 +393,7 @@ fn cut_start_is_rejected_while_preview_is_pending() { edits.toggle_mode(); edits .cuts - .push(CutBand::new(CutAxis::Columns, 1, 2).unwrap()); + .push(CutBand::try_new(CutAxis::Columns, 1, 2).unwrap()); edits.revision = 1; edits.set_desired_from(fingerprint(edits.source_rect)); assert!(!edits.preview_is_current()); @@ -407,7 +409,7 @@ fn loupe_is_suppressed_when_armed_or_cuts_exist() { edits.toggle_mode(); edits .cuts - .push(CutBand::new(CutAxis::Columns, 1, 2).unwrap()); + .push(CutBand::try_new(CutAxis::Columns, 1, 2).unwrap()); assert!(edits.loupe_suppressed()); } @@ -419,7 +421,7 @@ fn column_cut_preserves_top_left_and_height() { let cut = output_display_for( &token, rect, - &[CutBand::new(CutAxis::Columns, 2, 4).unwrap()], + &[CutBand::try_new(CutAxis::Columns, 2, 4).unwrap()], ) .unwrap(); assert_eq!(cut.start, full.start); @@ -450,13 +452,8 @@ fn pointer_edges_map_to_the_inclusive_pixel_edge_domain() { #[test] fn composed_board_origin_stays_put_while_size_contracts() { - let source = crate::canvas_export::CanvasExportRect::new(10.0, 20.0, 80.0, 40.0).unwrap(); - let composed = crate::backend::wayland::state::region_capture::world_rect_for_composed_region( - source, - (8, 8), - (6, 4), - ) - .unwrap(); + let source = CanvasExportRect::new(10.0, 20.0, 80.0, 40.0).unwrap(); + let composed = region_capture::world_rect_for_composed_region(source, (8, 8), (6, 4)).unwrap(); assert_eq!(composed.x, 10.0); assert_eq!(composed.y, 20.0); assert_eq!(composed.width, 60.0); diff --git a/src/backend/wayland/state/region_capture/delivery.rs b/src/backend/wayland/state/region_capture/delivery.rs index aca38b820..c2e4d4895 100644 --- a/src/backend/wayland/state/region_capture/delivery.rs +++ b/src/backend/wayland/state/region_capture/delivery.rs @@ -1,10 +1,12 @@ +use crate::backend::wayland::capture::RegionCapturePhase; use crate::capture::{ - CaptureDestination, ImageFormatMetadata, ImageOperationKind, RenderImageRequest, - RenderedImageDeliveryRequest, output_size as band_cut_output_size, + CaptureDestination, CaptureError, CutBand, ImageFormatMetadata, ImageOperationKind, + ImageRenderJob, RenderImageRequest, RenderedImageDeliveryRequest, + output_size as band_cut_output_size, }; use crate::input::state::{BoardPasteTarget, RegionPurposeTag, Toast, ToastPriority}; use crate::screen_pixels::{EmbeddedImageLimits, ImagePixelRect}; -use crate::ui::RegionAction; +use crate::ui::{RegionAction, RegionActionBar}; use super::super::capture::should_exit_after_capture; use super::render::{RegionRenderRequest, region_render_job}; @@ -15,7 +17,7 @@ use crate::i18n::tr; const TOAST_SOURCE: &str = "capture"; pub(super) fn region_delivery_request( - render: crate::capture::ImageRenderJob, + render: ImageRenderJob, intent: &RegionCaptureIntent, destination: CaptureDestination, ) -> RenderedImageDeliveryRequest { @@ -66,7 +68,7 @@ impl WaylandState { return None; } let selection = self.region_selection_geometry()?.display_selection(); - crate::ui::RegionActionBar::place(selection, (self.surface.width(), self.surface.height())) + RegionActionBar::place(selection, (self.surface.width(), self.surface.height())) .enabled_hit(point, self.region_cut_availability()) } @@ -98,7 +100,7 @@ impl WaylandState { return false; } self.region_selection_geometry().is_some_and(|geometry| { - crate::ui::RegionActionBar::place( + RegionActionBar::place( geometry.display_selection(), (self.surface.width(), self.surface.height()), ) @@ -162,7 +164,7 @@ impl WaylandState { true } - fn preview_permits_submit(&self, cuts: &[crate::capture::CutBand]) -> bool { + fn preview_permits_submit(&self, cuts: &[CutBand]) -> bool { cuts.is_empty() || self .region_capture @@ -238,9 +240,7 @@ impl WaylandState { pub(in crate::backend::wayland) fn submit_region_capture(&mut self, rect: ImagePixelRect) { let destination = match self.capture.region_phase() { - crate::backend::wayland::capture::RegionCapturePhase::Reserved(intent) => { - intent.destination() - } + RegionCapturePhase::Reserved(intent) => intent.destination(), _ => return, }; self.submit_region_capture_with(rect, Vec::new(), RegionSubmit::Deliver(destination)); @@ -249,7 +249,7 @@ impl WaylandState { fn submit_region_capture_with( &mut self, rect: ImagePixelRect, - cuts: Vec, + cuts: Vec, submit: RegionSubmit, ) { let Some(ActiveScreenRegion::Ready { @@ -267,7 +267,7 @@ impl WaylandState { } let snapshot = match self.snapshot_region_render(rect, include_drawings) { Ok(snapshot) => snapshot, - Err(crate::capture::CaptureError::Cancelled(_)) => { + Err(CaptureError::Cancelled(_)) => { self.cancel_region_capture_for_source_change(); return; } diff --git a/src/backend/wayland/state/region_capture/events.rs b/src/backend/wayland/state/region_capture/events.rs index c5974c9dd..a33520e84 100644 --- a/src/backend/wayland/state/region_capture/events.rs +++ b/src/backend/wayland/state/region_capture/events.rs @@ -1,4 +1,12 @@ -use super::*; +use crate::input::InputState; +use crate::ui::RegionResizeHandles; + +use super::active_state::{ActiveScreenRegion, RegionInteractionPhase}; +use super::cut_review::RegionReviewEdits; +use super::geometry::RegionSelectionGeometry; +use super::measure::{RegionOwnerLoss, RegionSelectionFinalize}; +use crate::input::SelectionHandle; +use crate::input::state::{RegionInputSource, RegionPurposeTag}; /// The grip under `logical`, placed from the rectangle Review is showing. The /// renderer places its chips from the same geometry, so what is painted and @@ -10,15 +18,13 @@ pub(super) fn review_resize_handle_at( let selection = region .review_geometry() .map(RegionSelectionGeometry::display_selection)?; - crate::ui::RegionResizeHandles::place(selection).hit(logical) + RegionResizeHandles::place(selection).hit(logical) } -fn sync_projection( - backend: &Option, - input_state: &mut crate::input::InputState, -) { +fn sync_projection(backend: Option<&ActiveScreenRegion>, input_state: &mut InputState) { input_state.sync_region_projection( backend + .copied() .map(ActiveScreenRegion::ui_state) .unwrap_or_default(), ); @@ -26,7 +32,7 @@ fn sync_projection( pub(super) fn begin_region_selection_event( backend: &mut Option, - input_state: &mut crate::input::InputState, + input_state: &mut InputState, owner: RegionInputSource, logical: (f64, f64), ) -> bool { @@ -44,12 +50,12 @@ pub(super) fn begin_region_selection_event( && region.begin_review_resize(handle, logical) { region.set_phase(RegionInteractionPhase::Review { owner: Some(owner) }); - sync_projection(backend, input_state); + sync_projection(backend.as_ref(), input_state); return true; } if region.begin_review_move(logical) { region.set_phase(RegionInteractionPhase::Review { owner: Some(owner) }); - sync_projection(backend, input_state); + sync_projection(backend.as_ref(), input_state); return true; } region.reset_review_for_selection(); @@ -59,13 +65,13 @@ pub(super) fn begin_region_selection_event( return false; } region.set_phase(RegionInteractionPhase::Selecting { owner }); - sync_projection(backend, input_state); + sync_projection(backend.as_ref(), input_state); true } pub(super) fn update_region_selection_event( backend: &mut Option, - input_state: &mut crate::input::InputState, + input_state: &mut InputState, owner: RegionInputSource, logical: (f64, f64), ) { @@ -106,7 +112,7 @@ pub(super) fn update_region_selection_event( region.update_endpoint(logical) }; if changed { - sync_projection(backend, input_state); + sync_projection(backend.as_ref(), input_state); } } @@ -125,7 +131,7 @@ fn finish_review_drag(region: &mut ActiveScreenRegion, owner: RegionInputSource) pub(super) fn sync_region_square_modifier_event( backend: &mut Option, - input_state: &mut crate::input::InputState, + input_state: &mut InputState, shift: bool, ) -> bool { let Some(region) = backend.as_mut() else { @@ -134,7 +140,7 @@ pub(super) fn sync_region_square_modifier_event( if !region.set_square_modifier(shift) { return false; } - sync_projection(backend, input_state); + sync_projection(backend.as_ref(), input_state); true } @@ -144,7 +150,7 @@ pub(super) fn initial_square_modifier(purpose: RegionPurposeTag, shift: bool) -> pub(super) fn rearm_region_selection_event( backend: &mut Option, - input_state: &mut crate::input::InputState, + input_state: &mut InputState, ) { if let Some(ActiveScreenRegion::Ready { anchor, @@ -174,12 +180,12 @@ pub(super) fn rearm_region_selection_event( *edge = None; *phase = RegionInteractionPhase::Armed; } - sync_projection(backend, input_state); + sync_projection(backend.as_ref(), input_state); } pub(super) fn region_owner_lost_event( backend: &mut Option, - input_state: &mut crate::input::InputState, + input_state: &mut InputState, source: RegionInputSource, ) -> RegionOwnerLoss { let Some(region) = backend.as_mut() else { @@ -198,8 +204,11 @@ pub(super) fn region_owner_lost_event( } if matches!(region.phase(), Some(RegionInteractionPhase::Review { .. })) { let finished = finish_review_drag(region, source); - debug_assert!(finished); - sync_projection(backend, input_state); + debug_assert!( + finished, + "a review owner always holds a resize or move drag that can finish" + ); + sync_projection(backend.as_ref(), input_state); return RegionOwnerLoss::Rearmed; } rearm_region_selection_event(backend, input_state); @@ -213,7 +222,7 @@ enum SelectionFinalization { fn finalize_region_selection( backend: &mut Option, - input_state: &mut crate::input::InputState, + input_state: &mut InputState, owner: RegionInputSource, logical: (f64, f64), ) -> SelectionFinalization { @@ -227,7 +236,7 @@ fn finalize_region_selection( region.update_endpoint(logical); if region.measure_selection().is_some() { region.set_phase(RegionInteractionPhase::Measured); - sync_projection(backend, input_state); + sync_projection(backend.as_ref(), input_state); return SelectionFinalization::Complete(RegionSelectionFinalize::Measured); } return SelectionFinalization::Complete(RegionSelectionFinalize::NotOwned); @@ -236,7 +245,7 @@ fn finalize_region_selection( let changed = region.update_review_resize(logical) || region.update_review_move(logical); let finished = finish_review_drag(region, owner); if changed || finished { - sync_projection(backend, input_state); + sync_projection(backend.as_ref(), input_state); } return SelectionFinalization::Complete(if finished { RegionSelectionFinalize::Reviewed @@ -252,7 +261,7 @@ fn finalize_region_selection( input_state.needs_redraw = true; } region.update_endpoint(logical); - sync_projection(backend, input_state); + sync_projection(backend.as_ref(), input_state); let Some(rect) = backend.as_ref().and_then(|region| region.selection_rect()) else { rearm_region_selection_event(backend, input_state); return SelectionFinalization::Complete(RegionSelectionFinalize::Rearmed); @@ -266,7 +275,7 @@ fn finalize_region_selection( return SelectionFinalization::Complete(RegionSelectionFinalize::NotOwned); }; region.set_phase(RegionInteractionPhase::Review { owner: None }); - sync_projection(backend, input_state); + sync_projection(backend.as_ref(), input_state); return SelectionFinalization::Review(seed); } SelectionFinalization::Complete(RegionSelectionFinalize::Selected { purpose, rect }) @@ -274,7 +283,7 @@ fn finalize_region_selection( pub(in crate::backend::wayland::state) fn finalize_region_selection_with_review_edits( backend: &mut Option, - input_state: &mut crate::input::InputState, + input_state: &mut InputState, review_edits: &mut Option, owner: RegionInputSource, logical: (f64, f64), diff --git a/src/backend/wayland/state/region_capture/geometry.rs b/src/backend/wayland/state/region_capture/geometry.rs index 9b7b61a69..f16d141bd 100644 --- a/src/backend/wayland/state/region_capture/geometry.rs +++ b/src/backend/wayland/state/region_capture/geometry.rs @@ -27,7 +27,11 @@ impl RegionSelectionGeometry { rect: ImagePixelRect, display_selection: RegionSelection, ) -> Self { - debug_assert_eq!(purpose, RegionPurposeTag::CaptureInteractive); + debug_assert_eq!( + purpose, + RegionPurposeTag::CaptureInteractive, + "only interactive captures have a review phase" + ); Self::authoritative(purpose, rect, display_selection) } @@ -36,7 +40,10 @@ impl RegionSelectionGeometry { rect: ImagePixelRect, display_selection: RegionSelection, ) -> Self { - debug_assert!(purpose.is_capture()); + debug_assert!( + purpose.is_capture(), + "authoritative geometry belongs to capture regions, got {purpose:?}" + ); let image_span = pixel_span( ImagePoint::new(f64::from(rect.x()), f64::from(rect.y())), ImagePoint::new( diff --git a/src/backend/wayland/state/region_capture/intent.rs b/src/backend/wayland/state/region_capture/intent.rs index b5e81a810..572c62cd3 100644 --- a/src/backend/wayland/state/region_capture/intent.rs +++ b/src/backend/wayland/state/region_capture/intent.rs @@ -1,9 +1,8 @@ -use crate::{ - backend::ExitAfterCaptureMode, - capture::{CaptureDestination, file::FileSaveConfig}, - config::Action, - input::state::RegionPurposeTag, -}; +use crate::backend::ExitAfterCaptureMode; +use crate::capture::CaptureDestination; +use crate::capture::file::FileSaveConfig; +use crate::config::Action; +use crate::input::state::RegionPurposeTag; /// Runtime picker settings captured when a region action is accepted. /// diff --git a/src/backend/wayland/state/region_capture/measure.rs b/src/backend/wayland/state/region_capture/measure.rs index 679583478..240121dce 100644 --- a/src/backend/wayland/state/region_capture/measure.rs +++ b/src/backend/wayland/state/region_capture/measure.rs @@ -1,4 +1,5 @@ -use super::*; +use crate::input::state::RegionPurposeTag; +use crate::screen_pixels::ImagePixelRect; pub(super) fn measure_anchor(point: (f64, f64), bounds: (u32, u32)) -> Option<(f64, f64)> { if !point.0.is_finite() || !point.1.is_finite() || bounds.0 == 0 || bounds.1 == 0 { diff --git a/src/backend/wayland/state/region_capture/picker.rs b/src/backend/wayland/state/region_capture/picker.rs index 9b49db443..b97ba5e6f 100644 --- a/src/backend/wayland/state/region_capture/picker.rs +++ b/src/backend/wayland/state/region_capture/picker.rs @@ -1,14 +1,13 @@ use crate::backend::wayland::acquisition::ScreenAcquisitionOwner; +use crate::backend::wayland::capture::{CapturePreflightRequest, RegionCapturePhase}; use crate::backend::wayland::zoom::ZoomWaiterOwner; -use crate::capture::{ - CaptureDestination, CaptureType, - file::{FileSaveConfig, expand_tilde}, -}; +use crate::capture::file::{FileSaveConfig, expand_tilde}; +use crate::capture::{CaptureDestination, CaptureRequest, CaptureType}; use crate::config::{Action, RegionPicker}; use crate::input::state::{RegionPurposeTag, ScreenCaptureSource, Toast, ToastPriority}; use super::super::capture::{overlay_suppression_for_screenshot, should_exit_after_capture}; -use super::super::screen_image::displayed_screen_image; +use super::super::screen_image::{ScreenSourceConditions, displayed_screen_image}; use super::{ActiveScreenRegion, FreezeOwnership, RegionCaptureIntent, RegionPickerOptions}; use crate::backend::wayland::state::WaylandState; use crate::i18n::tr; @@ -25,13 +24,15 @@ pub(super) enum RegionPickerEntry { ZoomImageUnavailable, } -pub(super) fn region_picker_entry( - has_source: bool, - board_is_transparent: bool, - zoom_engaged: bool, - zoom_active: bool, - frozen_enabled: bool, -) -> RegionPickerEntry { +pub(super) fn region_picker_entry(conditions: ScreenSourceConditions) -> RegionPickerEntry { + let ScreenSourceConditions { + has_source, + board_is_transparent, + zoom_engaged, + zoom_active, + frozen_enabled, + } = conditions; + if has_source { RegionPickerEntry::Activate } else if !board_is_transparent && zoom_engaged { @@ -66,10 +67,8 @@ pub(super) fn region_destination( } } -pub(super) fn legacy_region_request( - intent: &RegionCaptureIntent, -) -> crate::capture::CaptureRequest { - crate::capture::CaptureRequest { +pub(super) fn legacy_region_request(intent: &RegionCaptureIntent) -> CaptureRequest { + CaptureRequest { capture_type: CaptureType::Selection { x: 0, y: 0, @@ -181,13 +180,13 @@ impl WaylandState { self.input_state.board_is_transparent(), ) .is_some(); - match region_picker_entry( + match region_picker_entry(ScreenSourceConditions { has_source, - self.input_state.board_is_transparent(), - self.zoom.is_engaged(), - self.zoom.active, - self.frozen.enabled(), - ) { + board_is_transparent: self.input_state.board_is_transparent(), + zoom_engaged: self.zoom.is_engaged(), + zoom_active: self.zoom.active, + frozen_enabled: self.frozen.enabled(), + }) { RegionPickerEntry::Activate => { if !self.activate_screen_region( purpose, @@ -294,12 +293,10 @@ impl WaylandState { FreezeOwnership::PreExisting }; let include_drawings = match self.capture.region_phase() { - crate::backend::wayland::capture::RegionCapturePhase::Reserved(intent) => { - intent.include_drawings() - } - crate::backend::wayland::capture::RegionCapturePhase::Idle - | crate::backend::wayland::capture::RegionCapturePhase::Submitting(_) - | crate::backend::wayland::capture::RegionCapturePhase::Accepted => return false, + RegionCapturePhase::Reserved(intent) => intent.include_drawings(), + RegionCapturePhase::Idle + | RegionCapturePhase::Submitting(_) + | RegionCapturePhase::Accepted => return false, }; self.activate_screen_region( region.purpose(), @@ -366,7 +363,11 @@ impl WaylandState { self.capture.finish_capture_lifecycle(); return; }; - debug_assert!(intent.purpose().is_capture()); + debug_assert!( + intent.purpose().is_capture(), + "a reserved region intent is always a capture, got {:?}", + intent.purpose() + ); self.capture.set_exit_on_success(should_exit_after_capture( intent.exit_mode(), intent.destination(), @@ -392,9 +393,8 @@ impl WaylandState { return; } let request = legacy_region_request(&intent); - self.capture.queue_preflight( - crate::backend::wayland::capture::CapturePreflightRequest::Screenshot(request), - ); + self.capture + .queue_preflight(CapturePreflightRequest::Screenshot(request)); } pub(super) fn cancel_region_capture_for_source_change(&mut self) { diff --git a/src/backend/wayland/state/region_capture/render.rs b/src/backend/wayland/state/region_capture/render.rs index 9f634c480..928946f59 100644 --- a/src/backend/wayland/state/region_capture/render.rs +++ b/src/backend/wayland/state/region_capture/render.rs @@ -1,7 +1,11 @@ use std::sync::Arc; use crate::canvas_export::{CanvasRegionExportSnapshot, render_canvas_region_pixels}; -use crate::capture::{CaptureError, CutBand, apply_band_cuts, output_size as band_cut_output_size}; +use crate::capture; +use crate::capture::{ + BandCutError, CaptureError, CutBand, ImageRenderJob, apply_band_cuts, + output_size as band_cut_output_size, +}; use crate::screen_pixels::{ImagePixelRect, PackedArgb32, ScreenImage}; /// Immutable pixels a region render job may flatten, then cut. @@ -81,11 +85,15 @@ pub(super) fn render_region_pixels( /// The PNG job for a submission. Shared by every destination, so what Copy /// writes and what Board pastes can only differ by the Review toggle and cuts. -pub(super) fn region_render_job(request: RegionRenderRequest) -> crate::capture::ImageRenderJob { - Box::new(move || crate::capture::png::encode_packed_argb32_png(&render_region_pixels(request)?)) +pub(super) fn region_render_job(request: RegionRenderRequest) -> ImageRenderJob { + Box::new(move || capture::png::encode_packed_argb32_png(&render_region_pixels(request)?)) } -fn band_cut_error(error: crate::capture::BandCutError) -> CaptureError { +#[expect( + clippy::needless_pass_by_value, + reason = "passed to `Result::map_err`, which hands over the error by value" +)] +fn band_cut_error(error: BandCutError) -> CaptureError { CaptureError::ImageError(format!("Could not apply the requested cut: {error}")) } @@ -98,12 +106,12 @@ mod tests { fn swatch() -> ([u32; 6], Vec) { let ids = [ - 0xFF11_2233, - 0xFF44_5566, - 0xFF77_8899, - 0xFFAA_BBCC, - 0xFFDD_EEFF, - 0xFF01_0203, + 0xff11_2233, + 0xff44_5566, + 0xff77_8899, + 0xffaa_bbcc, + 0xffdd_eeff, + 0xff01_0203, ]; let bytes = ids .iter() @@ -137,13 +145,13 @@ mod tests { fn raw_and_annotated_jobs_share_the_cut_compose_path() { let (ids, bytes) = swatch(); let image = Arc::new(ScreenImage { - data: bytes.clone(), + data: bytes, width: 3, height: 2, stride: 12, }); let selection = ImagePixelRect::new(0, 0, 3, 2, (3, 2)).unwrap(); - let cut = CutBand::new(CutAxis::Columns, 1, 2).unwrap(); + let cut = CutBand::try_new(CutAxis::Columns, 1, 2).unwrap(); let rendered = region_render_job(RegionRenderRequest { source: RegionPixelSource::Raw { image: Arc::clone(&image), diff --git a/src/backend/wayland/state/region_capture/review_state.rs b/src/backend/wayland/state/region_capture/review_state.rs index 707bb2472..60c72633c 100644 --- a/src/backend/wayland/state/region_capture/review_state.rs +++ b/src/backend/wayland/state/region_capture/review_state.rs @@ -1,4 +1,11 @@ -use super::*; +use crate::backend::wayland::state::screen_image; + +use super::super::screen_image::{ScreenSourceToken, image_point_for_screen_point}; +use super::active_state::{ActiveScreenRegion, RegionInteractionPhase}; +use super::cut_review::RegionReviewEdits; +use crate::input::SelectionHandle; +use crate::input::state::{RegionPurposeTag, RegionSelection}; +use crate::screen_pixels::{ImagePixelRect, ImagePoint}; pub(super) struct InteractiveReviewSeed { pub(super) generation: u64, @@ -98,9 +105,7 @@ impl ActiveScreenRegion { if logical_anchor.is_some() || review_resize.is_some() { return false; } - let Some(display) = - crate::backend::wayland::state::screen_image::screen_rect_for_image_rect(source, rect) - else { + let Some(display) = screen_image::screen_rect_for_image_rect(source, rect) else { return false; }; let x = logical.0.floor() as i32; diff --git a/src/backend/wayland/state/region_capture/runtime.rs b/src/backend/wayland/state/region_capture/runtime.rs index 9eb1e4a6b..e7744bb2b 100644 --- a/src/backend/wayland/state/region_capture/runtime.rs +++ b/src/backend/wayland/state/region_capture/runtime.rs @@ -1,26 +1,53 @@ -use crate::backend::wayland::{RuntimeOperationController, RuntimeOperationIdSource}; - -use super::*; +use crate::backend::wayland::state::screen_image; +use crate::backend::wayland::{ + RuntimeOperationController, RuntimeOperationIdSource, RuntimeWakeHandle, +}; +use crate::capture::window_geometry::{WindowGeometryError, WindowQueryResult}; +use crate::i18n; +use crate::input::InputState; +use crate::input::state::{Toast, ToastPriority}; + +use super::super::WaylandState; +use super::super::screen_image::{ + ScreenSourceToken, current_screen_source_token, screen_source_is, +}; +use super::active_state::{ActiveScreenRegion, FreezeOwnership, RegionInteractionPhase}; +use super::cut_preview::CutPreviewOutcome; +use super::cut_review::{CutPreviewKey, RegionReviewEdits}; +use super::events::{ + begin_region_selection_event, finalize_region_selection_with_review_edits, + initial_square_modifier, region_owner_lost_event, review_resize_handle_at, + sync_region_square_modifier_event, update_region_selection_event, +}; +use super::geometry::{RegionPickerMeasurement, RegionSelectionGeometry}; +use super::measure::{ + MeasureModeTransition, RegionOwnerLoss, RegionSelectionFinalize, measure_mode_transition, +}; +use super::source_guard::{ + active_eyedropper_source_changed, active_region_source_changed, screen_region_invariant, +}; +use super::window_snap::{WindowSnapQuery, WindowSnapSession}; +use crate::backend::wayland::acquisition::ScreenAcquisitionId; +use crate::input::SelectionHandle; +use crate::input::state::{ + RegionInputSource, RegionPurposeTag, RegionSelectUiState, RegionSelection, +}; +use crate::screen_pixels::ImagePixelRect; pub(in crate::backend::wayland) struct RegionCaptureRuntime { active: Option, window_snap: Option, review_edits: Option, next_generation: u64, - window_query: RuntimeOperationController< - WindowSnapQuery, - Result< - crate::capture::window_geometry::WindowQueryResult, - crate::capture::window_geometry::WindowGeometryError, - >, - >, + window_query: + RuntimeOperationController>, cut_preview: RuntimeOperationController, } impl RegionCaptureRuntime { pub(in crate::backend::wayland) fn new( ids: RuntimeOperationIdSource, - wake: crate::backend::wayland::RuntimeWakeHandle, + wake: RuntimeWakeHandle, ) -> Self { Self { active: None, @@ -142,7 +169,7 @@ impl RegionCaptureRuntime { pub(in crate::backend::wayland::state) fn finalize_selection( &mut self, - input_state: &mut crate::input::InputState, + input_state: &mut InputState, owner: RegionInputSource, logical: (f64, f64), ) -> RegionSelectionFinalize { @@ -163,14 +190,14 @@ impl RegionCaptureRuntime { pub(in crate::backend::wayland::state) fn sync_input_projection( &self, - input_state: &mut crate::input::InputState, + input_state: &mut InputState, ) { input_state.sync_region_projection(self.ui_state()); } pub(in crate::backend::wayland::state) fn begin_review_aux_drag( &mut self, - input_state: &mut crate::input::InputState, + input_state: &mut InputState, owner: RegionInputSource, ) -> bool { let Some(region) = self.active.as_mut() else { @@ -186,7 +213,7 @@ impl RegionCaptureRuntime { pub(in crate::backend::wayland::state) fn finish_review_aux_drag( &mut self, - input_state: &mut crate::input::InputState, + input_state: &mut InputState, owner: RegionInputSource, ) -> bool { let Some(region) = self.active.as_mut() else { @@ -226,10 +253,7 @@ impl RegionCaptureRuntime { ) -> ( &mut RuntimeOperationController< WindowSnapQuery, - Result< - crate::capture::window_geometry::WindowQueryResult, - crate::capture::window_geometry::WindowGeometryError, - >, + Result, >, &mut Option, ) { @@ -272,9 +296,9 @@ impl WaylandState { } MeasureModeTransition::Refuse => { self.input_state.push_toast( - crate::input::state::ToastPriority::Info, + ToastPriority::Info, "measure.refused", - crate::input::state::Toast::info(crate::i18n::tr( + Toast::info(i18n::tr( "Finish or cancel the current screen selection first.", )), ); @@ -372,7 +396,7 @@ impl WaylandState { freeze_ownership: FreezeOwnership, include_drawings: bool, ) -> bool { - let Some(source) = crate::backend::wayland::state::screen_image::displayed_screen_image( + let Some(source) = screen_image::displayed_screen_image( &self.zoom, &self.frozen, self.input_state.board_is_transparent(), @@ -662,20 +686,24 @@ impl WaylandState { } pub(in crate::backend::wayland::state) fn debug_assert_screen_region_invariant(&self) { - debug_assert!(screen_region_invariant( + debug_assert!( + screen_region_invariant( + self.region_capture.active(), + self.input_state.region_state(), + ), + "backend region {:?} and input region UI {:?} have diverged", self.region_capture.active(), - self.input_state.region_state(), - )); + self.input_state.region_state() + ); } pub(in crate::backend::wayland) fn cancel_screen_modals_if_source_changed(&mut self) { let (cancel_eyedropper, cancel_region) = { - let current_source = - crate::backend::wayland::state::screen_image::displayed_screen_image( - &self.zoom, - &self.frozen, - self.input_state.board_is_transparent(), - ); + let current_source = screen_image::displayed_screen_image( + &self.zoom, + &self.frozen, + self.input_state.board_is_transparent(), + ); let source_matches = |expected| { current_source.as_ref().is_some_and(|source| { screen_source_is( @@ -735,7 +763,7 @@ mod owner_tests { use wayland_client::protocol::wl_output::Transform; fn runtime() -> RegionCaptureRuntime { - let wake = RuntimeWakeSource::new().expect("runtime wake source"); + let wake = RuntimeWakeSource::try_new().expect("runtime wake source"); RegionCaptureRuntime::new(RuntimeOperationIdSource::new(), wake.handle()) } diff --git a/src/backend/wayland/state/region_capture/selection_state.rs b/src/backend/wayland/state/region_capture/selection_state.rs index e1eb30ac7..bbef9d8ba 100644 --- a/src/backend/wayland/state/region_capture/selection_state.rs +++ b/src/backend/wayland/state/region_capture/selection_state.rs @@ -1,4 +1,8 @@ -use super::*; +use super::super::screen_image::image_point_for_screen_point; +use super::active_state::ActiveScreenRegion; +use super::geometry; +use super::measure::{measure_anchor, measure_edge}; +use crate::screen_pixels::clamp_edge; impl ActiveScreenRegion { pub(super) fn begin_selection(&mut self, logical: (f64, f64)) -> bool { diff --git a/src/backend/wayland/state/region_capture/source_guard.rs b/src/backend/wayland/state/region_capture/source_guard.rs index d8276d44d..13eaaf0a4 100644 --- a/src/backend/wayland/state/region_capture/source_guard.rs +++ b/src/backend/wayland/state/region_capture/source_guard.rs @@ -1,4 +1,6 @@ -use super::*; +use super::super::screen_image::ScreenSourceToken; +use super::active_state::ActiveScreenRegion; +use crate::input::state::RegionSelectUiState; pub(super) fn screen_region_invariant( backend: Option, diff --git a/src/backend/wayland/state/region_capture/tests/capture_selection.rs b/src/backend/wayland/state/region_capture/tests/capture_selection.rs index e6a9f890c..ec626f6b9 100644 --- a/src/backend/wayland/state/region_capture/tests/capture_selection.rs +++ b/src/backend/wayland/state/region_capture/tests/capture_selection.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::lifecycle_and_measure::ocr_region; use super::*; use crate::backend::wayland::state::region_capture::cut_review::{ @@ -93,7 +95,7 @@ fn interactive_release_enters_review_ready_for_the_first_cut_drag() { let mut input = make_test_input_state(); let mut review_edits = None; input.activate_region_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), RegionPurposeTag::CaptureInteractive, 1, ); @@ -162,7 +164,7 @@ fn reselecting_before_a_cut_replaces_the_review_edit_geometry() { let mut input = make_test_input_state(); let mut review_edits = None; input.activate_region_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), RegionPurposeTag::CaptureInteractive, 1, ); diff --git a/src/backend/wayland/state/region_capture/tests/event_characterization.rs b/src/backend/wayland/state/region_capture/tests/event_characterization.rs index c3832e7d2..567d62e54 100644 --- a/src/backend/wayland/state/region_capture/tests/event_characterization.rs +++ b/src/backend/wayland/state/region_capture/tests/event_characterization.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::capture_selection::{capture_region, interactive_region}; use super::lifecycle_and_measure::ocr_region; use super::*; @@ -28,9 +30,9 @@ fn each_purpose_keeps_its_event_geometry_and_terminal_ownership_contract() { let mut backend = Some(active_region_for(purpose)); let mut input = make_test_input_state(); if purpose == RegionPurposeTag::Measure { - input.activate_measure_mode_with(&crate::draw::TextMeasurer::default(), 1); + input.activate_measure_mode_with(&TextMeasurer::default(), 1); } else { - input.activate_region_with(&crate::draw::TextMeasurer::default(), purpose, 1); + input.activate_region_with(&TextMeasurer::default(), purpose, 1); } assert_eq!( input.region_state(), diff --git a/src/backend/wayland/state/region_capture/tests/lifecycle_and_measure.rs b/src/backend/wayland/state/region_capture/tests/lifecycle_and_measure.rs index 1a5d84d64..00d8136f2 100644 --- a/src/backend/wayland/state/region_capture/tests/lifecycle_and_measure.rs +++ b/src/backend/wayland/state/region_capture/tests/lifecycle_and_measure.rs @@ -1,3 +1,8 @@ +use wayland_client::protocol::wl_output::Transform; + +use crate::backend::wayland::state::screen_image::ScreenImageKind; +use crate::draw::TextMeasurer; + use super::capture_selection::capture_region; use super::*; @@ -17,13 +22,13 @@ fn pending_and_ready_region_state_preserve_generation_and_freeze_ownership() { source: ScreenSourceToken { output_id: 1, output_layout_generation: 0, - kind: crate::backend::wayland::state::screen_image::ScreenImageKind::Frozen, + kind: ScreenImageKind::Frozen, image_generation: 44, image_size: (100, 80), stride: 400, surface: (100, 80), output_scale: 1, - output_transform: wayland_client::protocol::wl_output::Transform::Normal, + output_transform: Transform::Normal, zoom_transformed: false, zoom_scale: 1.0, zoom_view_offset: (0.0, 0.0), @@ -55,7 +60,7 @@ fn measure_mode_owns_logical_geometry_without_a_screen_image() { phase: RegionInteractionPhase::Armed, }); let mut input = make_test_input_state(); - input.activate_measure_mode_with(&crate::draw::TextMeasurer::default(), 7); + input.activate_measure_mode_with(&TextMeasurer::default(), 7); assert!(begin_region_selection_event( &mut backend, @@ -132,7 +137,7 @@ fn lost_measure_drag_rearms_for_another_device() { phase: RegionInteractionPhase::Armed, }); let mut input = make_test_input_state(); - input.activate_measure_mode_with(&crate::draw::TextMeasurer::default(), 8); + input.activate_measure_mode_with(&TextMeasurer::default(), 8); assert!(begin_region_selection_event( &mut backend, &mut input, @@ -165,7 +170,7 @@ fn reversed_measure_drag_uses_outward_integer_edges() { phase: RegionInteractionPhase::Armed, }); let mut input = make_test_input_state(); - input.activate_measure_mode_with(&crate::draw::TextMeasurer::default(), 9); + input.activate_measure_mode_with(&TextMeasurer::default(), 9); assert!(begin_region_selection_event( &mut backend, &mut input, @@ -272,13 +277,13 @@ pub(super) fn ocr_region(scale: f64) -> ActiveScreenRegion { source: ScreenSourceToken { output_id: 1, output_layout_generation: 0, - kind: crate::backend::wayland::state::screen_image::ScreenImageKind::Frozen, + kind: ScreenImageKind::Frozen, image_generation: 1, image_size: ((100.0 * scale) as u32, (80.0 * scale) as u32), stride: (400.0 * scale) as i32, surface: (100, 80), output_scale: 1, - output_transform: wayland_client::protocol::wl_output::Transform::Normal, + output_transform: Transform::Normal, zoom_transformed: false, zoom_scale: 1.0, zoom_view_offset: (0.0, 0.0), diff --git a/src/backend/wayland/state/region_capture/tests/ocr.rs b/src/backend/wayland/state/region_capture/tests/ocr.rs index 26a3e417c..b99f57ade 100644 --- a/src/backend/wayland/state/region_capture/tests/ocr.rs +++ b/src/backend/wayland/state/region_capture/tests/ocr.rs @@ -1,3 +1,6 @@ +use crate::config::ChordModifiers; +use crate::draw::TextMeasurer; + use super::lifecycle_and_measure::ocr_region; use super::*; @@ -115,11 +118,7 @@ fn production_ocr_event_adapter_uses_release_endpoint_at_every_scale() { }; let mut backend = Some(ocr_region(scale)); let mut input = make_test_input_state(); - input.activate_region_with( - &crate::draw::TextMeasurer::default(), - RegionPurposeTag::Ocr, - 1, - ); + input.activate_region_with(&TextMeasurer::default(), RegionPurposeTag::Ocr, 1); assert!(begin_region_selection_event( &mut backend, @@ -173,12 +172,11 @@ fn production_ocr_event_adapter_uses_release_endpoint_at_every_scale() { fn production_ocr_event_adapter_rearms_small_drag_and_ignores_shift_square_policy() { let mut backend = Some(ocr_region(1.0)); let mut input = make_test_input_state(); - input.activate_region_with( - &crate::draw::TextMeasurer::default(), - RegionPurposeTag::Ocr, - 1, - ); - input.sync_modifiers(true, false, false, false); + input.activate_region_with(&TextMeasurer::default(), RegionPurposeTag::Ocr, 1); + input.sync_modifiers(ChordModifiers { + shift: true, + ..ChordModifiers::default() + }); assert!(begin_region_selection_event( &mut backend, @@ -203,11 +201,7 @@ fn production_ocr_event_adapter_rearms_small_drag_and_ignores_shift_square_polic let mut backend = Some(ocr_region(1.0)); let mut input = make_test_input_state(); - input.activate_region_with( - &crate::draw::TextMeasurer::default(), - RegionPurposeTag::Ocr, - 1, - ); + input.activate_region_with(&TextMeasurer::default(), RegionPurposeTag::Ocr, 1); assert!(begin_region_selection_event( &mut backend, &mut input, diff --git a/src/backend/wayland/state/region_capture/tests/picker.rs b/src/backend/wayland/state/region_capture/tests/picker.rs index 0f439248f..6230d5bed 100644 --- a/src/backend/wayland/state/region_capture/tests/picker.rs +++ b/src/backend/wayland/state/region_capture/tests/picker.rs @@ -1,3 +1,12 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use crate::capture::{CutAxis, CutBand, ImageRenderJob}; +use crate::config::CaptureConfig; +use crate::draw::{Frame, Shape}; +use crate::screen_pixels::ScreenImage; +use crate::{capture, draw}; + use super::*; use crate::backend::ExitAfterCaptureMode; use crate::backend::wayland::state::region_capture::delivery::{ @@ -9,12 +18,12 @@ use crate::backend::wayland::state::region_capture::picker::{ use crate::backend::wayland::state::region_capture::render::{ RegionPixelSource, RegionRenderRequest, region_render_job, }; +use crate::backend::wayland::state::screen_image::ScreenSourceConditions; use crate::canvas_export::{ CanvasExportRect, CanvasRegionExportSnapshot, CanvasRegionSource, SpotlightPassSnapshot, }; -use crate::capture::{ - CaptureDestination, CaptureType, ImageFormatMetadata, ImageOperationKind, file::FileSaveConfig, -}; +use crate::capture::file::FileSaveConfig; +use crate::capture::{CaptureDestination, CaptureType, ImageFormatMetadata, ImageOperationKind}; use crate::config::{Action, RegionPicker}; use crate::input::state::RegionPurposeTag; use crate::screen_pixels::{ImagePixelRect, PackedArgb32}; @@ -84,12 +93,12 @@ fn the_render_job_composes_drawings_when_asked_and_stays_raw_otherwise() { // A deliberately non-uniform 3x2 crop: a first-pixel check would pass on a // flat image even if the encoder transposed or truncated it. let swatch: [u32; 6] = [ - 0xFF11_2233, - 0xFF44_5566, - 0xFF77_8899, - 0xFFAA_BBCC, - 0xFFDD_EEFF, - 0xFF01_0203, + 0xff11_2233, + 0xff44_5566, + 0xff77_8899, + 0xffaa_bbcc, + 0xffdd_eeff, + 0xff01_0203, ]; let bytes: Vec = swatch .iter() @@ -98,7 +107,7 @@ fn the_render_job_composes_drawings_when_asked_and_stays_raw_otherwise() { let rendered = region_render_job(RegionRenderRequest { source: RegionPixelSource::Raw { - image: std::sync::Arc::new(crate::screen_pixels::ScreenImage { + image: Arc::new(ScreenImage { data: bytes.clone(), width: 3, height: 2, @@ -109,7 +118,7 @@ fn the_render_job_composes_drawings_when_asked_and_stays_raw_otherwise() { cuts: Vec::new(), })() .expect("raw render"); - let direct = crate::capture::png::encode_packed_argb32_png( + let direct = capture::png::encode_packed_argb32_png( &PackedArgb32::new(3, 2, 12, bytes).expect("a 3x2 crop"), ) .expect("direct encode"); @@ -128,20 +137,20 @@ fn the_render_job_composes_drawings_when_asked_and_stays_raw_otherwise() { // The composed path renders the same selection with the board's committed // shapes over it, so the crop's pixels are no longer what comes back. - let mut frame = crate::draw::Frame::new(); - frame.add_shape(crate::draw::Shape::Rect { + let mut frame = Frame::new(); + frame.add_shape(Shape::Rect { x: 10, y: 20, w: 3, h: 2, fill: true, fill_color: None, - color: crate::draw::RED, + color: draw::RED, thick: 1.0, }); let snapshot = CanvasRegionExportSnapshot { source: CanvasRegionSource { - image: std::sync::Arc::new(crate::screen_pixels::ScreenImage { + image: Arc::new(ScreenImage { data: swatch .iter() .flat_map(|pixel| pixel.to_ne_bytes()) @@ -180,21 +189,21 @@ fn the_render_job_composes_drawings_when_asked_and_stays_raw_otherwise() { #[test] fn the_render_job_cuts_after_flattening_and_keeps_composed_bytes() { let swatch: [u32; 6] = [ - 0xFF11_2233, - 0xFF44_5566, - 0xFF77_8899, - 0xFFAA_BBCC, - 0xFFDD_EEFF, - 0xFF01_0203, + 0xff11_2233, + 0xff44_5566, + 0xff77_8899, + 0xffaa_bbcc, + 0xffdd_eeff, + 0xff01_0203, ]; let bytes: Vec = swatch .iter() .flat_map(|pixel| pixel.to_ne_bytes()) .collect(); - let cut = crate::capture::CutBand::new(crate::capture::CutAxis::Columns, 1, 2).unwrap(); + let cut = CutBand::try_new(CutAxis::Columns, 1, 2).unwrap(); let request = RegionRenderRequest { source: RegionPixelSource::Raw { - image: std::sync::Arc::new(crate::screen_pixels::ScreenImage { + image: Arc::new(ScreenImage { data: bytes, width: 3, height: 2, @@ -235,11 +244,23 @@ fn decode_png_pixels(bytes: &[u8]) -> Vec { #[test] fn native_entry_auto_freezes_solid_boards_but_refuses_solid_zoom() { assert_eq!( - region_picker_entry(false, false, false, false, true), + region_picker_entry(ScreenSourceConditions { + has_source: false, + board_is_transparent: false, + zoom_engaged: false, + zoom_active: false, + frozen_enabled: true, + }), RegionPickerEntry::AutoFreeze ); assert_eq!( - region_picker_entry(false, false, true, true, true), + region_picker_entry(ScreenSourceConditions { + has_source: false, + board_is_transparent: false, + zoom_engaged: true, + zoom_active: true, + frozen_enabled: true, + }), RegionPickerEntry::RefuseSolidZoom ); } @@ -247,26 +268,50 @@ fn native_entry_auto_freezes_solid_boards_but_refuses_solid_zoom() { #[test] fn native_entry_uses_existing_waiting_legacy_and_missing_zoom_paths() { assert_eq!( - region_picker_entry(true, false, true, true, true), + region_picker_entry(ScreenSourceConditions { + has_source: true, + board_is_transparent: false, + zoom_engaged: true, + zoom_active: true, + frozen_enabled: true, + }), RegionPickerEntry::Activate ); assert_eq!( - region_picker_entry(false, true, true, false, true), + region_picker_entry(ScreenSourceConditions { + has_source: false, + board_is_transparent: true, + zoom_engaged: true, + zoom_active: false, + frozen_enabled: true, + }), RegionPickerEntry::WaitForZoom ); assert_eq!( - region_picker_entry(false, true, false, false, false), + region_picker_entry(ScreenSourceConditions { + has_source: false, + board_is_transparent: true, + zoom_engaged: false, + zoom_active: false, + frozen_enabled: false, + }), RegionPickerEntry::Legacy ); assert_eq!( - region_picker_entry(false, true, true, true, true), + region_picker_entry(ScreenSourceConditions { + has_source: false, + board_is_transparent: true, + zoom_engaged: true, + zoom_active: true, + frozen_enabled: true, + }), RegionPickerEntry::ZoomImageUnavailable ); } #[test] fn picker_options_snapshot_is_independent_of_later_config_changes() { - let mut live = crate::config::CaptureConfig::default(); + let mut live = CaptureConfig::default(); let options = RegionPickerOptions::new( live.region.show_size_readout, live.region.show_loupe, @@ -305,7 +350,7 @@ fn native_delivery_forces_png_naming_metadata_and_bytes() { RegionPurposeTag::CaptureDeliver, CaptureDestination::FileOnly, Some(FileSaveConfig { - save_directory: std::path::PathBuf::from("/tmp/captures"), + save_directory: PathBuf::from("/tmp/captures"), filename_template: "region-{timestamp}".to_string(), format: "jpg".to_string(), }), @@ -316,8 +361,7 @@ fn native_delivery_forces_png_naming_metadata_and_bytes() { let pixels = PackedArgb32::new(1, 1, 4, 0xff33_2211_u32.to_ne_bytes().to_vec()) .expect("one native ARGB32 pixel"); - let render: crate::capture::ImageRenderJob = - Box::new(move || crate::capture::png::encode_packed_argb32_png(&pixels)); + let render: ImageRenderJob = Box::new(move || capture::png::encode_packed_argb32_png(&pixels)); let request = region_delivery_request(render, &intent, intent.destination()); assert_eq!(request.destination, CaptureDestination::FileOnly); @@ -346,7 +390,7 @@ fn legacy_handoff_preserves_the_reserved_selection_request_snapshot() { RegionPurposeTag::CaptureDeliver, CaptureDestination::FileOnly, Some(FileSaveConfig { - save_directory: std::path::PathBuf::from("/tmp/original"), + save_directory: PathBuf::from("/tmp/original"), filename_template: "shot-{timestamp}".to_string(), format: "jpg".to_string(), }), @@ -370,10 +414,7 @@ fn legacy_handoff_preserves_the_reserved_selection_request_snapshot() { let save = request .save_config .expect("file selection keeps save config"); - assert_eq!( - save.save_directory, - std::path::PathBuf::from("/tmp/original") - ); + assert_eq!(save.save_directory, PathBuf::from("/tmp/original")); assert_eq!(save.filename_template, "shot-{timestamp}"); assert_eq!( save.format, "jpg", diff --git a/src/backend/wayland/state/region_capture/tests/review.rs b/src/backend/wayland/state/region_capture/tests/review.rs index 61e536183..b982159da 100644 --- a/src/backend/wayland/state/region_capture/tests/review.rs +++ b/src/backend/wayland/state/region_capture/tests/review.rs @@ -1,3 +1,6 @@ +use crate::draw::TextMeasurer; +use crate::util::Rect; + use super::capture_selection::{capture_region, capture_region_at_scale, interactive_region}; use super::lifecycle_and_measure::ocr_region; use super::*; @@ -7,7 +10,7 @@ fn capture_hover_motion_requests_a_repaint_while_armed_and_in_review() { let mut armed_backend = Some(capture_region()); let mut armed_input = make_test_input_state(); armed_input.activate_region_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), RegionPurposeTag::CaptureDeliver, 1, ); @@ -27,7 +30,7 @@ fn capture_hover_motion_requests_a_repaint_while_armed_and_in_review() { .dirty_tracker .take_region_report(100, 80) .regions, - vec![crate::util::Rect::new(0, 0, 100, 80).unwrap()] + vec![Rect::new(0, 0, 100, 80).unwrap()] ); let mut review_region = interactive_region(); @@ -58,7 +61,7 @@ fn capture_hover_motion_requests_a_repaint_while_armed_and_in_review() { .dirty_tracker .take_region_report(100, 80) .regions, - vec![crate::util::Rect::new(0, 0, 100, 80).unwrap()] + vec![Rect::new(0, 0, 100, 80).unwrap()] ); } @@ -184,7 +187,7 @@ fn capture_finalize_is_purpose_aware_and_one_axis_empty_rearms() { let mut backend = Some(capture_region()); let mut input = make_test_input_state(); input.activate_region_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), RegionPurposeTag::CaptureDeliver, 1, ); @@ -211,7 +214,7 @@ fn capture_finalize_is_purpose_aware_and_one_axis_empty_rearms() { let mut backend = Some(capture_region()); let mut input = make_test_input_state(); input.activate_region_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), RegionPurposeTag::CaptureDeliver, 1, ); @@ -349,7 +352,7 @@ fn compositor_shift_sync_recomputes_capture_preview_without_changing_ownership() let mut backend = Some(capture_region()); let mut input = make_test_input_state(); input.activate_region_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), RegionPurposeTag::CaptureDeliver, 1, ); @@ -398,7 +401,7 @@ fn capture_owner_loss_rearms_without_releasing_backend_ownership() { let mut backend = Some(capture_region()); let mut input = make_test_input_state(); input.activate_region_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), RegionPurposeTag::CaptureDeliver, 1, ); @@ -449,11 +452,7 @@ fn capture_owner_loss_rearms_without_releasing_backend_ownership() { fn ocr_owner_loss_requests_its_existing_terminal_cancel_path() { let mut backend = Some(ocr_region(1.0)); let mut input = make_test_input_state(); - input.activate_region_with( - &crate::draw::TextMeasurer::default(), - RegionPurposeTag::Ocr, - 1, - ); + input.activate_region_with(&TextMeasurer::default(), RegionPurposeTag::Ocr, 1); assert!(begin_region_selection_event( &mut backend, &mut input, diff --git a/src/backend/wayland/state/region_capture/window_snap.rs b/src/backend/wayland/state/region_capture/window_snap.rs index 79659d76c..6b4b1cc57 100644 --- a/src/backend/wayland/state/region_capture/window_snap.rs +++ b/src/backend/wayland/state/region_capture/window_snap.rs @@ -255,7 +255,7 @@ pub(super) fn apply_window_query_completion( } let mapped: Vec<_> = targets .into_iter() - .filter_map(|target| map_window_target(correlation.source, target)) + .filter_map(|target| map_window_target(correlation.source, &target)) .collect(); if mapped.is_empty() { *session = None; @@ -285,7 +285,7 @@ fn region_selection_for_rect(rect: Rect) -> RegionSelection { } } -fn map_window_target(source: ScreenSourceToken, target: WindowTarget) -> Option { +fn map_window_target(source: ScreenSourceToken, target: &WindowTarget) -> Option { let logical_rect = target.logical_rect; let first = output_logical_to_image_point(source, (logical_rect.x, logical_rect.y))?; let second = output_logical_to_image_point( diff --git a/src/backend/wayland/state/region_capture/window_snap/query.rs b/src/backend/wayland/state/region_capture/window_snap/query.rs index 3deb9c737..2d477502a 100644 --- a/src/backend/wayland/state/region_capture/window_snap/query.rs +++ b/src/backend/wayland/state/region_capture/window_snap/query.rs @@ -1,9 +1,9 @@ use crate::backend::wayland::runtime_operation::RuntimeOperationSubmitError; -use crate::backend::wayland::{ - RuntimeOperationController, RuntimeOperationPoll, state::WaylandState, -}; +use crate::backend::wayland::state::WaylandState; +use crate::backend::wayland::{RuntimeOperationController, RuntimeOperationPoll}; use crate::capture::window_geometry::{ - WindowQueryContext, WindowQueryResult, detect_backend, query_window_targets, + WindowGeometryError, WindowQueryContext, WindowQueryResult, detect_backend, + query_window_targets, }; use crate::input::state::RegionPurposeTag; use crate::util::Rect; @@ -32,15 +32,11 @@ enum WindowQuerySubmit { fn submit_queued_window_query_with( controller: &mut RuntimeOperationController< WindowSnapQuery, - Result, + Result, >, session: &mut Option, - operation: impl FnOnce( - WindowQueryContext, - ) -> Result< - WindowQueryResult, - crate::capture::window_geometry::WindowGeometryError, - > + Send + operation: impl FnOnce(WindowQueryContext) -> Result + + Send + 'static, ) -> WindowQuerySubmit { let Some(query) = session.as_ref().and_then(WindowSnapSession::queued_query) else { @@ -74,15 +70,11 @@ fn submit_queued_window_query_with( fn poll_window_query_with( controller: &mut RuntimeOperationController< WindowSnapQuery, - Result, + Result, >, session: &mut Option, - retry: impl FnOnce( - WindowQueryContext, - ) -> Result< - WindowQueryResult, - crate::capture::window_geometry::WindowGeometryError, - > + Send + retry: impl FnOnce(WindowQueryContext) -> Result + + Send + 'static, ) -> bool { let (consumed_terminal, changed) = match controller.poll() { @@ -204,11 +196,11 @@ fn clear_matching_window_session( #[cfg(test)] mod tests { + use std::thread; + use super::*; - use crate::backend::wayland::{ - RuntimeOperationIdSource, RuntimeWakeSource, - state::screen_image::{ScreenImageKind, ScreenSourceToken}, - }; + use crate::backend::wayland::state::screen_image::{ScreenImageKind, ScreenSourceToken}; + use crate::backend::wayland::{RuntimeOperationIdSource, RuntimeWakeSource}; use crate::capture::window_geometry::{WindowGeometryBackend, WindowTarget}; use std::sync::mpsc; use std::time::{Duration, Instant}; @@ -238,9 +230,7 @@ mod tests { } } - fn result( - id: &str, - ) -> Result { + fn result(id: &str) -> Result { Ok(WindowQueryResult { backend: WindowGeometryBackend::Hyprland, targets: vec![WindowTarget { @@ -279,7 +269,7 @@ mod tests { #[test] fn real_capacity_one_terminal_routes_the_reopened_picker_into_retry() { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut controller = RuntimeOperationController::new(RuntimeOperationIdSource::new(), wake.handle()); let old = WindowSnapCorrelation::new(1, source()); @@ -319,7 +309,7 @@ mod tests { while !session.as_ref().is_some_and(WindowSnapSession::is_ready) { poll_window_query_with(&mut controller, &mut session, |_| result("fresh")); assert!(Instant::now() < deadline, "reopened query did not retry"); - std::thread::yield_now(); + thread::yield_now(); } let session = session.unwrap(); assert_eq!(session.correlation(), fresh); diff --git a/src/backend/wayland/state/render/canvas/background.rs b/src/backend/wayland/state/render/canvas/background.rs index 13950ccd1..8c236734f 100644 --- a/src/backend/wayland/state/render/canvas/background.rs +++ b/src/backend/wayland/state/render/canvas/background.rs @@ -1,13 +1,18 @@ +use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; -use super::super::super::*; +use crate::backend::wayland::state::WaylandState; use crate::backend::wayland::state::screen_image::{ ScreenImageKind, ScreenSourceToken, current_screen_source_token, displayed_screen_image, }; -use crate::draw::Color; +use crate::domain::{BoardGrid, BoardGridKind}; +use crate::draw::{BoardPaper, Color, EraserReplayContext, SpotlightMagnifierSource}; +use crate::input::BoardBackground; +use anyhow::{Context, Result}; +use log::warn; pub(super) struct CanvasEraserContext { - paper: Option, + paper: Option, surface: Option, pattern: Option, backdrop_cache_key: Option, @@ -18,7 +23,7 @@ pub(super) struct CanvasEraserContext { /// same call that decided whether the capture below could be painted at /// all, and carried here so the pixels on the surface and the availability /// the loupe and the toolbar report cannot come from different facts. - magnifier_source: crate::draw::SpotlightMagnifierSource, + magnifier_source: SpotlightMagnifierSource, } impl CanvasEraserContext { @@ -31,36 +36,29 @@ impl CanvasEraserContext { bg_color, logical_to_image_scale_x: 1.0, logical_to_image_scale_y: 1.0, - magnifier_source: crate::draw::SpotlightMagnifierSource::from_backdrop( - None, - bg_color.is_some(), - ), + magnifier_source: SpotlightMagnifierSource::from_backdrop(None, bg_color.is_some()), } } - pub(super) fn prepare_paper( - &mut self, - ctx: &cairo::Context, - grid: crate::domain::BoardGrid, - ) -> Result<()> { + pub(super) fn prepare_paper(&mut self, ctx: &cairo::Context, grid: BoardGrid) -> Result<()> { if let Some(color) = self.bg_color { - if grid.kind == crate::domain::BoardGridKind::None { + if grid.kind == BoardGridKind::None { ctx.set_source_rgba(color.r, color.g, color.b, color.a); ctx.paint()?; } else { - let paper = crate::draw::BoardPaper::for_context(color, grid, ctx)?; + let paper = BoardPaper::for_context(color, grid, ctx)?; paper.paint(ctx)?; self.paper = Some(paper); } } Ok(()) } - pub(super) fn replay_context(&self) -> crate::draw::EraserReplayContext<'_> { + pub(super) fn replay_context(&self) -> EraserReplayContext<'_> { crate::draw::EraserReplayContext { pattern: self .paper .as_ref() - .map(crate::draw::BoardPaper::pattern) + .map(BoardPaper::pattern) .or_else(|| self.pattern.as_ref().map(|p| p as &cairo::Pattern)), surface: self.surface.as_ref(), backdrop_cache_key: self.backdrop_cache_key, @@ -72,7 +70,7 @@ impl CanvasEraserContext { } } - pub(super) fn magnifier_source(&self) -> crate::draw::SpotlightMagnifierSource { + pub(super) fn magnifier_source(&self) -> SpotlightMagnifierSource { self.magnifier_source } } @@ -84,7 +82,7 @@ impl CanvasEraserContext { /// all yield a different id. It is what makes the availability descriptor name /// a specific capture rather than merely "some raster". fn raster_source_id(token: &ScreenSourceToken) -> u64 { - let mut hasher = std::collections::hash_map::DefaultHasher::new(); + let mut hasher = DefaultHasher::new(); token.output_id.hash(&mut hasher); token.output_layout_generation.hash(&mut hasher); match token.kind { @@ -114,8 +112,8 @@ fn raster_source_id(token: &ScreenSourceToken) -> u64 { fn resolve_backdrop_provenance( raster_token: Option<&ScreenSourceToken>, board_is_transparent: bool, -) -> (bool, crate::draw::SpotlightMagnifierSource) { - let source = crate::draw::SpotlightMagnifierSource::from_backdrop( +) -> (bool, SpotlightMagnifierSource) { + let source = SpotlightMagnifierSource::from_backdrop( raster_token.map(raster_source_id), !board_is_transparent, ); @@ -150,7 +148,7 @@ impl WaylandState { /// caller can disagree about whether a loupe can preview. pub(in crate::backend::wayland::state) fn current_spotlight_magnifier_source( &self, - ) -> crate::draw::SpotlightMagnifierSource { + ) -> SpotlightMagnifierSource { resolve_backdrop_provenance( self.current_screen_source_token().as_ref(), self.input_state.board_is_transparent(), @@ -254,12 +252,12 @@ impl WaylandState { backdrop_cache_key = Some(cache_key); } else { let color = match self.input_state.boards.active_background() { - crate::input::BoardBackground::Solid(color) => Some(*color), - crate::input::BoardBackground::Transparent => None, + BoardBackground::Solid(color) => Some(*color), + BoardBackground::Transparent => None, }; let mut backdrop = CanvasEraserContext::for_board(color); if paint_board { - backdrop.prepare_paper(ctx, crate::domain::BoardGrid::default())?; + backdrop.prepare_paper(ctx, BoardGrid::default())?; } return Ok(backdrop); } @@ -337,10 +335,7 @@ mod tests { // painting it while merely disabling the loupe is not. let (paint, source) = resolve_backdrop_provenance(None, true); assert!(!paint, "a stale capture must not reach the surface"); - assert_eq!( - source, - crate::draw::SpotlightMagnifierSource::IncompleteTransparent - ); + assert_eq!(source, SpotlightMagnifierSource::IncompleteTransparent); // A valid token paints and magnifies, and the descriptor names that // exact capture rather than merely reporting "some raster". @@ -356,20 +351,18 @@ mod tests { // pixel itself, so the loupe keeps a complete source. let (paint, source) = resolve_backdrop_provenance(None, false); assert!(!paint); - assert_eq!(source, crate::draw::SpotlightMagnifierSource::CompleteSolid); + assert_eq!(source, SpotlightMagnifierSource::CompleteSolid); } #[test] fn a_transparent_board_without_valid_pixels_is_incomplete() { assert_eq!( - crate::draw::SpotlightMagnifierSource::from_backdrop(None, false), - crate::draw::SpotlightMagnifierSource::IncompleteTransparent + SpotlightMagnifierSource::from_backdrop(None, false), + SpotlightMagnifierSource::IncompleteTransparent ); // Same board, valid capture: the loupe has a source again without the // shape's requested factor ever being rewritten. let live = raster_source_id(&token(ScreenImageKind::Frozen, 1, 1)); - assert!( - crate::draw::SpotlightMagnifierSource::from_backdrop(Some(live), false).is_complete() - ); + assert!(SpotlightMagnifierSource::from_backdrop(Some(live), false).is_complete()); } } diff --git a/src/backend/wayland/state/render/canvas/context.rs b/src/backend/wayland/state/render/canvas/context.rs index 73c2e23c3..30c147f4b 100644 --- a/src/backend/wayland/state/render/canvas/context.rs +++ b/src/backend/wayland/state/render/canvas/context.rs @@ -5,10 +5,10 @@ use crate::util::Rect; /// Parameters borrowed from the owned frame plan and its local Cairo target. /// Backdrop replay resources and mutable runtime caches stay in the painter. -pub(in crate::backend::wayland::state::render) struct CanvasRenderCtx<'a> { - pub(in crate::backend::wayland::state::render) cairo: &'a cairo::Context, - pub(in crate::backend::wayland::state::render) geometry: &'a FrameGeometry, - pub(in crate::backend::wayland::state::render) canvas: &'a CanvasFrame, - pub(in crate::backend::wayland::state::render) damage_world: &'a [Rect], +pub(in crate::backend::wayland::state::render) struct CanvasRenderCtx<'frame> { + pub(in crate::backend::wayland::state::render) cairo: &'frame cairo::Context, + pub(in crate::backend::wayland::state::render) geometry: &'frame FrameGeometry, + pub(in crate::backend::wayland::state::render) canvas: &'frame CanvasFrame, + pub(in crate::backend::wayland::state::render) damage_world: &'frame [Rect], pub(in crate::backend::wayland::state::render) now: Instant, } diff --git a/src/backend/wayland/state/render/canvas/mod.rs b/src/backend/wayland/state/render/canvas/mod.rs index 6127bc7e7..e0109a8a5 100644 --- a/src/backend/wayland/state/render/canvas/mod.rs +++ b/src/backend/wayland/state/render/canvas/mod.rs @@ -3,9 +3,23 @@ mod context; mod overlays; mod text; -use super::super::*; +use crate::domain::BoardGrid; +use crate::draw; +use crate::draw::{ + DrawnShape, RenderCaches, SpotlightMagnifierOutcome, SpotlightMagnifierSource, SpotlightPass, + TextMeasurer, +}; +use crate::input::state::{SpotlightFrameRegions, Toast, ToastPriority}; +use crate::input::tool::ProvisionalToolStroke; +use crate::util::Rect; + +use super::super::WaylandState; +use super::super::perf::PerfRenderBreakdown; use crate::i18n::tr; +use anyhow::Result; pub(super) use context::CanvasRenderCtx; +use log::debug; +use std::time::Instant; const SPOTLIGHT_MAGNIFIER_TOAST_SOURCE: &str = "spotlight-magnifier"; @@ -41,7 +55,7 @@ impl WaylandState { fn warn_once_for_arrived_unavailable_magnification( &mut self, has_magnified_region: bool, - source: crate::draw::SpotlightMagnifierSource, + source: SpotlightMagnifierSource, show_toast: bool, ) { if self @@ -68,9 +82,9 @@ impl WaylandState { fn push_spotlight_magnifier_toast(&mut self, message: &str) { self.input_state.push_toast( - crate::input::state::ToastPriority::Critical, + ToastPriority::Critical, SPOTLIGHT_MAGNIFIER_TOAST_SOURCE, - crate::input::state::Toast::warning(message), + Toast::warning(message), ); } @@ -195,7 +209,7 @@ impl WaylandState { let (screen_x, screen_y) = self.pointer.position(); self.canvas_world_coords(screen_x as f64, screen_y as f64) }); - let crate::input::state::SpotlightFrameRegions { + let SpotlightFrameRegions { regions: spotlight_regions, committed_magnified, } = self.input_state.spotlight_frame_regions(spotlight_cursor); @@ -205,7 +219,7 @@ impl WaylandState { magnifier_source, render_transients, ); - match crate::draw::render_spotlight_magnification_pass( + match draw::render_spotlight_magnification_pass( ctx, &spotlight_regions, self.input_state.style.spotlight_feather, @@ -222,10 +236,10 @@ impl WaylandState { // It also ends any run of render failures: the flag below tracks // failing *renders*, and a frame that never attempted one must not // leave it armed to swallow the next real failure. - Ok(crate::draw::SpotlightMagnifierOutcome::SourceUnavailable) => { + Ok(SpotlightMagnifierOutcome::SourceUnavailable) => { self.spotlight.clear_render_warning(); } - Ok(crate::draw::SpotlightMagnifierOutcome::AllocationFailed) => { + Ok(SpotlightMagnifierOutcome::AllocationFailed) => { self.push_spotlight_magnifier_warning( tr("Spotlight magnification could not allocate its render buffer."), render_transients, @@ -238,7 +252,7 @@ impl WaylandState { render_transients, ); } - Ok(crate::draw::SpotlightMagnifierOutcome::Rendered(metrics)) => { + Ok(SpotlightMagnifierOutcome::Rendered(metrics)) => { if let Some(perf) = perf.as_mut() { perf.stages.spotlight_snapshot = perf .stages @@ -254,17 +268,17 @@ impl WaylandState { } self.spotlight.clear_render_warning(); } - Ok(crate::draw::SpotlightMagnifierOutcome::NotNeeded) => { + Ok(SpotlightMagnifierOutcome::NotNeeded) => { self.spotlight.clear_render_warning(); } } // Remember for the next frame's damage decision: once the last spotlight // is gone this buffer still carries its dim until a full repaint. self.spotlight.note_frame(!spotlight_regions.is_empty()); - crate::draw::render_spotlight_pass( + draw::render_spotlight_pass( ctx, &spotlight_regions, - crate::draw::SpotlightPass { + SpotlightPass { dim_opacity: self.input_state.style.spotlight_dim_opacity, feather: self.input_state.style.spotlight_feather, }, @@ -303,7 +317,7 @@ impl WaylandState { let (caches, measurer) = self.render.draw_text_parts_mut(); let mut render = crate::draw::RenderCtx::new(ctx, caches); let rendered_provisional = match provisional { - crate::input::tool::ProvisionalToolStroke::BlurReplayPreview(params) => { + ProvisionalToolStroke::BlurReplayPreview(params) => { render.render_blur_rect(params, &replay_ctx); true } @@ -347,14 +361,14 @@ impl WaylandState { #[allow(clippy::too_many_arguments)] fn render_committed_canvas_shapes( - measurer: &crate::draw::TextMeasurer, - shapes: &[crate::draw::DrawnShape], + measurer: &TextMeasurer, + shapes: &[DrawnShape], layer_cache: &super::super::canvas_layer::CanvasLayerCache, - draw_caches: &mut crate::draw::RenderCaches, + draw_caches: &mut RenderCaches, canvas: &CanvasRenderCtx<'_>, layer_cache_ready: bool, eraser_ctx: &mut background::CanvasEraserContext, - grid: crate::domain::BoardGrid, + grid: BoardGrid, mut perf: Option<&mut PerfRenderBreakdown>, ) -> Result<()> { let ctx = canvas.cairo; @@ -383,7 +397,7 @@ fn render_committed_canvas_shapes( cairo: ctx, caches: draw_caches, }; - let mut render_shape = |shape: &crate::draw::DrawnShape| { + let mut render_shape = |shape: &DrawnShape| { super::super::canvas_layer::render_committed_shape( measurer, &mut render, @@ -424,7 +438,7 @@ fn render_committed_canvas_shapes( Ok(()) } -fn union_damage_bounds(regions: &[crate::util::Rect]) -> Option { +fn union_damage_bounds(regions: &[Rect]) -> Option { regions.iter().copied().reduce(|union, region| { let min_x = union.x.min(region.x); let min_y = union.y.min(region.y); @@ -436,7 +450,7 @@ fn union_damage_bounds(regions: &[crate::util::Rect]) -> Option Option Option { +) -> Option { let margin = 2; let x = bounds.x.saturating_sub(margin); let y = bounds.y.saturating_sub(margin); let width_with_margin = bounds.width.saturating_add(margin * 2); let height_with_margin = bounds.height.saturating_add(margin * 2); if canvas_transform_active { - return crate::util::Rect::new(x, y, width_with_margin, height_with_margin); + return Rect::new(x, y, width_with_margin, height_with_margin); } let x = x.max(0); let y = y.max(0); - crate::util::Rect::new( + Rect::new( x, y, width_with_margin.min((width as i32).saturating_sub(x)), @@ -469,7 +483,7 @@ fn safe_shape_damage_bounds( ) } -fn rects_intersect(a: crate::util::Rect, b: crate::util::Rect) -> bool { +fn rects_intersect(a: Rect, b: Rect) -> bool { let a_right = a.x.saturating_add(a.width); let a_bottom = a.y.saturating_add(a.height); let b_right = b.x.saturating_add(b.width); @@ -477,7 +491,7 @@ fn rects_intersect(a: crate::util::Rect, b: crate::util::Rect) -> bool { !(a.x >= b_right || a_right <= b.x || a.y >= b_bottom || a_bottom <= b.y) } -fn provisional_point_count(stroke: &crate::input::tool::ProvisionalToolStroke<'_>) -> usize { +fn provisional_point_count(stroke: &ProvisionalToolStroke<'_>) -> usize { match stroke { crate::input::tool::ProvisionalToolStroke::BorrowedFreehand { points, .. } | crate::input::tool::ProvisionalToolStroke::BorrowedPressureFreehand { points, .. } @@ -485,9 +499,9 @@ fn provisional_point_count(stroke: &crate::input::tool::ProvisionalToolStroke<'_ | crate::input::tool::ProvisionalToolStroke::EraserPreview { points, .. } | crate::input::tool::ProvisionalToolStroke::Laser { points, .. } | crate::input::tool::ProvisionalToolStroke::Recognized { ink: points, .. } => points.len(), - crate::input::tool::ProvisionalToolStroke::Shape(_) - | crate::input::tool::ProvisionalToolStroke::BlurReplayPreview(_) - | crate::input::tool::ProvisionalToolStroke::None => 0, + ProvisionalToolStroke::Shape(_) + | ProvisionalToolStroke::BlurReplayPreview(_) + | ProvisionalToolStroke::None => 0, } } diff --git a/src/backend/wayland/state/render/canvas/overlays.rs b/src/backend/wayland/state/render/canvas/overlays.rs index 765f61f2e..e5e671d2d 100644 --- a/src/backend/wayland/state/render/canvas/overlays.rs +++ b/src/backend/wayland/state/render/canvas/overlays.rs @@ -1,4 +1,7 @@ -use super::super::super::*; +use crate::backend::wayland::state::WaylandState; +use crate::draw::shape; +use crate::input::{DrawingState, EraserMode, Tool}; +use crate::{draw, ui}; use std::collections::HashSet; impl WaylandState { @@ -8,7 +11,7 @@ impl WaylandState { let frame = self.input_state.boards.active_frame(); for drawn in &frame.shapes { if selected.contains(&drawn.id) { - crate::draw::render_selection_halo_with_measurer( + draw::render_selection_halo_with_measurer( self.render.text_measurer(), ctx, drawn, @@ -30,7 +33,7 @@ impl WaylandState { .input_state .selection_bounds_with(self.render.text_measurer()) { - crate::draw::render_selection_handles(ctx, &bounds); + draw::render_selection_handles(ctx, &bounds); } self.render_spotlight_magnification_control(ctx); @@ -74,7 +77,7 @@ impl WaylandState { let Some(handle) = self.input_state.selected_arrow_bend_handle() else { return; }; - crate::ui::render_arrow_bend_handle(ctx, handle.rect); + ui::render_arrow_bend_handle(ctx, handle.rect); } /// Draws the magnification slider above a selected loupe. @@ -100,13 +103,13 @@ impl WaylandState { // inline status is: a 1x loupe is an ordinary bright opening that needs // no pixel source, so "1x - Freeze screen to preview" would be advice // about a problem the user does not have. - let unavailable = crate::draw::spotlight_magnification_is_active(control.magnification) + let unavailable = draw::spotlight_magnification_is_active(control.magnification) .then(|| { self.current_spotlight_magnifier_source() .unavailable_reason() }) .flatten(); - crate::ui::render_spotlight_magnification_control( + ui::render_spotlight_magnification_control( self.render.ui_text(), ctx, control.track, @@ -142,7 +145,7 @@ impl WaylandState { } = &self.input_state.state { // Preview the integer path the committed stroke erases with. - let rounded = crate::draw::shape::quantize_path(points); + let rounded = shape::quantize_path(points); let sampled = self.input_state.sample_eraser_path_points(&rounded); self.input_state.hit_test_all_for_points_cached_with( self.render.text_measurer(), @@ -165,7 +168,7 @@ impl WaylandState { match ids.len() { 1 => { if let Some(drawn) = frame.shape(ids[0]) { - crate::draw::render_selection_halo_with_measurer( + draw::render_selection_halo_with_measurer( self.render.text_measurer(), ctx, drawn, @@ -202,7 +205,7 @@ impl WaylandState { } for &index in &indices[..count] { if let Some(drawn) = frame.shapes.get(index) { - crate::draw::render_selection_halo_with_measurer( + draw::render_selection_halo_with_measurer( self.render.text_measurer(), ctx, drawn, @@ -214,7 +217,7 @@ impl WaylandState { let hover_ids: HashSet<_> = ids.into_iter().collect(); for drawn in &frame.shapes { if hover_ids.contains(&drawn.id) { - crate::draw::render_selection_halo_with_measurer( + draw::render_selection_halo_with_measurer( self.render.text_measurer(), ctx, drawn, diff --git a/src/backend/wayland/state/render/canvas/resource_tests.rs b/src/backend/wayland/state/render/canvas/resource_tests.rs index addad1218..55b3b7ea2 100644 --- a/src/backend/wayland/state/render/canvas/resource_tests.rs +++ b/src/backend/wayland/state/render/canvas/resource_tests.rs @@ -1,3 +1,8 @@ +use crate::domain::{BoardGrid, BoardGridKind}; +use crate::draw::FontDescriptor; + +use std::io::Cursor; + use super::*; use crate::backend::wayland::state::canvas_layer::{CanvasLayerCache, CanvasLayerInputs}; use crate::backend::wayland::state::render::plan::{CanvasFrame, FrameGeometry}; @@ -23,7 +28,7 @@ fn inputs() -> CanvasLayerInputs { } fn shapes() -> Vec { - let mut bytes = std::io::Cursor::new(Vec::new()); + let mut bytes = Cursor::new(Vec::new()); let source = cairo::ImageSurface::create(cairo::Format::ARgb32, 2, 2).unwrap(); let context = cairo::Context::new(&source).unwrap(); context.set_source_rgb(220.0 / 255.0, 40.0 / 255.0, 20.0 / 255.0); @@ -53,7 +58,7 @@ fn shapes() -> Vec { a: 1.0, }, size: 14.0, - font_descriptor: crate::draw::FontDescriptor::default(), + font_descriptor: FontDescriptor::default(), background_enabled: false, wrap_width: None, }, @@ -72,10 +77,10 @@ fn shapes() -> Vec { } fn paint( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, shapes: &[DrawnShape], layer: &CanvasLayerCache, - caches: &mut crate::draw::RenderCaches, + caches: &mut RenderCaches, inputs: CanvasLayerInputs, cached: bool, ) -> Vec { @@ -144,18 +149,18 @@ fn assert_pixels_match(actual: &[u8], expected: &[u8], label: &str) { } fn fresh_baked(shapes: &[DrawnShape], request: CanvasLayerInputs) -> Vec { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut layer = CanvasLayerCache::new(); - let mut caches = crate::draw::RenderCaches::default(); + let mut caches = RenderCaches::default(); assert!(layer.ensure(&measurer, &mut caches, shapes, request)); paint(&measurer, shapes, &layer, &mut caches, request, true) } #[test] fn baked_and_direct_passes_match_fresh_owners_across_reuse_and_invalidation() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut layer = CanvasLayerCache::new(); - let mut caches = crate::draw::RenderCaches::default(); + let mut caches = RenderCaches::default(); let mut shapes = shapes(); let initial = inputs(); for (iteration, request) in [ @@ -213,9 +218,9 @@ fn baked_and_direct_passes_match_fresh_owners_across_reuse_and_invalidation() { &fresh_baked(&shapes, request), &format!("baked pass {iteration}"), ); - let mut fresh = crate::draw::RenderCaches::default(); + let mut fresh = RenderCaches::default(); let expected_direct = paint( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &shapes, &CanvasLayerCache::new(), &mut fresh, @@ -232,9 +237,9 @@ fn baked_and_direct_passes_match_fresh_owners_across_reuse_and_invalidation() { #[test] fn rejected_bake_clears_previous_layer_and_direct_fallback_still_paints() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut layer = CanvasLayerCache::new(); - let mut caches = crate::draw::RenderCaches::default(); + let mut caches = RenderCaches::default(); let shapes = shapes(); let request = inputs(); assert!(layer.ensure(&measurer, &mut caches, &shapes, request)); @@ -258,7 +263,7 @@ fn rejected_bake_clears_previous_layer_and_direct_fallback_still_paints() { #[test] fn each_scene_key_rebakes_without_shape_identity_changes() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let initial = inputs(); for (name, changed, replace_image) in [ ( @@ -308,7 +313,7 @@ fn each_scene_key_rebakes_without_shape_identity_changes() { ), ] { let mut layer = CanvasLayerCache::new(); - let mut caches = crate::draw::RenderCaches::default(); + let mut caches = RenderCaches::default(); let mut scene = shapes(); assert!(layer.ensure(&measurer, &mut caches, &scene, initial)); let before = paint(&measurer, &scene, &layer, &mut caches, initial, true); @@ -346,9 +351,9 @@ fn each_scene_key_rebakes_without_shape_identity_changes() { #[test] #[ignore = "release timing workload; run with --release --ignored --nocapture"] fn measure_sparse_damage_scan() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let layer = CanvasLayerCache::new(); - let mut caches = crate::draw::RenderCaches::default(); + let mut caches = RenderCaches::default(); let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 1920, 1080).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); let geometry = FrameGeometry::new(1920, 1080, 1); @@ -361,7 +366,7 @@ fn measure_sparse_damage_scan() { text_halo_enabled: true, layer_cache_eligible: false, }; - let damage = [crate::util::Rect { + let damage = [Rect { x: 0, y: 0, width: 32, @@ -429,9 +434,9 @@ fn measure_sparse_damage_scan() { #[test] fn board_grid_baked_pan_matches_direct_and_invalidates_on_pattern_and_spacing() { use crate::domain::{BoardGrid, BoardGridKind}; - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut cache = CanvasLayerCache::new(); - let mut caches = crate::draw::RenderCaches::default(); + let mut caches = RenderCaches::default(); let shapes = shapes(); for origin in [(0.0, 0.0), (-71.0, -53.0), (-1_000_021.0, -2_000_003.0)] { for kind in BoardGridKind::ALL { @@ -462,15 +467,15 @@ mod grid_performance; #[test] fn warm_4k_paper_render_skips_source_and_failed_blit_restores_paper() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let request = CanvasLayerInputs { width: 3840, height: 2160, - grid: crate::domain::BoardGrid::new(crate::domain::BoardGridKind::Isometric, 40), + grid: BoardGrid::new(BoardGridKind::Isometric, 40), ..inputs() }; let mut cache = CanvasLayerCache::new(); - let mut caches = crate::draw::RenderCaches::default(); + let mut caches = RenderCaches::default(); assert!(cache.ensure(&measurer, &mut caches, &[], request)); let warm = paint(&measurer, &[], &cache, &mut caches, request, true); // Simulate an unavailable cache even though readiness was reported. The diff --git a/src/backend/wayland/state/render/canvas/resource_tests/grid_performance.rs b/src/backend/wayland/state/render/canvas/resource_tests/grid_performance.rs index 12e189762..0d5ba0bdb 100644 --- a/src/backend/wayland/state/render/canvas/resource_tests/grid_performance.rs +++ b/src/backend/wayland/state/render/canvas/resource_tests/grid_performance.rs @@ -1,4 +1,6 @@ //! Opt-in end-to-end paper consumers. No compositor or native windows. +use crate::draw::Frame; + use super::*; use crate::canvas_export::{ BoardExportSnapshot, CanvasExportBackdropSnapshot, CanvasExportSnapshot, CanvasExportViewport, @@ -7,15 +9,15 @@ use crate::canvas_export::{ use crate::domain::{BoardGrid, BoardGridKind}; use std::time::Instant; -fn workload(origin: (i32, i32), size: (u32, u32), count: usize) -> crate::draw::Frame { - let mut frame = crate::draw::Frame::new(); +fn workload(origin: (i32, i32), size: (u32, u32), count: usize) -> Frame { + let mut frame = Frame::new(); for n in 0..80 { frame.add_shape(Shape::Line { x1: origin.0, y1: origin.1 + n * 19, x2: origin.0 + size.0 as i32, y2: origin.1 + n * 19 + 60, - color: crate::draw::BLUE, + color: draw::BLUE, thick: 3.0, }); } @@ -56,7 +58,7 @@ fn board_grid_consumers_performance() { width: size.0, height: size.1, origin: (f64::from(origin.0), f64::from(origin.1)), - background: Some(crate::draw::WHITE), + background: Some(draw::WHITE), grid, ..inputs() }; @@ -68,10 +70,7 @@ fn board_grid_consumers_performance() { origin_x: origin.0, origin_y: origin.1, }, - backdrop: CanvasExportBackdropSnapshot::board_paper( - crate::draw::WHITE, - grid, - ), + backdrop: CanvasExportBackdropSnapshot::board_paper(draw::WHITE, grid), board: BoardExportSnapshot { frame: frame.clone_without_history(), }, @@ -81,10 +80,10 @@ fn board_grid_consumers_performance() { }; let mut samples = [Vec::new(), Vec::new(), Vec::new()]; let mut png_bytes = 0; - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); for sample in 0..6 { let mut layer = CanvasLayerCache::new(); - let mut caches = crate::draw::RenderCaches::default(); + let mut caches = RenderCaches::default(); let start = Instant::now(); assert!(layer.ensure(&measurer, &mut caches, &frame.shapes, request)); let cold = start.elapsed().as_micros(); diff --git a/src/backend/wayland/state/render/canvas/text.rs b/src/backend/wayland/state/render/canvas/text.rs index 996dd3f18..b21c9946b 100644 --- a/src/backend/wayland/state/render/canvas/text.rs +++ b/src/backend/wayland/state/render/canvas/text.rs @@ -1,8 +1,16 @@ mod ime; -use super::super::super::*; +use crate::draw; +use crate::draw::Color; +use crate::input::TextInputMode; +use crate::util::Rect; + +use std::f64::consts; + use super::CanvasRenderCtx; +use crate::backend::wayland::state::WaylandState; use crate::draw::Shape; +use crate::input::DrawingState; use ime::{paint_preedit_selection, paint_preedit_underline}; use std::ops::Range; @@ -44,8 +52,8 @@ impl WaylandState { self.input_state.style.current_color, ); match self.input_state.text_editing.mode() { - crate::input::TextInputMode::Plain => { - crate::draw::render_text_with_halo_with_measurer( + TextInputMode::Plain => { + draw::render_text_with_halo_with_measurer( self.render.text_measurer(), ctx, *x, @@ -59,8 +67,8 @@ impl WaylandState { canvas.canvas.text_halo_enabled, ); } - crate::input::TextInputMode::StickyNote => { - crate::draw::render_sticky_note_preview_with_measurer( + TextInputMode::StickyNote => { + draw::render_sticky_note_preview_with_measurer( self.render.text_measurer(), ctx, *x, @@ -104,7 +112,7 @@ impl WaylandState { y: i32, preview_text: &str, caret: Option, - color: crate::draw::Color, + color: Color, ) { let Some(caret) = caret else { return; @@ -144,7 +152,7 @@ impl WaylandState { y: i32, preview_text: &str, range: Option<&Range>, - color: crate::draw::Color, + color: Color, ) { let Some(range) = range else { return; @@ -221,7 +229,7 @@ impl WaylandState { background_enabled, wrap_width, } if !text.is_empty() => { - crate::draw::render_text_with_halo_with_measurer( + draw::render_text_with_halo_with_measurer( self.render.text_measurer(), ctx, *x, @@ -244,7 +252,7 @@ impl WaylandState { font_descriptor, wrap_width, } if !text.is_empty() => { - crate::draw::render_sticky_note_with_measurer( + draw::render_sticky_note_with_measurer( self.render.text_measurer(), ctx, *x, @@ -270,7 +278,7 @@ impl WaylandState { } /// Renders a subtle dashed border around the ghost text area. - fn render_ghost_border(&self, ctx: &cairo::Context, bounds: crate::util::Rect) { + fn render_ghost_border(&self, ctx: &cairo::Context, bounds: Rect) { let _ = ctx.save(); let padding = 4.0; @@ -314,13 +322,7 @@ impl WaylandState { gradient.add_color_stop_rgba(1.0, 0.2, 0.55, 0.65, 0.0); ctx.set_source(&gradient).ok(); - ctx.arc( - x as f64, - y as f64, - glow_radius, - 0.0, - std::f64::consts::PI * 2.0, - ); + ctx.arc(x as f64, y as f64, glow_radius, 0.0, consts::PI * 2.0); let _ = ctx.fill(); let _ = ctx.restore(); @@ -332,29 +334,24 @@ fn render_caret_stroke( caret_x: f64, top: f64, bottom: f64, - color: crate::draw::Color, + color: Color, size: f64, text_halo_enabled: bool, ) { ctx.save().ok(); // Widths come from the draw layer so the damage tracker sizes the caret's // repaint rectangle from the exact same numbers. - let line_width = crate::draw::caret_line_width(size); + let line_width = draw::caret_line_width(size); if text_halo_enabled { // The caret stands where the glyphs will, so it asks the same question // they do: what is behind this point on the canvas? - let background_luminance = crate::draw::painted_background_luminance( + let background_luminance = draw::painted_background_luminance( ctx, - ( - caret_x, - top, - crate::draw::caret_outline_width(size), - bottom - top, - ), + (caret_x, top, draw::caret_outline_width(size), bottom - top), ); - let outline = crate::draw::text_outline_color(color, background_luminance); + let outline = draw::text_outline_color(color, background_luminance); ctx.set_source_rgba(outline.r, outline.g, outline.b, outline.a); - ctx.set_line_width(crate::draw::caret_outline_width(size)); + ctx.set_line_width(draw::caret_outline_width(size)); ctx.move_to(caret_x, top); ctx.line_to(caret_x, bottom); let _ = ctx.stroke(); @@ -367,20 +364,17 @@ fn render_caret_stroke( ctx.restore().ok(); } -fn text_preview_decoration_color( - mode: crate::input::TextInputMode, - current_color: crate::draw::Color, -) -> crate::draw::Color { +fn text_preview_decoration_color(mode: TextInputMode, current_color: Color) -> Color { match mode { - crate::input::TextInputMode::Plain => current_color, - crate::input::TextInputMode::StickyNote => { - crate::draw::sticky_note_foreground(current_color) - } + TextInputMode::Plain => current_color, + TextInputMode::StickyNote => draw::sticky_note_foreground(current_color), } } #[cfg(test)] mod tests { + use crate::draw; + use super::{render_caret_stroke, text_preview_decoration_color}; use crate::draw::Color; use crate::input::TextInputMode; @@ -396,7 +390,7 @@ mod tests { assert_eq!( text_preview_decoration_color(TextInputMode::StickyNote, background), - crate::draw::sticky_note_foreground(background) + draw::sticky_note_foreground(background) ); assert_eq!( text_preview_decoration_color(TextInputMode::Plain, background), diff --git a/src/backend/wayland/state/render/canvas/text/ime.rs b/src/backend/wayland/state/render/canvas/text/ime.rs index 49c374c86..738ba9648 100644 --- a/src/backend/wayland/state/render/canvas/text/ime.rs +++ b/src/backend/wayland/state/render/canvas/text/ime.rs @@ -1,5 +1,9 @@ use std::ops::Range; +use pangocairo::functions; + +use crate::draw::{Color, shape}; + /// Paint a Pango-backed highlight for a non-collapsed preedit cursor range. pub(super) fn paint_preedit_selection( ctx: &cairo::Context, @@ -10,7 +14,7 @@ pub(super) fn paint_preedit_selection( font_desc: &str, wrap_width: Option, ) { - let layout = pangocairo::functions::create_layout(ctx); + let layout = functions::create_layout(ctx); let font_desc = pango::FontDescription::from_string(font_desc); layout.set_font_description(Some(&font_desc)); layout.set_text(text); @@ -38,7 +42,7 @@ pub(super) fn paint_preedit_selection( // The transparent source suppresses a second glyph pass; Pango's explicit // background attributes still paint the selected range. ctx.set_source_rgba(0.0, 0.0, 0.0, 0.0); - pangocairo::functions::show_layout(ctx, &layout); + functions::show_layout(ctx, &layout); ctx.restore().ok(); } @@ -53,7 +57,7 @@ pub(super) fn paint_preedit_underline( range: Range, font_desc: &str, wrap_width: Option, - color: crate::draw::Color, + color: Color, ) { let Ok(end) = usize::try_from(range.end) else { return; @@ -64,7 +68,7 @@ pub(super) fn paint_preedit_underline( // Same layout configuration as measurement and rendering, so the underline // lands on the glyph run the caret and damage were computed against. - let layout = crate::draw::shape::configured_layout(ctx, text, font_desc, wrap_width); + let layout = shape::configured_layout(ctx, text, font_desc, wrap_width); let attrs = pango::AttrList::new(); let mut underline = pango::AttrInt::new_underline(pango::Underline::Single); @@ -87,7 +91,7 @@ pub(super) fn paint_preedit_underline( ctx.save().ok(); ctx.move_to(origin.0 as f64, origin.1 as f64 - baseline); ctx.set_source_rgba(0.0, 0.0, 0.0, color.a.clamp(0.0, 1.0)); - pangocairo::functions::show_layout(ctx, &layout); + functions::show_layout(ctx, &layout); ctx.restore().ok(); } diff --git a/src/backend/wayland/state/render/measure_badge.rs b/src/backend/wayland/state/render/measure_badge.rs index f4a26f210..6864542a0 100644 --- a/src/backend/wayland/state/render/measure_badge.rs +++ b/src/backend/wayland/state/render/measure_badge.rs @@ -1,4 +1,7 @@ -use super::super::*; +use crate::ui; +use crate::ui::ShapeMeasureBadge; + +use super::super::WaylandState; use crate::ui::theme; impl WaylandState { @@ -6,13 +9,13 @@ impl WaylandState { &self, width: u32, height: u32, - ) -> Option { + ) -> Option { let pointer = self.pointer.position(); let world = self.canvas_world_coords(pointer.0 as f64, pointer.1 as f64); let readout = self .input_state .provisional_shape_readout(world.0, world.1)?; - crate::ui::measure_shape_badge( + ui::measure_shape_badge( self.render.ui_text(), self.config.ui.show_shape_size_readout, readout, @@ -29,7 +32,7 @@ impl WaylandState { let (x, y, badge_width, badge_height) = badge.bounds; let _ = ctx.save(); - crate::ui::draw_pill( + ui::draw_pill( ctx, x, y, @@ -46,12 +49,7 @@ impl WaylandState { ctx.clip(); self.render .ui_text() - .layout( - ctx, - crate::ui::shape_measure_badge_text_style(), - &badge.text, - None, - ) + .layout(ctx, ui::shape_measure_badge_text_style(), &badge.text, None) .show_at_baseline(ctx, badge.baseline.0, badge.baseline.1); let _ = ctx.restore(); diff --git a/src/backend/wayland/state/render/mod.rs b/src/backend/wayland/state/render/mod.rs index 1ab43b09b..a961bb811 100644 --- a/src/backend/wayland/state/render/mod.rs +++ b/src/backend/wayland/state/render/mod.rs @@ -1,4 +1,10 @@ -use super::*; +use super::WaylandState; +use super::buffer_damage::FullDamageReason; +use super::perf::{PerfRenderBreakdown, PerfRenderSkipReason}; +use anyhow::Result; +use log::debug; +use std::time::{Duration, Instant}; +use wayland_client::QueueHandle; macro_rules! record_stage { ($breakdown:expr, $field:ident, $body:expr) => {{ @@ -29,7 +35,7 @@ mod ui_effect_damage; use crate::backend::wayland::state::buffer_damage::BufferDamageTracker; use plan::{FrameGeometry, FrameVisibility, plan_frame}; pub(in crate::backend::wayland) use runtime::RenderRuntime; -use runtime::{UiEffect, UiEffectFlags}; +use runtime::UiEffect; /// What a render pass actually did. /// diff --git a/src/backend/wayland/state/render/paint.rs b/src/backend/wayland/state/render/paint.rs index cb27a79a6..a110e1c8c 100644 --- a/src/backend/wayland/state/render/paint.rs +++ b/src/backend/wayland/state/render/paint.rs @@ -1,7 +1,14 @@ -use super::plan::FramePlan; +use std::slice; +use std::time::{Duration, Instant}; + +use super::super::WaylandState; +use super::super::perf::PerfRenderBreakdown; +use super::plan::{FrameGeometry, FramePlan}; use super::profile::{PixelBuffer, ProfileMode}; -use super::*; +use super::{canvas, record_render_stage}; use crate::backend::wayland::surface::AcquiredBuffer; +use anyhow::{Context, Result}; +use log::debug; impl WaylandState { pub(super) fn paint_frame( @@ -25,7 +32,7 @@ impl WaylandState { // The acquired SHM slot supplies width * height * 4 bytes in ARgb32. // It stays owned by this render attempt until submission. All Cairo // handles are dropped before attach_to marks the slot in flight. - let draw_start = std::time::Instant::now(); + let draw_start = Instant::now(); let (cairo_surface, ctx) = record_stage!(breakdown, cairo_surface, { // SAFETY: the unsubmitted slot owns the backing memory throughout // painting; geometry and stride match its acquisition parameters. @@ -91,10 +98,10 @@ impl WaylandState { // SAFETY: the acquired slot remains exclusive to this paint pass. // Cairo is flushed and the temporary slice ends before UI painting. let data = unsafe { - std::slice::from_raw_parts_mut(canvas_ptr as *mut u8, plan.geometry.byte_len) + slice::from_raw_parts_mut(canvas_ptr as *mut u8, plan.geometry.byte_len) }; let rewritten = plan.profile.before_ui( - PixelBuffer { + &mut PixelBuffer { data, width: phys_width as i32, height: phys_height as i32, @@ -128,10 +135,10 @@ impl WaylandState { // SAFETY: Cairo is flushed and dropped, and the slot has not been // attached. This is the only access to its pixel bytes. let data = unsafe { - std::slice::from_raw_parts_mut(canvas_ptr as *mut u8, plan.geometry.byte_len) + slice::from_raw_parts_mut(canvas_ptr as *mut u8, plan.geometry.byte_len) }; plan.profile.after_ui( - PixelBuffer { + &mut PixelBuffer { data, width: phys_width as i32, height: phys_height as i32, @@ -157,7 +164,7 @@ impl WaylandState { width, height, phys_width, phys_height, scale, draw_duration ); } - if draw_duration > std::time::Duration::from_millis(2) { + if draw_duration > Duration::from_millis(2) { debug!("Cairo draw took {:?}", draw_duration); } diff --git a/src/backend/wayland/state/render/prepare.rs b/src/backend/wayland/state/render/prepare.rs index 6a91deb70..00cc76e5e 100644 --- a/src/backend/wayland/state/render/prepare.rs +++ b/src/backend/wayland/state/render/prepare.rs @@ -1,8 +1,19 @@ -use super::plan::{CanvasPolicyInputs, PreparedFrame}; +use crate::draw::DirtyFullReason; +use crate::util::Rect; + +use super::super::WaylandState; +use super::super::buffer_damage::FullDamageReason; +use super::super::perf::{ + PerfDamageDiagnostics, PerfRenderBreakdown, PerfRenderProfileKind, + damage_covers_logical_surface, +}; +use super::plan::{CanvasPolicyInputs, FrameGeometry, FrameVisibility, PreparedFrame}; use super::profile::FrameProfile; -use super::*; +use super::record_render_stage; +use super::runtime::{UiEffect, UiEffectFlags}; use crate::backend::wayland::state::buffer_damage::{BufferDamageReport, BufferDamageTracker}; use crate::backend::wayland::surface::AcquiredBuffer; +use std::time::Instant; #[derive(Clone, Copy)] struct RenderAnimationState { @@ -237,7 +248,7 @@ fn take_frame_damage( pool_size, ); if report.regions.is_empty() - && let Some(full) = crate::util::Rect::new(0, 0, logical_width, logical_height) + && let Some(full) = Rect::new(0, 0, logical_width, logical_height) { report.regions = vec![full]; report.full_reason = Some(FullDamageReason::EmptyDamageFallback); @@ -247,13 +258,11 @@ fn take_frame_damage( report } -fn input_full_damage_reason( - reason: Option, -) -> Option { +fn input_full_damage_reason(reason: Option) -> Option { reason.map(|reason| match reason { - crate::draw::DirtyFullReason::CanvasClear => FullDamageReason::CanvasClear, - crate::draw::DirtyFullReason::FirstRunOnboarding => FullDamageReason::FirstRunOnboarding, - crate::draw::DirtyFullReason::InlineToolbar => FullDamageReason::InlineToolbar, + DirtyFullReason::CanvasClear => FullDamageReason::CanvasClear, + DirtyFullReason::FirstRunOnboarding => FullDamageReason::FirstRunOnboarding, + DirtyFullReason::InlineToolbar => FullDamageReason::InlineToolbar, }) } diff --git a/src/backend/wayland/state/render/profile.rs b/src/backend/wayland/state/render/profile.rs index 7f4695105..eac887de6 100644 --- a/src/backend/wayland/state/render/profile.rs +++ b/src/backend/wayland/state/render/profile.rs @@ -17,12 +17,12 @@ pub(super) struct FrameProfile { } /// A flushed ARGB8888 target and its physical damage regions. -pub(super) struct PixelBuffer<'a> { - pub data: &'a mut [u8], +pub(super) struct PixelBuffer<'buffer> { + pub data: &'buffer mut [u8], pub width: i32, pub height: i32, pub stride: i32, - pub damage: &'a [Rect], + pub damage: &'buffer [Rect], } impl FrameProfile { @@ -55,7 +55,7 @@ impl FrameProfile { /// Returns whether Cairo must be notified of rewritten canvas pixels. pub(super) fn before_ui( &self, - pixels: PixelBuffer<'_>, + pixels: &mut PixelBuffer<'_>, baseline: &mut Vec, render_ui: bool, ) -> bool { @@ -83,7 +83,7 @@ impl FrameProfile { } /// Called after UI painting with the same visibility decision used by `before_ui`. - pub(super) fn after_ui(&self, pixels: PixelBuffer<'_>, baseline: &[u8], render_ui: bool) { + pub(super) fn after_ui(&self, pixels: &mut PixelBuffer<'_>, baseline: &[u8], render_ui: bool) { let Some(profile) = self.profile.as_ref() else { return; }; diff --git a/src/backend/wayland/state/render/profile/tests.rs b/src/backend/wayland/state/render/profile/tests.rs index ea2ae72e2..48cee6d29 100644 --- a/src/backend/wayland/state/render/profile/tests.rs +++ b/src/backend/wayland/state/render/profile/tests.rs @@ -79,7 +79,7 @@ fn profile_passes_preserve_canvas_and_ui_separation_with_partial_damage() { let mut baseline = Vec::new(); let rewritten = profile.needs_before_ui(true) && profile.before_ui( - pixels(&mut surface.data().unwrap(), &damage), + &mut pixels(&mut surface.data().unwrap(), &damage), &mut baseline, true, ); @@ -93,7 +93,7 @@ fn profile_passes_preserve_canvas_and_ui_separation_with_partial_damage() { ctx.fill().unwrap(); } profile.after_ui( - pixels(&mut surface.data().unwrap(), &damage), + &mut pixels(&mut surface.data().unwrap(), &damage), &baseline, true, ); @@ -109,13 +109,13 @@ fn suppressed_ui_does_not_copy_or_remap_a_stale_baseline() { let mut baseline = vec![0; 16]; assert!(!profile.needs_before_ui(false)); assert!(!profile.before_ui( - pixels(&mut surface.data().unwrap(), &damage), + &mut pixels(&mut surface.data().unwrap(), &damage), &mut baseline, false, )); assert_eq!(baseline, vec![0; 16]); profile.after_ui( - pixels(&mut surface.data().unwrap(), &damage), + &mut pixels(&mut surface.data().unwrap(), &damage), &baseline, false, ); @@ -131,12 +131,12 @@ fn absent_profile_does_not_remap_even_when_both_targets_are_enabled() { assert_eq!(profile.mode(), ProfileMode::Off); assert!(!profile.needs_before_ui(true)); assert!(!profile.before_ui( - pixels(&mut surface.data().unwrap(), &damage), + &mut pixels(&mut surface.data().unwrap(), &damage), &mut baseline, true, )); profile.after_ui( - pixels(&mut surface.data().unwrap(), &damage), + &mut pixels(&mut surface.data().unwrap(), &damage), &baseline, true, ); diff --git a/src/backend/wayland/state/render/recognition_chip.rs b/src/backend/wayland/state/render/recognition_chip.rs index 04cd8606c..e912fbdc4 100644 --- a/src/backend/wayland/state/render/recognition_chip.rs +++ b/src/backend/wayland/state/render/recognition_chip.rs @@ -1,4 +1,8 @@ -use super::super::*; +use crate::ui; +use crate::ui::RecognitionChipVisual; + +use super::super::WaylandState; +use std::time::Instant; impl WaylandState { /// Where the Shape Pen recognition chip sits this frame, beside the shape @@ -7,10 +11,10 @@ impl WaylandState { &self, width: u32, height: u32, - ) -> Option { + ) -> Option { let chip = self.input_state.recognition_chip()?; let anchor = self.input_state.screen_rect_for_canvas(chip.anchor())?; - crate::ui::recognition_chip_layout( + ui::recognition_chip_layout( self.render.ui_text(), chip.label(), ( @@ -27,7 +31,7 @@ impl WaylandState { pub(super) fn render_recognition_chip(&self, ctx: &cairo::Context, width: u32, height: u32) { if let Some(visual) = self.recognition_chip_visual(width, height) { - crate::ui::render_recognition_chip(self.render.ui_text(), ctx, &visual); + ui::render_recognition_chip(self.render.ui_text(), ctx, &visual); } } } diff --git a/src/backend/wayland/state/render/runtime.rs b/src/backend/wayland/state/render/runtime.rs index bc594c3ee..c56d89e93 100644 --- a/src/backend/wayland/state/render/runtime.rs +++ b/src/backend/wayland/state/render/runtime.rs @@ -1,3 +1,9 @@ +use std::mem; + +use crate::draw::{RenderCaches, TextMeasurer}; +use crate::ui::theme::Theme; +use crate::ui::{HelpContentCache, UiRenderCaches}; +use crate::ui_text::UiTextEngine; use crate::util::Rect; use super::super::canvas_layer::CanvasLayerCache; @@ -92,7 +98,7 @@ impl UiDamageHistory { current: Option, regions: &mut Vec, ) { - let previous = std::mem::replace(&mut self.prev[effect.index()], current); + let previous = mem::replace(&mut self.prev[effect.index()], current); match (previous, current) { (Some(previous), Some(current)) if previous == current => regions.push(current), (previous, current) => { @@ -110,7 +116,7 @@ impl UiDamageHistory { current: Option, regions: &mut Vec, ) { - let previous = std::mem::replace(&mut self.prev[effect.index()], current); + let previous = mem::replace(&mut self.prev[effect.index()], current); if previous != current { regions.extend(previous); regions.extend(current); @@ -157,28 +163,28 @@ impl UiDamageHistory { pub(in crate::backend::wayland) struct RenderRuntime { canvas_layer_cache: CanvasLayerCache, - draw_caches: crate::draw::RenderCaches, - theme: crate::ui::theme::Theme, - ui_caches: crate::ui::UiRenderCaches, - help_content: crate::ui::HelpContentCache, - ui_text: crate::ui_text::UiTextEngine, - text_measurer: crate::draw::TextMeasurer, + draw_caches: RenderCaches, + theme: Theme, + ui_caches: UiRenderCaches, + help_content: HelpContentCache, + ui_text: UiTextEngine, + text_measurer: TextMeasurer, ui_damage: UiDamageHistory, profile_ui_baseline: Vec, } impl RenderRuntime { pub(in crate::backend::wayland) fn new( - theme: crate::ui::theme::Theme, - ui_text: crate::ui_text::UiTextEngine, - text_measurer: crate::draw::TextMeasurer, + theme: Theme, + ui_text: UiTextEngine, + text_measurer: TextMeasurer, ) -> Self { Self { canvas_layer_cache: CanvasLayerCache::new(), - draw_caches: crate::draw::RenderCaches::default(), + draw_caches: RenderCaches::default(), theme, - ui_caches: crate::ui::UiRenderCaches::default(), - help_content: crate::ui::HelpContentCache::default(), + ui_caches: UiRenderCaches::default(), + help_content: HelpContentCache::default(), ui_text, text_measurer, ui_damage: UiDamageHistory::default(), @@ -186,35 +192,31 @@ impl RenderRuntime { } } - pub(in crate::backend::wayland) fn text_measurer(&self) -> &crate::draw::TextMeasurer { + pub(in crate::backend::wayland) fn text_measurer(&self) -> &TextMeasurer { &self.text_measurer } - pub(in crate::backend::wayland) fn ui_text(&self) -> &crate::ui_text::UiTextEngine { + pub(in crate::backend::wayland) fn ui_text(&self) -> &UiTextEngine { &self.ui_text } - pub(in crate::backend::wayland::state) fn theme(&self) -> &crate::ui::theme::Theme { + pub(in crate::backend::wayland::state) fn theme(&self) -> &Theme { &self.theme } pub(in crate::backend::wayland::state) fn ui_parts_with_text_mut( &mut self, - ) -> ( - &crate::ui::theme::Theme, - &mut crate::ui::UiRenderCaches, - &crate::ui_text::UiTextEngine, - ) { + ) -> (&Theme, &mut UiRenderCaches, &UiTextEngine) { (&self.theme, &mut self.ui_caches, &self.ui_text) } pub(in crate::backend::wayland::state) fn help_parts_mut( &mut self, ) -> ( - &crate::ui::theme::Theme, - &mut crate::ui::UiRenderCaches, - &crate::ui_text::UiTextEngine, - &mut crate::ui::HelpContentCache, + &Theme, + &mut UiRenderCaches, + &UiTextEngine, + &mut HelpContentCache, ) { ( &self.theme, @@ -232,17 +234,17 @@ impl RenderRuntime { pub(in crate::backend::wayland::state) fn draw_text_parts_mut( &mut self, - ) -> (&mut crate::draw::RenderCaches, &crate::draw::TextMeasurer) { + ) -> (&mut RenderCaches, &TextMeasurer) { (&mut self.draw_caches, &self.text_measurer) } pub(in crate::backend::wayland::state) fn draw_ui_text_parts_mut( &mut self, ) -> ( - &mut crate::draw::RenderCaches, - &mut crate::ui::UiRenderCaches, - &crate::ui_text::UiTextEngine, - &crate::draw::TextMeasurer, + &mut RenderCaches, + &mut UiRenderCaches, + &UiTextEngine, + &TextMeasurer, ) { ( &mut self.draw_caches, @@ -254,11 +256,7 @@ impl RenderRuntime { pub(in crate::backend::wayland::state) fn canvas_draw_parts_mut( &mut self, - ) -> ( - &mut CanvasLayerCache, - &mut crate::draw::RenderCaches, - &crate::draw::TextMeasurer, - ) { + ) -> (&mut CanvasLayerCache, &mut RenderCaches, &TextMeasurer) { ( &mut self.canvas_layer_cache, &mut self.draw_caches, @@ -281,6 +279,8 @@ impl RenderRuntime { #[cfg(test)] mod tests { + use crate::ui::theme::ThemeMode; + use super::super::tool_preview::mouse_tool_preview_damage_rect; use super::*; use crate::backend::wayland::state::buffer_damage::{BufferDamageTracker, FullDamageReason}; @@ -288,19 +288,19 @@ mod tests { #[test] fn runtime_themes_are_independent_of_other_owners() { let mut dark = RenderRuntime::new( - crate::ui::theme::Theme::resolve(crate::ui::theme::ThemeMode::Dark), - crate::ui_text::UiTextEngine::default(), - crate::draw::TextMeasurer::default(), + Theme::resolve(ThemeMode::Dark), + UiTextEngine::default(), + TextMeasurer::default(), ); let light = RenderRuntime::new( - crate::ui::theme::Theme::resolve(crate::ui::theme::ThemeMode::Light), - crate::ui_text::UiTextEngine::default(), - crate::draw::TextMeasurer::default(), + Theme::resolve(ThemeMode::Light), + UiTextEngine::default(), + TextMeasurer::default(), ); - assert_eq!(dark.theme(), &crate::ui::theme::Theme::dark()); - assert_eq!(light.theme(), &crate::ui::theme::Theme::light()); + assert_eq!(dark.theme(), &Theme::dark()); + assert_eq!(light.theme(), &Theme::light()); let (theme, _caches, _engine) = dark.ui_parts_with_text_mut(); - assert_eq!(theme, &crate::ui::theme::Theme::dark()); + assert_eq!(theme, &Theme::dark()); assert_ne!(dark.theme(), light.theme()); } diff --git a/src/backend/wayland/state/render/submit.rs b/src/backend/wayland/state/render/submit.rs index ee1cb07ff..b162f7dab 100644 --- a/src/backend/wayland/state/render/submit.rs +++ b/src/backend/wayland/state/render/submit.rs @@ -1,6 +1,13 @@ +use super::super::WaylandState; +use super::super::helpers::{damage_summary, debug_damage_logging_enabled}; +use super::super::perf::{PerfFrameDamageContext, PerfRenderBreakdown}; use super::plan::FramePlan; -use super::*; +use super::record_render_stage; use crate::backend::wayland::surface::AcquiredBuffer; +use anyhow::{Context, Result}; +use log::debug; +use std::time::Instant; +use wayland_client::QueueHandle; impl WaylandState { pub(super) fn submit_frame( @@ -66,7 +73,7 @@ impl WaylandState { } self.commit_perf_frame( - PerfFrameDamageContext { + &PerfFrameDamageContext { damage_screen: &plan.damage.screen, logical_width: width, logical_height: height, diff --git a/src/backend/wayland/state/render/tool_preview.rs b/src/backend/wayland/state/render/tool_preview.rs index ca71e94ff..2cb8337df 100644 --- a/src/backend/wayland/state/render/tool_preview.rs +++ b/src/backend/wayland/state/render/tool_preview.rs @@ -1,3 +1,5 @@ +use std::f64::consts; + use super::ui_effect_damage::effect_rect; use crate::draw::Color; use crate::input::Tool; @@ -80,14 +82,14 @@ pub(super) fn draw_tool_preview( let dot_cy = by + bubble_h / 2.0; let dot_r = dot_d / 2.0; ctx.set_source_rgba(dr, dg, db, 1.0); - ctx.arc(dot_cx, dot_cy, dot_r, 0.0, std::f64::consts::PI * 2.0); + ctx.arc(dot_cx, dot_cy, dot_r, 0.0, consts::PI * 2.0); let _ = ctx.fill(); // Auto-contrast outline keeps the dot legible against both its own color // and the dark bubble (see `theme::cursor_preview_outline`). theme::set_color(ctx, theme::cursor_preview_outline(dr, dg, db)); ctx.set_line_width(1.0); - ctx.arc(dot_cx, dot_cy, dot_r + 0.5, 0.0, std::f64::consts::PI * 2.0); + ctx.arc(dot_cx, dot_cy, dot_r + 0.5, 0.0, consts::PI * 2.0); let _ = ctx.stroke(); let _ = ctx.restore(); @@ -246,26 +248,20 @@ pub(super) fn draw_stylus_hover_cursor( let _ = ctx.save(); ctx.set_source_rgba(0.0, 0.0, 0.0, 0.35); - ctx.arc( - x + 1.0, - y + 1.0, - radius + 2.0, - 0.0, - std::f64::consts::PI * 2.0, - ); + ctx.arc(x + 1.0, y + 1.0, radius + 2.0, 0.0, consts::PI * 2.0); let _ = ctx.fill(); ctx.set_source_rgba(1.0, 1.0, 1.0, 0.9); - ctx.arc(x, y, radius + 1.4, 0.0, std::f64::consts::PI * 2.0); + ctx.arc(x, y, radius + 1.4, 0.0, consts::PI * 2.0); let _ = ctx.fill(); ctx.set_source_rgba(r, g, b, 0.95); - ctx.arc(x, y, radius, 0.0, std::f64::consts::PI * 2.0); + ctx.arc(x, y, radius, 0.0, consts::PI * 2.0); let _ = ctx.fill(); ctx.set_source_rgba(0.0, 0.0, 0.0, 0.72); ctx.set_line_width(1.0); - ctx.arc(x, y, radius + 1.4, 0.0, std::f64::consts::PI * 2.0); + ctx.arc(x, y, radius + 1.4, 0.0, consts::PI * 2.0); let _ = ctx.stroke(); let _ = ctx.restore(); } diff --git a/src/backend/wayland/state/render/ui.rs b/src/backend/wayland/state/render/ui.rs index eae6ce655..17ceb2300 100644 --- a/src/backend/wayland/state/render/ui.rs +++ b/src/backend/wayland/state/render/ui.rs @@ -1,9 +1,22 @@ +use crate::backend::wayland::capture::RegionCapturePhase; +use crate::backend::wayland::state::eyedropper; +use crate::backend::wayland::toolbar; +use crate::input::state::RegionPurposeTag; +use crate::ui; +use crate::ui::{ + CommandPaletteView, HelpContentOptions, RegionAction, RegionActionBar, + RegionCaptureLoupeVisual, RegionCutDragVisual, RegionResizeHandles, +}; + +use std::time::Instant; + +use super::super::WaylandState; use super::tool_preview::{draw_stylus_hover_cursor, draw_tool_preview, mouse_tool_preview_redraw}; -use super::*; use crate::backend::wayland::state::region_capture::RegionPickerMeasurement; use crate::backend::wayland::state::screen_image::{ displayed_screen_image, image_point_for_screen_point, screen_source_is, }; +use crate::input::DrawingState; impl WaylandState { pub(super) fn render_ui_layer( @@ -103,7 +116,7 @@ impl WaylandState { && !status_visible && fallback_visible { - crate::ui::render_frozen_badge_with_engine(self.render.ui_text(), ctx, width, height); + ui::render_frozen_badge_with_engine(self.render.ui_text(), ctx, width, height); } let mut offset = 0.0; if self.input_state.zoom_active() @@ -111,7 +124,7 @@ impl WaylandState { && !self.zoom_chip_visible() && fallback_visible { - offset += crate::ui::render_zoom_badge_with_engine( + offset += ui::render_zoom_badge_with_engine( self.render.ui_text(), ctx, width, @@ -126,7 +139,7 @@ impl WaylandState { && !status_visible && fallback_visible { - offset += crate::ui::render_pan_badge_with_engine( + offset += ui::render_pan_badge_with_engine( self.render.ui_text(), ctx, width, @@ -140,13 +153,7 @@ impl WaylandState { && !status_visible && fallback_visible { - crate::ui::render_editing_badge_with_engine( - self.render.ui_text(), - ctx, - width, - height, - offset, - ); + ui::render_editing_badge_with_engine(self.render.ui_text(), ctx, width, height, offset); } } @@ -158,7 +165,7 @@ impl WaylandState { capture_picker: bool, ) { if !capture_picker && self.input_state.floating_badge_visible() { - crate::ui::render_page_badge_with_engine( + ui::render_page_badge_with_engine( self.render.ui_text(), ctx, width, @@ -171,7 +178,7 @@ impl WaylandState { ); } if self.input_state.ui_visibility.show_status_bar { - crate::ui::render_status_bar_with_resources( + ui::render_status_bar_with_resources( self.render.ui_text(), ctx, self.render.theme(), @@ -181,11 +188,11 @@ impl WaylandState { height, ); if let Some(tooltip) = self.input_state.status_hud.tooltip() { - crate::ui::draw_tooltip(self.render.ui_text(), ctx, tooltip.text, tooltip.rect); + ui::draw_tooltip(self.render.ui_text(), ctx, tooltip.text, tooltip.rect); } } if !capture_picker && self.zoom_chip_visible() { - crate::ui::render_zoom_chip_with_resources( + ui::render_zoom_chip_with_resources( self.render.ui_text(), ctx, self.render.theme(), @@ -196,7 +203,7 @@ impl WaylandState { ); } if !capture_picker && self.input_state.input_hud_visible() { - crate::ui::render_input_hud_with_engine( + ui::render_input_hud_with_engine( self.render.ui_text(), ctx, &self.input_state, @@ -219,17 +226,19 @@ impl WaylandState { let (theme, caches, engine, content_cache) = self.render.help_parts_mut(); let content = content_cache.get_or_build( &self.input_state, - self.frozen.enabled(), - self.config.ui.help_overlay_context_filter, - self.input_state.boards.board_count() > 1, - self.config.capture.enabled, + HelpContentOptions { + frozen_enabled: self.frozen.enabled(), + context_filter: self.config.ui.help_overlay_context_filter, + board_enabled: self.input_state.boards.board_count() > 1, + capture_enabled: self.config.capture.enabled, + }, ); let mut render = crate::ui::UiRenderCtx { cairo: ctx, theme, caches, }; - crate::ui::render_help_overlay_result_with_content( + ui::render_help_overlay_result_with_content( engine, &mut render, &self.config.ui.help_overlay_style, @@ -250,7 +259,7 @@ impl WaylandState { .update_board_picker_layout(ctx, width, height); let (caches, ui_caches, engine, measurer) = self.render.draw_ui_text_parts_mut(); let mut render = crate::draw::RenderCtx::new(ctx, caches); - crate::ui::render_board_picker_with_halo( + ui::render_board_picker_with_halo( engine, measurer, &mut ui_caches.thumbnails, @@ -266,7 +275,7 @@ impl WaylandState { if !capture_picker && self.input_state.is_color_picker_popup_open() { self.input_state .update_color_picker_popup_layout(width, height); - crate::ui::render_color_picker_popup_with_engine( + ui::render_color_picker_popup_with_engine( self.render.ui_text(), ctx, &self.input_state, @@ -277,7 +286,7 @@ impl WaylandState { self.input_state.clear_color_picker_popup_layout(); } if !capture_picker && self.input_state.is_font_picker_open() { - crate::ui::render_font_picker(ctx, &self.input_state, width, height); + ui::render_font_picker(ctx, &self.input_state, width, height); } if !capture_picker && self.input_state.is_precision_entry_open() { self.render_precision_entry(ctx, width, height); @@ -290,14 +299,13 @@ impl WaylandState { fn render_precision_entry(&mut self, ctx: &cairo::Context, width: u32, height: u32) { let snapshot = self.toolbar_snapshot(); - let (_, top_h) = - crate::backend::wayland::toolbar::top_size(self.render.ui_text(), &snapshot); + let (_, top_h) = toolbar::top_size(self.render.ui_text(), &snapshot); let top_offset = self.toolbar_chrome.top_offset(); let anchor = ( self.inline_top_base_x() + top_offset.0, self.inline_top_base_y() + top_offset.1 + top_h as f64 + 8.0, ); - crate::ui::render_precision_entry_popup_with_engine( + ui::render_precision_entry_popup_with_engine( self.render.ui_text(), ctx, &self.input_state, @@ -318,7 +326,7 @@ impl WaylandState { self.input_state.update_radial_menu_layout(width, height); if self .input_state - .radial_menu_mark_painted_if_due(std::time::Instant::now()) + .radial_menu_mark_painted_if_due(Instant::now()) { let (theme, caches, engine) = self.render.ui_parts_with_text_mut(); let mut render = crate::ui::UiRenderCtx { @@ -326,7 +334,7 @@ impl WaylandState { theme, caches, }; - crate::ui::render_radial_menu_with_context( + ui::render_radial_menu_with_context( engine, &mut render, &self.input_state, @@ -340,7 +348,7 @@ impl WaylandState { if !capture_picker { self.render_recognition_chip(ctx, width, height); } - let toast_geometry = crate::ui::render_ui_toast_with_engine( + let toast_geometry = ui::render_ui_toast_with_engine( self.render.ui_text(), ctx, &self.input_state, @@ -353,14 +361,14 @@ impl WaylandState { .map(|geometry| geometry.1) .unwrap_or([None, None]), ); - crate::ui::render_preset_toast_with_engine( + ui::render_preset_toast_with_engine( self.render.ui_text(), ctx, &self.input_state, width, height, ); - crate::ui::render_blocked_feedback(ctx, &self.input_state, width, height); + ui::render_blocked_feedback(ctx, &self.input_state, width, height); } fn render_properties_panel( @@ -384,7 +392,7 @@ impl WaylandState { width, height, ); - crate::ui::render_properties_panel_with_engine( + ui::render_properties_panel_with_engine( self.render.ui_text(), ctx, &self.input_state, @@ -404,7 +412,7 @@ impl WaylandState { if !self.input_state.is_context_menu_open() { self.input_state.clear_context_menu_layout(); } - crate::ui::render_context_menu_with_engine(self.render.ui_text(), ctx, &self.input_state); + ui::render_context_menu_with_engine(self.render.ui_text(), ctx, &self.input_state); } fn render_inline_toolbar_chrome(&mut self, ctx: &cairo::Context, capture_picker: bool) { @@ -429,9 +437,7 @@ impl WaylandState { height: u32, capture_picker: bool, ) { - if self.input_state.region_state().purpose() - == Some(crate::input::state::RegionPurposeTag::Measure) - { + if self.input_state.region_state().purpose() == Some(RegionPurposeTag::Measure) { self.render_capture_picker(ctx, width, height); } self.render_ocr_scan(ctx, width, height); @@ -440,7 +446,7 @@ impl WaylandState { return; } let card_layout = self.first_run_onboarding_card().map(|card| { - crate::ui::render_onboarding_card_with_engine( + ui::render_onboarding_card_with_engine( self.render.ui_text(), ctx, width, @@ -460,8 +466,8 @@ impl WaylandState { .ui_damage_mut() .record_painted(super::UiEffect::OnboardingCard, painted_card); self.onboarding_card.set_layout(card_layout); - let palette_view = crate::ui::CommandPaletteView::prepare(&self.input_state, width, height); - crate::ui::paint_command_palette( + let palette_view = CommandPaletteView::prepare(&self.input_state, width, height); + ui::paint_command_palette( self.render.theme(), self.render.ui_text(), ctx, @@ -478,9 +484,9 @@ impl WaylandState { let Some(scan) = self.input_state.ocr_scan() else { return; }; - let now = std::time::Instant::now(); + let now = Instant::now(); if let Some((outcome, shown)) = scan.result(now) { - crate::ui::render_ocr_scan_result( + ui::render_ocr_scan_result( self.render.ui_text(), ctx, scan.region(), @@ -489,33 +495,30 @@ impl WaylandState { (width, height), ); } else if let Some(progress) = scan.sweep_progress(now) { - crate::ui::render_ocr_scan_sweep(ctx, scan.region(), progress); + ui::render_ocr_scan_sweep(ctx, scan.region(), progress); } else if scan.is_scanning() { // Reduced motion: the region is still marked as being read, just // without a band travelling across it. - crate::ui::render_ocr_scan_still(ctx, scan.region()); + ui::render_ocr_scan_still(ctx, scan.region()); } } fn render_capture_picker(&self, ctx: &cairo::Context, width: u32, height: u32) { let purpose = self.input_state.region_state().purpose(); if !self.input_state.region_is_active() - || !purpose.is_some_and(|purpose| { - purpose.is_capture() || purpose == crate::input::state::RegionPurposeTag::Measure - }) + || !purpose + .is_some_and(|purpose| purpose.is_capture() || purpose == RegionPurposeTag::Measure) { return; } - let measure_mode = purpose == Some(crate::input::state::RegionPurposeTag::Measure); + let measure_mode = purpose == Some(RegionPurposeTag::Measure); let options = match self.capture.region_phase() { - crate::backend::wayland::capture::RegionCapturePhase::Reserved(intent) => { - Some(intent.options()) - } - crate::backend::wayland::capture::RegionCapturePhase::Idle - | crate::backend::wayland::capture::RegionCapturePhase::Submitting(_) - | crate::backend::wayland::capture::RegionCapturePhase::Accepted => None, + RegionCapturePhase::Reserved(intent) => Some(intent.options()), + RegionCapturePhase::Idle + | RegionCapturePhase::Submitting(_) + | RegionCapturePhase::Accepted => None, }; let geometry = self.region_selection_geometry(); let selection = if measure_mode { @@ -532,29 +535,25 @@ impl WaylandState { .map(|measurement| match measurement { RegionPickerMeasurement::Point { x, y } => format!("{x}, {y}"), RegionPickerMeasurement::Size { width, height } => { - crate::ui::capture_size_text((width, height)) + ui::capture_size_text((width, height)) } }); let region_state = self.input_state.region_state(); let action_bar = if region_state.is_review() { geometry.map(|geometry| { - crate::ui::RegionActionBar::place(geometry.display_selection(), (width, height)) + RegionActionBar::place(geometry.display_selection(), (width, height)) }) } else { None }; - let hovered_action: Option = - action_bar.and_then(|bar| bar.hit(pointer)); + let hovered_action: Option = action_bar.and_then(|bar| bar.hit(pointer)); // Grips belong to the reviewed rectangle, not the window-mode // candidate, so they follow `region_selection_geometry` rather than // the effective selection the scrim cuts out. let resize_handles = (region_state.is_review() && !self.region_review_crop_locked() && !self.region_cut_mode_armed()) - .then(|| { - geometry - .map(|geometry| crate::ui::RegionResizeHandles::place(geometry.display_selection())) - }); + .then(|| geometry.map(|geometry| RegionResizeHandles::place(geometry.display_selection()))); let resize_handles = resize_handles.flatten(); // A grip sitting under the action bar is painted over by it, so the // bar's hover wins the pointer. @@ -580,7 +579,7 @@ impl WaylandState { }; let loupe = loupe_source.as_ref().and_then(|(_source, token)| { let image_point = image_point_for_screen_point(token, pointer); - crate::ui::RegionCaptureLoupeVisual::when_enabled( + RegionCaptureLoupeVisual::when_enabled( loupe_enabled, pointer, (image_point.x, image_point.y), @@ -593,7 +592,7 @@ impl WaylandState { highlighted_target: self.region_window_snap_highlighted_index(), }; - crate::ui::render_region_capture_picker( + ui::render_region_capture_picker( self.render.ui_text(), ctx, width, @@ -621,7 +620,7 @@ impl WaylandState { }), drag: self .region_cut_drag_overlay() - .map(|(axis, band)| crate::ui::RegionCutDragVisual { axis, band }), + .map(|(axis, band)| RegionCutDragVisual { axis, band }), availability: self.region_cut_availability(), cut_armed: self.region_cut_mode_armed(), status: self.region_cut_status(), @@ -630,11 +629,7 @@ impl WaylandState { }, |image_x, image_y| { loupe_source.as_ref().and_then(|(source, _token)| { - crate::backend::wayland::state::eyedropper::sample_at( - source.image, - image_x, - image_y, - ) + eyedropper::sample_at(source.image, image_x, image_y) }) }, ); diff --git a/src/backend/wayland/state/render/ui_effect_damage.rs b/src/backend/wayland/state/render/ui_effect_damage.rs index 9ad05ddbf..ace7da4a0 100644 --- a/src/backend/wayland/state/render/ui_effect_damage.rs +++ b/src/backend/wayland/state/render/ui_effect_damage.rs @@ -6,10 +6,16 @@ //! with the footprint from the previous frame so moves, content changes, and //! disappearance are cleaned up correctly. -use super::super::*; +use crate::input::state::RegionPurposeTag; +use crate::ui; +use crate::ui_text::UiTextEngine; + +use super::super::WaylandState; +use super::runtime::{UiEffect, UiEffectFlags}; use super::tool_preview::mouse_tool_preview_damage_rect; -use super::{UiEffect, UiEffectFlags}; +use crate::input::{DrawingState, InputState}; use crate::util::Rect; +use std::time::Instant; /// Safety margin around effect bounds for anti-aliasing bleed. const UI_EFFECT_DAMAGE_MARGIN: i32 = 2; @@ -29,12 +35,12 @@ pub(super) fn effect_rect(bounds: (f64, f64, f64, f64), width: u32, height: u32) } fn color_picker_effect_rect( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, input_state: &InputState, width: u32, height: u32, ) -> Option { - crate::ui::color_picker_popup_visual_geometry_with_engine(engine, input_state, width, height) + ui::color_picker_popup_visual_geometry_with_engine(engine, input_state, width, height) .and_then(|bounds| effect_rect(bounds, width, height)) } @@ -93,7 +99,7 @@ impl WaylandState { let mut regions = Vec::new(); let toast_rect = if flags.active(UiEffect::UiToast) { - crate::ui::ui_toast_geometry_with_engine( + ui::ui_toast_geometry_with_engine( self.render.ui_text(), &self.input_state, width, @@ -108,7 +114,7 @@ impl WaylandState { .roll(UiEffect::UiToast, toast_rect, &mut regions); let preset_rect = if flags.active(UiEffect::PresetToast) { - crate::ui::preset_toast_geometry_with_engine( + ui::preset_toast_geometry_with_engine( self.render.ui_text(), &self.input_state, width, @@ -128,7 +134,7 @@ impl WaylandState { .roll_blocked_feedback(flags.blocked_feedback()) { regions.extend( - crate::ui::blocked_feedback_rects(width, height) + ui::blocked_feedback_rects(width, height) .into_iter() .filter_map(|bounds| effect_rect(bounds, width, height)), ); @@ -161,7 +167,7 @@ impl WaylandState { height, chrome_cursor_focused, ); - crate::ui::status_hud_geometry(&self.input_state, width, height) + ui::status_hud_geometry(&self.input_state, width, height) .and_then(|bounds| effect_rect(bounds, width, height)) } else { self.input_state.clear_status_hud_layout(); @@ -206,7 +212,7 @@ impl WaylandState { height, chrome_cursor_focused, ); - crate::ui::zoom_chip_geometry(&self.input_state, width, height) + ui::zoom_chip_geometry(&self.input_state, width, height) .and_then(|bounds| effect_rect(bounds, width, height)) } else { self.input_state.clear_zoom_chip_layout(); @@ -220,7 +226,7 @@ impl WaylandState { // the same appear → resize → disappear union keeps the stale chips // cleaned up without escalating a keystroke to the full surface. let input_hud_rect = if flags.active(UiEffect::InputHud) { - crate::ui::input_hud_geometry_with_engine( + ui::input_hud_geometry_with_engine( self.render.ui_text(), &self.input_state, width, @@ -239,7 +245,7 @@ impl WaylandState { // optional action tooltip change, so typing and selection no longer // fall through to the full-surface empty-damage fallback. let command_palette_rect = if flags.active(UiEffect::CommandPalette) { - crate::ui::command_palette_visual_geometry_with_engine( + ui::command_palette_visual_geometry_with_engine( self.render.ui_text(), &self.input_state, width, @@ -279,9 +285,9 @@ impl WaylandState { height, ); ( - crate::ui::context_menu_visual_geometry(&self.input_state) + ui::context_menu_visual_geometry(&self.input_state) .and_then(|bounds| effect_rect(bounds, width, height)), - crate::ui::context_submenu_visual_geometry(&self.input_state) + ui::context_submenu_visual_geometry(&self.input_state) .and_then(|bounds| effect_rect(bounds, width, height)), ) } else { @@ -356,11 +362,9 @@ impl WaylandState { // still has to be damaged and cleared even though it asks for no // frames of its own, so this keys off the overlay itself. let ocr_scan_rect = self.input_state.ocr_scan().and_then(|scan| { - let outcome = scan - .result(std::time::Instant::now()) - .map(|(outcome, _)| outcome); + let outcome = scan.result(Instant::now()).map(|(outcome, _)| outcome); effect_rect( - crate::ui::ocr_scan_geometry( + ui::ocr_scan_geometry( self.render.ui_text(), scan.region(), outcome, @@ -374,18 +378,17 @@ impl WaylandState { .ui_damage_mut() .roll(UiEffect::OcrScan, ocr_scan_rect, &mut regions); - let measure_picker_damage = if self.input_state.region_state().purpose() - == Some(crate::input::state::RegionPurposeTag::Measure) - { - let (pointer_x, pointer_y) = self.pointer.position(); - crate::ui::measure_picker_damage( - self.input_state.region_state().selection(), - (f64::from(pointer_x), f64::from(pointer_y)), - (width, height), - ) - } else { - Vec::new() - }; + let measure_picker_damage = + if self.input_state.region_state().purpose() == Some(RegionPurposeTag::Measure) { + let (pointer_x, pointer_y) = self.pointer.position(); + ui::measure_picker_damage( + self.input_state.region_state().selection(), + (f64::from(pointer_x), f64::from(pointer_y)), + (width, height), + ) + } else { + Vec::new() + }; self.render .ui_damage_mut() .roll_measure_picker(measure_picker_damage, &mut regions); @@ -420,6 +423,11 @@ impl WaylandState { #[cfg(test)] mod tests { + use crate::config::InputHudConfig; + use crate::input::state::{InputHudSettings, test_support}; + use crate::input::{Key, Modifiers, state}; + use crate::ui::ShapeReadout; + use super::*; #[test] @@ -441,19 +449,19 @@ mod tests { #[test] fn shape_badge_damage_unions_appear_move_and_disappear_footprints() { - let first = crate::ui::measure_shape_badge( - &crate::ui_text::UiTextEngine::default(), + let first = ui::measure_shape_badge( + &UiTextEngine::default(), true, - crate::ui::ShapeReadout::size(20, 30), + ShapeReadout::size(20, 30), (100.0, 100.0), 800, 600, ) .and_then(|badge| effect_rect(badge.bounds, 800, 600)); - let second = crate::ui::measure_shape_badge( - &crate::ui_text::UiTextEngine::default(), + let second = ui::measure_shape_badge( + &UiTextEngine::default(), true, - crate::ui::ShapeReadout::size(200, 300), + ShapeReadout::size(200, 300), (300.0, 250.0), 800, 600, @@ -478,8 +486,8 @@ mod tests { #[test] fn recognition_chip_damage_stays_on_its_own_footprint_until_it_expires() { - let chip = crate::ui::recognition_chip_layout( - &crate::ui_text::UiTextEngine::default(), + let chip = ui::recognition_chip_layout( + &UiTextEngine::default(), "Circle · Ctrl+Z keeps ink", (300.0, 100.0, 200.0, 150.0), 0.5, @@ -609,16 +617,14 @@ mod tests { /// keystroke to the whole surface. #[test] fn input_hud_damage_is_a_compact_row_not_the_full_surface() { - let mut input = crate::input::state::test_support::make_test_input_state(); - input.init_input_hud_from_config(crate::input::state::InputHudSettings::from( - &crate::config::InputHudConfig { - enabled: true, - ..crate::config::InputHudConfig::default() - }, - )); - input.note_input_hud_key(crate::input::Key::Escape, crate::input::Modifiers::new()); - - let bounds = crate::ui::input_hud_geometry(&input, 1920, 1080).expect("row geometry"); + let mut input = test_support::make_test_input_state(); + input.init_input_hud_from_config(InputHudSettings::from(&InputHudConfig { + enabled: true, + ..InputHudConfig::default() + })); + input.note_input_hud_key(Key::Escape, Modifiers::new()); + + let bounds = ui::input_hud_geometry(&input, 1920, 1080).expect("row geometry"); let damage = effect_rect(bounds, 1920, 1080).expect("row damage"); assert!(damage.width * damage.height < 1920 * 1080 / 10); } @@ -649,12 +655,11 @@ mod tests { #[test] fn color_picker_typing_damage_is_compact_instead_of_full_screen() { - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); input.open_color_picker_popup(); - let damage = - color_picker_effect_rect(&crate::ui_text::UiTextEngine::default(), &input, 1920, 1080) - .expect("popup damage"); + let damage = color_picker_effect_rect(&UiTextEngine::default(), &input, 1920, 1080) + .expect("popup damage"); // Before targeted popup damage, an ordinary key used the renderer's // 1920x1080 empty-damage fallback (2,073,600 pixels). Now it is the @@ -662,8 +667,8 @@ mod tests { // from the layout constants rather than hard-coded, so changing the // panel's size does not fail this test for the wrong reason. const MARGIN: i32 = 2; - let panel_w = crate::input::state::COLOR_PICKER_POPUP_WIDTH as i32; - let panel_h = crate::input::state::COLOR_PICKER_POPUP_HEIGHT as i32; + let panel_w = state::COLOR_PICKER_POPUP_WIDTH as i32; + let panel_h = state::COLOR_PICKER_POPUP_HEIGHT as i32; let expected = Rect::new( (1920 - panel_w) / 2 - MARGIN, (1080 - panel_h) / 2 - MARGIN, @@ -677,7 +682,7 @@ mod tests { #[test] fn color_picker_damage_includes_the_hovered_action_tooltip() { - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); input.open_color_picker_popup(); input.update_color_picker_popup_layout(1920, 1080); let layout = input.color_picker_popup_layout().expect("popup layout"); @@ -686,9 +691,8 @@ mod tests { layout.eyedropper_btn_y + layout.action_btn_size / 2.0, ))); - let damage = - color_picker_effect_rect(&crate::ui_text::UiTextEngine::default(), &input, 1920, 1080) - .expect("tooltip damage"); + let damage = color_picker_effect_rect(&UiTextEngine::default(), &input, 1920, 1080) + .expect("tooltip damage"); assert!(damage.width > 304); assert!(damage.width < 1920); diff --git a/src/backend/wayland/state/screen_image.rs b/src/backend/wayland/state/screen_image.rs index f912207a2..45173f82d 100644 --- a/src/backend/wayland/state/screen_image.rs +++ b/src/backend/wayland/state/screen_image.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use crate::backend::wayland::frozen::{FrozenImage, FrozenState, ScreenImageProvenance}; use crate::backend::wayland::zoom::ZoomState; -use crate::screen_pixels::{ImagePixelRect, ImagePoint, PixelSpan}; +use crate::screen_pixels::{ImagePixelRect, ImagePoint, PackedArgb32, PixelSpan}; use crate::util::Rect; use wayland_client::protocol::wl_output; @@ -20,8 +20,8 @@ pub(super) enum ScreenImageKind { } /// The captured image that the canvas renderer actually displays. -pub(super) struct DisplayedScreenImage<'a> { - pub image: &'a FrozenImage, +pub(super) struct DisplayedScreenImage<'image> { + pub image: &'image FrozenImage, pub kind: ScreenImageKind, pub provenance: ScreenImageProvenance, /// The renderer is painting this image through the zoom transform, so @@ -68,13 +68,28 @@ pub(super) enum ScreenSourceEntry { ZoomImageUnavailable, } -pub(super) fn screen_source_entry( - has_source: bool, - board_is_transparent: bool, - zoom_engaged: bool, - zoom_active: bool, - frozen_enabled: bool, -) -> ScreenSourceEntry { +/// The overlay state that decides how a screen-image modal can start. +#[derive(Debug, Clone, Copy)] +pub(super) struct ScreenSourceConditions { + /// A displayed screen image is ready to read. + pub(super) has_source: bool, + pub(super) board_is_transparent: bool, + /// Zoom is active or waiting for its activation capture. + pub(super) zoom_engaged: bool, + pub(super) zoom_active: bool, + /// Frozen capture is available for a temporary freeze. + pub(super) frozen_enabled: bool, +} + +pub(super) fn screen_source_entry(conditions: ScreenSourceConditions) -> ScreenSourceEntry { + let ScreenSourceConditions { + has_source, + board_is_transparent, + zoom_engaged, + zoom_active, + frozen_enabled, + } = conditions; + if has_source { ScreenSourceEntry::Activate } else if !board_is_transparent && zoom_engaged { @@ -347,7 +362,7 @@ fn image_rect_from_points( pub(super) fn copy_image_rect( source: &FrozenImage, rect: ImagePixelRect, -) -> Result { +) -> Result { if source.width == 0 || source.height == 0 { return Err(CropError::Empty); } @@ -356,6 +371,8 @@ pub(super) fn copy_image_rect( #[cfg(test)] mod tests { + use std::ptr; + use super::*; use crate::backend::wayland::frozen::ScreenImageProvenance; @@ -372,7 +389,7 @@ mod tests { let mut data = Vec::with_capacity((width * height * 4) as usize); for y in 0..height { for x in 0..width { - data.extend_from_slice(&[x as u8, y as u8, 0, 0xFF]); + data.extend_from_slice(&[x as u8, y as u8, 0, 0xff]); } } image(width, height, (width * 4) as i32, data) @@ -447,7 +464,7 @@ mod tests { let shared = shared_displayed_screen_image(&zoom, &frozen, source.kind).unwrap(); - assert!(std::ptr::eq(shared.as_ref(), source.image)); + assert!(ptr::eq(shared.as_ref(), source.image)); assert_eq!(shared.data.as_ptr(), source.image.data.as_ptr()); } @@ -852,7 +869,7 @@ mod tests { // 2x3, and the crop must read that post-transform layout. let mut data = Vec::new(); for value in [1u8, 2, 3, 4, 5, 6] { - data.extend_from_slice(&[value, 0, 0, 0xFF]); + data.extend_from_slice(&[value, 0, 0, 0xff]); } let rotated = image(3, 2, 12, data) .with_output_transform(wl_output::Transform::_270) diff --git a/src/backend/wayland/state/tablet_runtime.rs b/src/backend/wayland/state/tablet_runtime.rs index 9936ccde0..e24f2fc84 100644 --- a/src/backend/wayland/state/tablet_runtime.rs +++ b/src/backend/wayland/state/tablet_runtime.rs @@ -1,18 +1,25 @@ use std::collections::HashMap; +use std::mem; -use wayland_client::{backend::ObjectId, protocol::wl_surface}; -use wayland_protocols::wp::tablet::zv2::client::{ - zwp_tablet_manager_v2::ZwpTabletManagerV2, zwp_tablet_pad_group_v2::ZwpTabletPadGroupV2, - zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2, zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2, - zwp_tablet_pad_v2::ZwpTabletPadV2, zwp_tablet_seat_v2::ZwpTabletSeatV2, - zwp_tablet_tool_v2::ZwpTabletToolV2, zwp_tablet_v2::ZwpTabletV2, -}; +use wayland_client::backend::ObjectId; +use wayland_client::protocol::wl_surface; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_manager_v2::ZwpTabletManagerV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_group_v2::ZwpTabletPadGroupV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_pad_v2::ZwpTabletPadV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_seat_v2::ZwpTabletSeatV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::ZwpTabletToolV2; +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_v2::ZwpTabletV2; + +use crate::input::state::ToastPress; use super::PendingStylusFrame; use super::contact_owner::{ContactMotion, ContactOwner, ReleaseRoute, held_motion, release_route}; use crate::backend::wayland::TabletToolType; -use crate::input::{Tool, tablet::TabletSettings}; +use crate::input::Tool; +use crate::input::tablet::TabletSettings; mod pressure; pub(in crate::backend::wayland) use pressure::StylusDownAdmission; @@ -45,7 +52,7 @@ pub(in crate::backend::wayland) struct TabletState { pub(in crate::backend::wayland) pending_frame: PendingStylusFrame, pub(in crate::backend::wayland) contact_retired: bool, /// A tip-down on a toast, resolved when the tip lifts. - pub(in crate::backend::wayland) toast_press: Option, + pub(in crate::backend::wayland) toast_press: Option, pub(in crate::backend::wayland) tool_types: HashMap, pub(in crate::backend::wayland) auto_switched_to_eraser: bool, pub(in crate::backend::wayland) pre_eraser_tool_override: Option, @@ -193,7 +200,7 @@ impl TabletState { } pub(in crate::backend::wayland) fn take_retired_contact(&mut self) -> bool { - std::mem::take(&mut self.contact_retired) + mem::take(&mut self.contact_retired) } pub(super) fn new(manager: Option, settings: TabletSettings) -> Self { diff --git a/src/backend/wayland/state/tablet_runtime/pressure.rs b/src/backend/wayland/state/tablet_runtime/pressure.rs index a8826ab8a..541d5d5bf 100644 --- a/src/backend/wayland/state/tablet_runtime/pressure.rs +++ b/src/backend/wayland/state/tablet_runtime/pressure.rs @@ -1,6 +1,9 @@ +use crate::input::DrawingState; + use super::TabletState; use crate::draw::TextMeasurer; -use crate::input::{InputState, tablet::try_apply_pressure_to_state_with}; +use crate::input::InputState; +use crate::input::tablet::try_apply_pressure_to_state_with; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(in crate::backend::wayland) enum StylusDownAdmission { @@ -55,7 +58,7 @@ impl TabletState { || !self.is_canvas_gesture() || input.screen_modal_is_active() || input.command_palette_is_engaged() - || !matches!(input.state, crate::input::DrawingState::Drawing { .. }) + || !matches!(input.state, DrawingState::Drawing { .. }) || pressure == 0 { return; @@ -84,11 +87,13 @@ impl TabletState { #[cfg(test)] mod tests { + use std::mem; + use super::*; use crate::backend::wayland::state::contact_owner::ContactOwner; - use crate::input::{ - MouseButton, state::test_support::make_test_input_state, tablet::TabletSettings, - }; + use crate::input::MouseButton; + use crate::input::state::test_support::make_test_input_state; + use crate::input::tablet::TabletSettings; #[test] fn a_hard_strip_tap_cannot_change_tool_thickness_or_the_next_light_canvas_stroke() { @@ -122,7 +127,7 @@ mod tests { input.on_mouse_press(MouseButton::Left, 600, 400); tablet.pending_frame.pressure = Some(4096); tablet.pending_frame.down = true; - let pending = std::mem::take(&mut tablet.pending_frame); + let pending = mem::take(&mut tablet.pending_frame); tablet.bind_tip(ContactOwner::Canvas); tablet.apply_canvas_pressure(&measurer, &mut input, pending.pressure.unwrap()); assert!(tablet.peak_thickness.unwrap() < 3.0); diff --git a/src/backend/wayland/state/tests.rs b/src/backend/wayland/state/tests.rs index 86891e242..034fd7297 100644 --- a/src/backend/wayland/state/tests.rs +++ b/src/backend/wayland/state/tests.rs @@ -1,5 +1,4 @@ -use super::helpers; -use super::*; +use super::{helpers, *}; use crate::util::Rect; // NOTE: The renderer uses per-buffer damage tracking via BufferDamageTracker. diff --git a/src/backend/wayland/state/text_clipboard.rs b/src/backend/wayland/state/text_clipboard.rs index 9b2d036e0..17ef6a319 100644 --- a/src/backend/wayland/state/text_clipboard.rs +++ b/src/backend/wayland/state/text_clipboard.rs @@ -8,6 +8,8 @@ //! handlers, drained each input cycle, fulfill it against the compositor //! clipboard (reusing the generic clipboard worker pipeline). +use std::time::Instant; + use super::{TextCopyOutcome, TextPasteOutcome, WaylandState}; use crate::backend::wayland::RuntimeOperationPoll; use crate::i18n::tr; @@ -21,7 +23,7 @@ impl WaylandState { return; } self.focus - .suppress_exit_for(std::time::Instant::now(), Duration::from_millis(1500)); + .suppress_exit_for(Instant::now(), Duration::from_millis(1500)); if let Err(err) = self.clipboard.queue_text_copy(request) { log::warn!("Failed to start text clipboard copy: {err}"); self.input_state.push_toast( @@ -99,7 +101,7 @@ impl WaylandState { return; } self.focus - .suppress_exit_for(std::time::Instant::now(), Duration::from_millis(1500)); + .suppress_exit_for(Instant::now(), Duration::from_millis(1500)); if let Err(err) = self.clipboard.queue_text_paste(target) { log::warn!("Failed to start text clipboard paste: {err}"); self.push_text_paste_failure(); diff --git a/src/backend/wayland/state/text_input.rs b/src/backend/wayland/state/text_input.rs index 44a977c99..8395bddd9 100644 --- a/src/backend/wayland/state/text_input.rs +++ b/src/backend/wayland/state/text_input.rs @@ -1,7 +1,7 @@ -use wayland_client::{QueueHandle, protocol::wl_seat}; -use wayland_protocols::wp::text_input::zv3::client::{ - zwp_text_input_manager_v3::ZwpTextInputManagerV3, zwp_text_input_v3::ZwpTextInputV3, -}; +use wayland_client::QueueHandle; +use wayland_client::protocol::wl_seat; +use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; +use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3; use super::WaylandState; diff --git a/src/backend/wayland/state/toolbar.rs b/src/backend/wayland/state/toolbar.rs index 8bee3a81c..220bdfb21 100644 --- a/src/backend/wayland/state/toolbar.rs +++ b/src/backend/wayland/state/toolbar.rs @@ -1,8 +1,18 @@ +use super::super::toolbar::layout::top_size; +use super::super::toolbar_intent::intent_to_event; +use super::WaylandState; +use super::helpers::{ + debug_toolbar_drag_logging_enabled, drag_log, toolbar_drag_throttle_interval, +}; +use crate::input::DrawingState; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; #[allow(unused_imports)] -use super::*; +use anyhow::Result; +use log::debug; +use std::time::{Duration, Instant}; mod chrome; -pub(in crate::backend::wayland) use chrome::{ConfigureVerdict, ToolbarChrome}; +pub(in crate::backend::wayland) use chrome::ToolbarChrome; mod drag; pub(in crate::backend::wayland) use drag::{MoveDragKind, ToolbarDrag}; mod events; diff --git a/src/backend/wayland/state/toolbar/chrome.rs b/src/backend/wayland/state/toolbar/chrome.rs index f5b74c853..fb04b4dd1 100644 --- a/src/backend/wayland/state/toolbar/chrome.rs +++ b/src/backend/wayland/state/toolbar/chrome.rs @@ -1,20 +1,16 @@ use std::time::{Duration, Instant}; -use crate::{ - backend::wayland::{ - toolbar::{ - ToolbarCursorHint, - hit::{ - HitRegion, drag_intent_for_hit, find_hit, focus_hover_point, focused_event, - intent_for_hit, next_focus_index, quick_color_slot_for_hit, resolve_focus_index, - resolve_hit_index, - }, - render::TOOLTIP_DELAY, - }, - toolbar_intent::ToolbarIntent, - }, - ui::toolbar::{ToolbarEvent, snapshot::fade::TopStripFade}, +use crate::backend::wayland::toolbar::ToolbarCursorHint; +use crate::backend::wayland::toolbar::hit::{ + HitRegion, drag_intent_for_hit, find_hit, focus_hover_point, focused_event, intent_for_hit, + next_focus_index, quick_color_slot_for_hit, resolve_focus_index, resolve_hit_index, }; +use crate::backend::wayland::toolbar::render::TOOLTIP_DELAY; +use crate::backend::wayland::toolbar_intent::ToolbarIntent; +use crate::ui::toolbar::ToolbarEvent; +use crate::ui::toolbar::model::StylePillSlider; +use crate::ui::toolbar::snapshot::fade; +use crate::ui::toolbar::snapshot::fade::TopStripFade; use super::fade::StripRevealKey; @@ -200,7 +196,7 @@ pub(in crate::backend::wayland) struct ToolbarChrome { focus_active: bool, inline: InlineTopStrip, meter_wheel: super::meter_wheel::MeterWheel, - slider_wheel: super::meter_wheel::MeterWheel, + slider_wheel: super::meter_wheel::MeterWheel, } impl ToolbarChrome { @@ -237,13 +233,13 @@ impl ToolbarChrome { /// Partial wheel travel over a style-pill slider or its numeral. pub(in crate::backend::wayland) fn slider_wheel( &self, - ) -> &super::meter_wheel::MeterWheel { + ) -> &super::meter_wheel::MeterWheel { &self.slider_wheel } pub(in crate::backend::wayland) fn slider_wheel_mut( &mut self, - ) -> &mut super::meter_wheel::MeterWheel { + ) -> &mut super::meter_wheel::MeterWheel { &mut self.slider_wheel } @@ -363,8 +359,7 @@ impl ToolbarChrome { /// Whether an inline-strip position takes toolbar input. The idle-hidden /// strip takes none, so a click on its invisible area draws instead. pub(in crate::backend::wayland) fn inline_contains(&self, position: (f64, f64)) -> bool { - !crate::ui::toolbar::snapshot::fade::top_strip_hidden(self.top_strip_fade.value()) - && self.inline.contains(position) + !fade::top_strip_hidden(self.top_strip_fade.value()) && self.inline.contains(position) } pub(in crate::backend::wayland) fn inline_primary_hit_at( diff --git a/src/backend/wayland/state/toolbar/drag/base.rs b/src/backend/wayland/state/toolbar/drag/base.rs index 748dae4b5..89d4b5848 100644 --- a/src/backend/wayland/state/toolbar/drag/base.rs +++ b/src/backend/wayland/state/toolbar/drag/base.rs @@ -1,4 +1,5 @@ -use super::*; +use super::super::{WaylandState, drag_log}; +use super::state::MoveDragKind; impl WaylandState { /// Base X position for the top toolbar when laid out inline. diff --git a/src/backend/wayland/state/toolbar/drag/clamp.rs b/src/backend/wayland/state/toolbar/drag/clamp.rs index 3b3724884..9aaeb0c4d 100644 --- a/src/backend/wayland/state/toolbar/drag/clamp.rs +++ b/src/backend/wayland/state/toolbar/drag/clamp.rs @@ -1,6 +1,9 @@ use std::time::Instant; -use super::*; +use super::super::{ + ToolbarSnapshot, WaylandState, debug, debug_toolbar_drag_logging_enabled, drag_log, geometry, + toolbar_drag_throttle_interval, top_size, +}; impl WaylandState { pub(in crate::backend::wayland::state::toolbar) fn apply_toolbar_offsets_throttled( diff --git a/src/backend/wayland/state/toolbar/drag/handoff.rs b/src/backend/wayland/state/toolbar/drag/handoff.rs index 645ae82bb..5222b35c6 100644 --- a/src/backend/wayland/state/toolbar/drag/handoff.rs +++ b/src/backend/wayland/state/toolbar/drag/handoff.rs @@ -1,4 +1,7 @@ -use super::*; +use crate::toolbar_gtk::GtkToolbarKind; + +use super::super::{Duration, Instant, WaylandState, drag_log}; +use super::state::HandoffEnd; use crate::backend::wayland::state::helpers::toolbar_drag_handoff_delay; impl WaylandState { @@ -48,7 +51,7 @@ impl WaylandState { pub(in crate::backend::wayland) fn begin_gtk_toolbar_drag_preview( &mut self, - kind: crate::toolbar_gtk::GtkToolbarKind, + kind: GtkToolbarKind, ) { let frozen_top_base_x = self.inline_top_base_x(); drag_log(|| { diff --git a/src/backend/wayland/state/toolbar/drag/mod.rs b/src/backend/wayland/state/toolbar/drag/mod.rs index 69138dc9a..8d02f2f40 100644 --- a/src/backend/wayland/state/toolbar/drag/mod.rs +++ b/src/backend/wayland/state/toolbar/drag/mod.rs @@ -1,5 +1,3 @@ -use super::*; - mod base; mod clamp; mod handoff; @@ -7,4 +5,4 @@ mod move_drag; mod relative; mod state; -pub(in crate::backend::wayland) use state::{HandoffEnd, MoveDragKind, ToolbarDrag}; +pub(in crate::backend::wayland) use state::{MoveDragKind, ToolbarDrag}; diff --git a/src/backend/wayland/state/toolbar/drag/move_drag.rs b/src/backend/wayland/state/toolbar/drag/move_drag.rs index 76ce9efeb..f9396482b 100644 --- a/src/backend/wayland/state/toolbar/drag/move_drag.rs +++ b/src/backend/wayland/state/toolbar/drag/move_drag.rs @@ -1,5 +1,6 @@ -use super::state::MoveSample; -use super::*; +use super::state::{MoveDragKind, MoveSample}; +use crate::backend::wayland::state::WaylandState; +use crate::backend::wayland::state::helpers::{drag_log, toolbar_drag_preview_enabled}; impl WaylandState { pub(in crate::backend::wayland::state::toolbar) fn begin_toolbar_move_drag( diff --git a/src/backend/wayland/state/toolbar/drag/relative.rs b/src/backend/wayland/state/toolbar/drag/relative.rs index eb5cbdab9..772867526 100644 --- a/src/backend/wayland/state/toolbar/drag/relative.rs +++ b/src/backend/wayland/state/toolbar/drag/relative.rs @@ -1,4 +1,6 @@ -use super::*; +use super::state::MoveDragKind; +use crate::backend::wayland::state::WaylandState; +use crate::backend::wayland::state::helpers::drag_log; impl WaylandState { /// Apply a relative delta to toolbar offsets (used with locked pointer + relative motion). diff --git a/src/backend/wayland/state/toolbar/drag/state.rs b/src/backend/wayland/state/toolbar/drag/state.rs index 487954356..e9c988966 100644 --- a/src/backend/wayland/state/toolbar/drag/state.rs +++ b/src/backend/wayland/state/toolbar/drag/state.rs @@ -1,3 +1,4 @@ +use std::mem; use std::time::{Duration, Instant}; use crate::toolbar_gtk::{GtkToolbarFeedback, GtkToolbarKind}; @@ -111,7 +112,7 @@ impl ToolbarDrag { } pub(in crate::backend::wayland) fn take_flush_requested(&mut self) -> bool { - std::mem::take(&mut self.flush_requested) + mem::take(&mut self.flush_requested) } pub(in crate::backend::wayland) fn begin_move( @@ -183,7 +184,7 @@ impl ToolbarDrag { if *active_kind != kind { return None; } - Some(std::mem::replace(previous, sample)) + Some(mem::replace(previous, sample)) } /// Compare screen samples across surfaces, except that a suppressed toolbar's diff --git a/src/backend/wayland/state/toolbar/events.rs b/src/backend/wayland/state/toolbar/events.rs index d575ba966..a112d88f8 100644 --- a/src/backend/wayland/state/toolbar/events.rs +++ b/src/backend/wayland/state/toolbar/events.rs @@ -1,12 +1,23 @@ -use super::*; -use crate::{ - backend::wayland::runtime_ui_state::ToolbarRuntimeFinish, - input::{InputState, state::TopMenuState}, - ui::toolbar::model::{ - ToolbarBackendRoute, ToolbarEventPolicy, ToolbarPersistence, ToolbarPopover, - ToolbarRuntimeUiPersistenceTarget, popovers_for_event, - }, +use std::env; +use std::sync::OnceLock; +use std::time::Instant; + +use crate::env_vars; +use crate::ui::toolbar::model; + +use super::super::WaylandState; +use super::super::helpers::drag_log; +use super::drag::MoveDragKind; +use crate::backend::wayland::runtime_ui_state::ToolbarRuntimeFinish; +use crate::input::InputState; +use crate::input::state::TopMenuState; +use crate::ui::toolbar::model::{ + ToolbarBackendRoute, ToolbarEventPolicy, ToolbarPersistence, ToolbarPopover, + ToolbarRuntimeUiPersistenceTarget, popovers_for_event, }; +use crate::ui::toolbar::{ToolbarBindingHints, ToolbarEvent, ToolbarSnapshot}; +#[cfg(test)] +use anyhow::Result; use wayland_client::{Connection, QueueHandle}; mod feedback; @@ -25,9 +36,8 @@ use session::populate_session_snapshot; /// The daemon marks its overlay children with the child-generation variable /// (`daemon::overlay::spawn`), which never changes during a run. fn overlay_is_daemon_child() -> bool { - static DAEMON_CHILD: std::sync::OnceLock = std::sync::OnceLock::new(); - *DAEMON_CHILD - .get_or_init(|| std::env::var_os(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV).is_some()) + static DAEMON_CHILD: OnceLock = OnceLock::new(); + *DAEMON_CHILD.get_or_init(|| env::var_os(env_vars::OVERLAY_CHILD_GENERATION_ENV).is_some()) } fn toolbar_event_blocked_by_modal(input_state: &InputState) -> bool { @@ -38,13 +48,13 @@ fn toolbar_event_blocked_by_modal(input_state: &InputState) -> bool { /// palette, and a click landing right after the minimized tab (or micro chip) /// restored the strip, whose controls now sit under the pointer. Both /// frontends deliver clicks through this one path. -fn toolbar_event_dropped(input_state: &InputState, now: std::time::Instant) -> bool { +fn toolbar_event_dropped(input_state: &InputState, now: Instant) -> bool { toolbar_event_blocked_by_modal(input_state) || input_state.toolbar_restore_guard_blocks(now) } fn finalize_pointer_gestures_before_toolbar_dispatch( input_state: &mut InputState, - spotlight_wheel_idle_deadline: &mut Option, + spotlight_wheel_idle_deadline: &mut Option, ) { // Several backend-owned toolbar routes return before // `InputState::apply_toolbar_event`, including session operations. Close @@ -68,7 +78,7 @@ enum ToolbarEventPreflight { fn handle_toolbar_event_preflight( input_state: &mut InputState, - spotlight_wheel_idle_deadline: &mut Option, + spotlight_wheel_idle_deadline: &mut Option, event: &ToolbarEvent, rebind_requested: bool, ) -> ToolbarEventPreflight { @@ -78,7 +88,7 @@ fn handle_toolbar_event_preflight( // already resolved. finalize_pointer_gestures_before_toolbar_dispatch(input_state, spotlight_wheel_idle_deadline); - if rebind_requested && let Some(action) = crate::ui::toolbar::model::action_for_event(event) { + if rebind_requested && let Some(action) = model::action_for_event(event) { input_state.begin_keybinding_capture(action); return ToolbarEventPreflight::RebindCaptured; } @@ -197,7 +207,7 @@ impl WaylandState { // GTK toolbar feedback bypasses the built-in pointer modal gate, so // enforce the same rule in the shared event path as well. The same // path drops the second click of a double-click on the restore tab. - if toolbar_event_dropped(&self.input_state, std::time::Instant::now()) { + if toolbar_event_dropped(&self.input_state, Instant::now()) { return; } // A toolbar interaction replaces the modal sampler. Do this before diff --git a/src/backend/wayland/state/toolbar/events/presets.rs b/src/backend/wayland/state/toolbar/events/presets.rs index 75dc7b549..19324ed4d 100644 --- a/src/backend/wayland/state/toolbar/events/presets.rs +++ b/src/backend/wayland/state/toolbar/events/presets.rs @@ -1,8 +1,10 @@ -use super::*; use crate::backend::wayland::config_edits::{ConfigEdit, ConfigEditWorker}; +use crate::backend::wayland::state::WaylandState; use crate::config::{Config, ConfigEditNotReadBack, ConfigEditOutcome, ConfigEditWrite}; +use crate::domain::Action; use crate::i18n::{tr, trf}; use crate::input::state::{PresetAction, Toast, ToastPriority}; +use anyhow::Result; /// Apply an accepted preset save/clear to the effective config. /// @@ -123,8 +125,7 @@ impl WaylandState { self.input_state.push_toast( ToastPriority::Action, "presets", - Toast::warning(message) - .action(tr("Edit"), crate::domain::Action::OpenConfiguratorPresets), + Toast::warning(message).action(tr("Edit"), Action::OpenConfiguratorPresets), ); } } @@ -133,11 +134,16 @@ impl WaylandState { #[cfg(test)] mod tests { + use std::path::{Path, PathBuf}; + + use crate::config::ConfigDocument; + use crate::input::Tool; + use crate::{config, test_temp}; + use super::*; use crate::config::io::persist_preset_slot_at; - use crate::config::persist_preset_slot; use crate::config::test_helpers::with_temp_config_home; - use crate::config::{ColorSpec, ToolPresetConfig}; + use crate::config::{ColorSpec, ToolPresetConfig, persist_preset_slot}; use crate::draw::Color; use std::fs; @@ -147,7 +153,7 @@ mod tests { fn preset(name: &str) -> Box { Box::new(ToolPresetConfig { name: Some(name.to_string()), - tool: crate::input::Tool::Pen, + tool: Tool::Pen, color: ColorSpec::from(Color { r: 0.13, g: 0.27, @@ -183,10 +189,7 @@ mod tests { apply_preset_action(&mut config, &action); assert_eq!( - config - .presets - .get_slot(2) - .and_then(|slot| slot.name.clone()), + config.presets.slot(2).and_then(|slot| slot.name.clone()), Some("Run preset".to_string()) ); // The wording waits for the write; this is what the completion says @@ -208,7 +211,7 @@ mod tests { let action = PresetAction::Clear { slot: 1 }; apply_preset_action(&mut config, &action); - assert!(config.presets.get_slot(1).is_none()); + assert!(config.presets.slot(1).is_none()); assert_eq!(preset_saved_message(&action), "Cleared preset 1."); } @@ -253,7 +256,7 @@ undo = [\"Ctrl+Alt+Shift+Q\"] redo = [\"Ctrl+Alt+Shift+Q\"] "; - fn write_fixture(dir: &std::path::Path) -> std::path::PathBuf { + fn write_fixture(dir: &Path) -> PathBuf { let path = dir.join("config.toml"); fs::write(&path, AUTHORED_FILE).expect("the fixture should be written"); path @@ -263,7 +266,7 @@ redo = [\"Ctrl+Alt+Shift+Q\"] /// comments, the contested pair, the unknown key — exactly as authored. #[test] fn saving_a_preset_changes_exactly_its_own_slot() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); persist_preset_slot_at(&path, 1, Some(&preset("Run preset"))) @@ -288,7 +291,7 @@ redo = [\"Ctrl+Alt+Shift+Q\"] /// Clearing removes the slot's own block and leaves the rest byte-identical. #[test] fn clearing_a_preset_changes_exactly_its_own_slot() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); persist_preset_slot_at(&path, 1, None).expect("the write should succeed"); @@ -304,13 +307,13 @@ redo = [\"Ctrl+Alt+Shift+Q\"] /// says so instead of claiming a save. #[test] fn a_preset_edit_the_file_already_holds_writes_nothing() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); - let stored = crate::config::ConfigDocument::load_from_path(&path) + let stored = ConfigDocument::load_from_path(&path) .expect("load") .config() .presets - .get_slot(1) + .slot(1) .cloned() .expect("the fixture authors slot 1"); @@ -337,7 +340,7 @@ redo = [\"Ctrl+Alt+Shift+Q\"] #[test] fn a_preset_write_leaves_a_timestamped_backup() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); let outcome = persist_preset_slot_at(&path, 2, Some(&preset("Run preset"))) @@ -354,7 +357,7 @@ redo = [\"Ctrl+Alt+Shift+Q\"] fn a_read_only_config_fails_the_preset_write_and_leaves_the_file_alone() { use std::os::unix::fs::PermissionsExt; - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); fs::set_permissions(&path, fs::Permissions::from_mode(0o444)).expect("chmod file"); fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o555)).expect("chmod dir"); @@ -364,6 +367,7 @@ redo = [\"Ctrl+Alt+Shift+Q\"] fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir"); fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file"); + // SAFETY: geteuid has no preconditions and cannot fail. if unsafe { libc::geteuid() } == 0 { return; } @@ -386,7 +390,7 @@ redo = [\"Ctrl+Alt+Shift+Q\"] /// dropped by `set_slot`. #[test] fn an_out_of_range_preset_slot_is_refused() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); assert!(persist_preset_slot_at(&path, 0, Some(&preset("Nope"))).is_err()); @@ -399,7 +403,7 @@ redo = [\"Ctrl+Alt+Shift+Q\"] #[test] fn a_fresh_load_returns_the_written_preset() { with_temp_config_home(|config_root| { - let config_dir = config_root.join(crate::config::PRIMARY_CONFIG_DIR); + let config_dir = config_root.join(config::PRIMARY_CONFIG_DIR); fs::create_dir_all(&config_dir).expect("test config directory"); fs::write(config_dir.join("config.toml"), AUTHORED_PRESET) .expect("test config should be written"); @@ -408,10 +412,7 @@ redo = [\"Ctrl+Alt+Shift+Q\"] let restarted = Config::load().expect("test config should reload").config; assert_eq!( - restarted - .presets - .get_slot(1) - .and_then(|slot| slot.name.clone()), + restarted.presets.slot(1).and_then(|slot| slot.name.clone()), Some("Run preset".to_string()) ); }); diff --git a/src/backend/wayland/state/toolbar/events/quick_colors.rs b/src/backend/wayland/state/toolbar/events/quick_colors.rs index 8b33f7fd9..6fb19cbcc 100644 --- a/src/backend/wayland/state/toolbar/events/quick_colors.rs +++ b/src/backend/wayland/state/toolbar/events/quick_colors.rs @@ -1,10 +1,12 @@ -use super::*; use crate::backend::wayland::config_edits::{ConfigEdit, ConfigEditWorker}; +use crate::backend::wayland::state::WaylandState; use crate::config::{ Config, ConfigEditNotReadBack, ConfigEditOutcome, ConfigEditWrite, QuickColorSlotMissing, }; +use crate::domain::Action; use crate::i18n::{tr, trf}; use crate::input::state::{QuickColorEdit, Toast, ToastPriority}; +use anyhow::Result; /// What the user is told when the swatch and the file both took the recolor. fn quick_color_saved_message(index: usize) -> String { @@ -109,10 +111,7 @@ impl WaylandState { self.input_state.push_toast( ToastPriority::Action, "drawing.quick-color", - Toast::warning(message).action( - tr("Edit"), - crate::domain::Action::OpenConfiguratorQuickColors, - ), + Toast::warning(message).action(tr("Edit"), Action::OpenConfiguratorQuickColors), ); } } @@ -121,11 +120,15 @@ impl WaylandState { #[cfg(test)] mod tests { + use std::path::{Path, PathBuf}; + + use crate::config::{ColorSpec, ConfigDocument}; + use crate::{config, test_temp}; + use super::*; use crate::config::io::persist_quick_color_at; - use crate::config::persist_quick_color; use crate::config::test_helpers::with_temp_config_home; - use crate::config::{Config, QuickColorSlotMissing, QuickColorsConfig}; + use crate::config::{Config, QuickColorSlotMissing, QuickColorsConfig, persist_quick_color}; use crate::draw::Color; use std::fs; @@ -160,7 +163,7 @@ label = \"Green\" color = \"#33F54F\" "; - fn write_fixture(dir: &std::path::Path) -> std::path::PathBuf { + fn write_fixture(dir: &Path) -> PathBuf { let path = dir.join("config.toml"); fs::write(&path, AUTHORED_FILE).expect("the fixture should be written"); path @@ -176,7 +179,7 @@ color = \"#33F54F\" /// not asked to change. #[test] fn a_recolor_changes_exactly_its_own_slot() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); persist_quick_color_at(&path, 1, NEW_COLOR).expect("the write should succeed"); @@ -201,7 +204,7 @@ color = \"#33F54F\" /// nothing changes meaning. #[test] fn recoloring_an_implied_slot_materializes_only_up_to_that_slot() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); // Slot 3 is past the two authored entries, inside the default backfill. @@ -213,8 +216,8 @@ color = \"#33F54F\" 4, "the array must reach the edited slot and stop: {after}" ); - let reloaded = crate::config::ConfigDocument::load_from_path(&path) - .expect("the written config should reload"); + let reloaded = + ConfigDocument::load_from_path(&path).expect("the written config should reload"); let palette = &reloaded.config().drawing.quick_colors; assert_eq!( palette.configured_entry_count(), @@ -224,7 +227,7 @@ color = \"#33F54F\" let entries = palette.effective_entries(); assert_eq!( entries.get(3).map(|entry| entry.color.clone()), - Some(crate::config::ColorSpec::from(NEW_COLOR)) + Some(ColorSpec::from(NEW_COLOR)) ); assert_eq!( entries.get(2).map(|entry| entry.color.clone()), @@ -250,9 +253,9 @@ color = \"#33F54F\" /// a success message for a save that did not happen. #[test] fn recoloring_a_slot_to_the_color_it_already_has_writes_nothing() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); - let existing = crate::config::ConfigDocument::load_from_path(&path) + let existing = ConfigDocument::load_from_path(&path) .expect("load") .config() .drawing @@ -278,10 +281,10 @@ color = \"#33F54F\" /// business. #[test] fn recoloring_an_implied_slot_to_its_default_writes_nothing() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); let default = - crate::config::default_quick_color_for_index(3).expect("slot 4 has a shipped default"); + config::default_quick_color_for_index(3).expect("slot 4 has a shipped default"); let outcome = persist_quick_color_at(&path, 3, default).expect("a no-op is not a failure"); @@ -291,7 +294,7 @@ color = \"#33F54F\" #[test] fn a_recolor_leaves_a_timestamped_backup() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); let outcome = @@ -308,7 +311,7 @@ color = \"#33F54F\" /// failure: nothing is wrong with the file, it simply got shorter. #[test] fn a_missing_slot_is_reported_without_touching_the_file() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); // Past the authored entries and past the default backfill. @@ -330,7 +333,7 @@ color = \"#33F54F\" fn a_read_only_config_fails_the_recolor_and_leaves_the_file_alone() { use std::os::unix::fs::PermissionsExt; - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = write_fixture(temp.path()); fs::set_permissions(&path, fs::Permissions::from_mode(0o444)).expect("chmod file"); fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o555)).expect("chmod dir"); @@ -340,6 +343,7 @@ color = \"#33F54F\" fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir"); fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file"); + // SAFETY: geteuid has no preconditions and cannot fail. if unsafe { libc::geteuid() } == 0 { return; } @@ -364,7 +368,7 @@ color = \"#33F54F\" #[test] fn a_fresh_load_returns_the_written_quick_color() { with_temp_config_home(|config_root| { - let config_dir = config_root.join(crate::config::PRIMARY_CONFIG_DIR); + let config_dir = config_root.join(config::PRIMARY_CONFIG_DIR); fs::create_dir_all(&config_dir).expect("test config directory"); fs::write(config_dir.join("config.toml"), AUTHORED_FILE) .expect("test config should be written"); @@ -379,7 +383,7 @@ color = \"#33F54F\" .effective_entries() .get(1) .map(|entry| entry.color.clone()), - Some(crate::config::ColorSpec::from(NEW_COLOR)) + Some(ColorSpec::from(NEW_COLOR)) ); assert_eq!(quick_color_saved_message(1), "Updated quick color 2."); }); diff --git a/src/backend/wayland/state/toolbar/events/session.rs b/src/backend/wayland/state/toolbar/events/session.rs index f4af125b7..d66ff3308 100644 --- a/src/backend/wayland/state/toolbar/events/session.rs +++ b/src/backend/wayland/state/toolbar/events/session.rs @@ -1,25 +1,36 @@ -use super::*; -use crate::backend::wayland::session::{SessionCommand, SessionCommandReport}; +use std::time::Instant; + +use crate::backend::wayland::session::{ + SessionCommand, SessionCommandReport, SessionHome, SourceWriteRefused, driver, +}; +use crate::backend::wayland::state::WaylandState; +use crate::backend::wayland::state::core::overlay::OverlaySuppression; +use crate::backend::wayland::state::render::RenderOutcome; use crate::i18n::{tr, trf}; use crate::input::state::{Toast, ToastPriority}; -use crate::session::catalog; +use crate::session::{ + ClearToolStateOutcome, CorruptArtifactPreservationFailed, MissingNamedSessionFile, + MissingNamedSessionParent, SaveAsOverwrite, SessionInspection, SessionOptions, catalog, +}; use crate::ui::toolbar::session_format::session_display_name; +use crate::ui::toolbar::{SessionRecentSnapshot, ToolbarEvent, ToolbarSnapshot}; use anyhow::{Context, Error as AnyhowError, Result, anyhow}; +use log::debug; use std::path::{Path, PathBuf}; use wayland_client::{Connection, QueueHandle}; -fn clear_tools_message(outcome: Option) -> &'static str { +fn clear_tools_message(outcome: Option) -> &'static str { match outcome { - Some(crate::session::ClearToolStateOutcome::Cleared { + Some(ClearToolStateOutcome::Cleared { preserved_board_data: true, }) => tr("Tool defaults reset from config. Saved boards and history were preserved."), - Some(crate::session::ClearToolStateOutcome::Cleared { + Some(ClearToolStateOutcome::Cleared { preserved_board_data: false, }) => tr("Tool defaults reset from config. No board data was present."), - Some(crate::session::ClearToolStateOutcome::NoToolState) => { + Some(ClearToolStateOutcome::NoToolState) => { tr("Tool defaults reset from config. No saved tool state was stored.") } - Some(crate::session::ClearToolStateOutcome::NoSession) => { + Some(ClearToolStateOutcome::NoSession) => { tr("Tool defaults reset from config. No saved session file was present.") } None => tr("Tool defaults reset from config for this run. No active session file to edit."), @@ -28,8 +39,8 @@ fn clear_tools_message(outcome: Option) - pub(super) fn populate_session_snapshot( snapshot: &mut ToolbarSnapshot, - options: Option<&crate::session::SessionOptions>, - home: &crate::backend::wayland::session::SessionHome, + options: Option<&SessionOptions>, + home: &SessionHome, ) { snapshot.home_session_name = home.name(); snapshot.at_home_session = home.is_at_home(); @@ -45,7 +56,7 @@ pub(super) fn populate_session_snapshot( snapshot.active_session_path = active_path; } -pub(super) fn session_info_summary(inspection: &crate::session::SessionInspection) -> String { +pub(super) fn session_info_summary(inspection: &SessionInspection) -> String { let name = session_display_name(&inspection.session_path); if !inspection.exists { return if inspection.backup_exists { @@ -95,9 +106,7 @@ fn format_byte_count(bytes: u64) -> String { } } -fn recent_session_snapshots( - current_path: Option<&Path>, -) -> Vec { +fn recent_session_snapshots(current_path: Option<&Path>) -> Vec { let recent = match catalog::recent_sessions() { Ok(recent) => recent, Err(err) => { @@ -116,7 +125,7 @@ fn recent_session_snapshots( { return None; } - Some(crate::ui::toolbar::SessionRecentSnapshot { + Some(SessionRecentSnapshot { display_name: entry.display_name, path, }) @@ -183,7 +192,7 @@ impl WaylandState { fn current_session_file_path(&self) -> Option { self.session .options() - .map(crate::session::SessionOptions::session_file_path) + .map(SessionOptions::session_file_path) } fn start_session_file_dialog_with_overlay_suppressed( @@ -403,7 +412,7 @@ impl WaylandState { } self.clear_toolbar_save_as_overwrite_prompt(); - self.commit_toolbar_save_session_as(path, crate::session::SaveAsOverwrite::ConfirmReplace); + self.commit_toolbar_save_session_as(path, SaveAsOverwrite::ConfirmReplace); } fn handle_toolbar_save_session_as_cancel(&mut self) { @@ -412,11 +421,7 @@ impl WaylandState { } } - fn commit_toolbar_save_session_as( - &mut self, - path: &Path, - overwrite: crate::session::SaveAsOverwrite, - ) { + fn commit_toolbar_save_session_as(&mut self, path: &Path, overwrite: SaveAsOverwrite) { if let Err(error) = self.start_session_command(SessionCommand::SaveAs(path.to_path_buf(), overwrite)) { @@ -483,10 +488,7 @@ impl WaylandState { &[&session_display_name(&path)], )); } else { - self.commit_toolbar_save_session_as( - &path, - crate::session::SaveAsOverwrite::Deny, - ); + self.commit_toolbar_save_session_as(&path, SaveAsOverwrite::Deny); } } SessionCommandReport::Clear(report) => self.set_session_toolbar_info(trf( @@ -550,18 +552,11 @@ impl WaylandState { fn fail_output_session_command(&mut self, error: &AnyhowError) { let preservation_failed = error - .downcast_ref::() - .is_some(); - let source_unwritable = error - .downcast_ref::() + .downcast_ref::() .is_some(); + let source_unwritable = error.downcast_ref::().is_some(); let backoff = self.output_transition_failure_backoff(); - if !crate::backend::wayland::session::driver::defer_failed_output( - &mut self.session, - error, - std::time::Instant::now(), - backoff, - ) { + if !driver::defer_failed_output(&mut self.session, error, Instant::now(), backoff) { log::debug!("Output session transition requeued after guard abort: {error:#}"); return; } @@ -649,10 +644,10 @@ pub(super) fn forget_missing_recent_session_after_open_error( } fn missing_session_error_matches_path(path: &Path, err: &AnyhowError) -> bool { - err.downcast_ref::() + err.downcast_ref::() .is_some_and(|missing| catalog::session_paths_match(missing.path(), path)) || err - .downcast_ref::() + .downcast_ref::() .is_some_and(|missing| catalog::session_paths_match(missing.path(), path)) } diff --git a/src/backend/wayland/state/toolbar/events/session/dialog.rs b/src/backend/wayland/state/toolbar/events/session/dialog.rs index cfc5eff7a..27b41e405 100644 --- a/src/backend/wayland/state/toolbar/events/session/dialog.rs +++ b/src/backend/wayland/state/toolbar/events/session/dialog.rs @@ -1,8 +1,14 @@ +use std::fmt::{Debug, Formatter}; +use std::{env, fmt}; + use crate::backend::wayland::runtime_operation::{ RuntimeOperationController, RuntimeOperationIdSource, RuntimeOperationPoll, }; +use crate::backend::wayland::RuntimeWakeHandle; use crate::i18n::tr; +use crate::process_broker::{BrokerErrorKind, HelperKind}; +use crate::{paths, process_broker}; use anyhow::{Result, anyhow}; use std::ffi::{OsStr, OsString}; @@ -33,8 +39,8 @@ pub(in crate::backend::wayland::state) struct SessionFileDialogController { operation: RuntimeOperationController, String>>, } -impl std::fmt::Debug for SessionFileDialogController { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Debug for SessionFileDialogController { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter .debug_struct("SessionFileDialogController") .field("active", &self.operation.is_active()) @@ -43,9 +49,7 @@ impl std::fmt::Debug for SessionFileDialogController { } impl SessionFileDialogController { - pub(in crate::backend::wayland::state) fn new( - runtime_wake: crate::backend::wayland::RuntimeWakeHandle, - ) -> Self { + pub(in crate::backend::wayland::state) fn new(runtime_wake: RuntimeWakeHandle) -> Self { Self { operation: RuntimeOperationController::new( RuntimeOperationIdSource::new(), @@ -184,11 +188,7 @@ fn run_zenity_session_file_dialog( "--file-filter".into(), "All files | *".into(), ]); - run_session_file_dialog_command( - crate::process_broker::HelperKind::SessionZenity, - "zenity", - arguments, - ) + run_session_file_dialog_command(HelperKind::SessionZenity, "zenity", &arguments) } fn run_kdialog_session_file_dialog( @@ -213,23 +213,19 @@ fn run_kdialog_session_file_dialog( } } arguments.push("Wayscriber sessions (*.wayscriber-session *.session);;All files (*)".into()); - run_session_file_dialog_command( - crate::process_broker::HelperKind::SessionKdialog, - "kdialog", - arguments, - ) + run_session_file_dialog_command(HelperKind::SessionKdialog, "kdialog", &arguments) } fn run_session_file_dialog_command( - kind: crate::process_broker::HelperKind, + kind: HelperKind, program: &'static str, - arguments: Vec, + arguments: &[OsString], ) -> Result> { - let output = match crate::process_broker::current().and_then(|broker| { + let output = match process_broker::current().and_then(|broker| { broker.run( kind, OsStr::new(program), - &arguments, + arguments, Vec::new(), Duration::from_secs(120), 64 * 1024, @@ -237,8 +233,7 @@ fn run_session_file_dialog_command( }) { Ok(output) => output, Err(err) - if crate::process_broker::error_kind(&err) - == Some(crate::process_broker::BrokerErrorKind::MissingExecutable) => + if process_broker::error_kind(&err) == Some(BrokerErrorKind::MissingExecutable) => { return Ok(None); } @@ -270,7 +265,7 @@ fn run_session_file_dialog_command( } fn default_session_dir() -> PathBuf { - crate::paths::home_dir().unwrap_or_else(std::env::temp_dir) + paths::home_dir().unwrap_or_else(env::temp_dir) } pub(in crate::backend::wayland::state::toolbar::events) fn default_save_as_path( @@ -280,8 +275,8 @@ pub(in crate::backend::wayland::state::toolbar::events) fn default_save_as_path( } fn default_save_as_dir() -> PathBuf { - let Some(home) = crate::paths::home_dir() else { - return std::env::temp_dir(); + let Some(home) = paths::home_dir() else { + return env::temp_dir(); }; let documents = home.join("Documents"); if documents.is_dir() { documents } else { home } @@ -322,6 +317,8 @@ pub(in crate::backend::wayland::state::toolbar::events) fn ensure_save_as_extens #[cfg(test)] mod controller_tests { + use std::sync::mpsc; + use super::*; use crate::backend::wayland::RuntimeWakeSource; @@ -333,7 +330,7 @@ mod controller_tests { Err("chooser error".into()), ]; for expected in outcomes { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut controller = SessionFileDialogController::new(wake.handle()); let result = expected.clone(); controller @@ -351,7 +348,7 @@ mod controller_tests { #[test] fn panicking_dialog_wakes_idle_runtime_with_one_terminal_failure() { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut controller = SessionFileDialogController::new(wake.handle()); controller .submit(SessionFileDialogMode::Open, || panic!("chooser panicked")) @@ -367,9 +364,9 @@ mod controller_tests { #[test] fn active_dialog_rejects_overlap_before_spawning_worker() { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut controller = SessionFileDialogController::new(wake.handle()); - let (release, wait) = std::sync::mpsc::channel(); + let (release, wait) = mpsc::channel(); controller .submit(SessionFileDialogMode::Open, move || { wait.recv().unwrap(); diff --git a/src/backend/wayland/state/toolbar/events/session/home.rs b/src/backend/wayland/state/toolbar/events/session/home.rs index 1b33fdb21..edb036b73 100644 --- a/src/backend/wayland/state/toolbar/events/session/home.rs +++ b/src/backend/wayland/state/toolbar/events/session/home.rs @@ -1,4 +1,5 @@ -use super::*; +use crate::backend::wayland::session::SessionCommand; +use crate::backend::wayland::state::WaylandState; use crate::i18n::{tr, trf}; use crate::session::SessionOptions; diff --git a/src/backend/wayland/state/toolbar/events/session/queued.rs b/src/backend/wayland/state/toolbar/events/session/queued.rs index fd7a07d6b..d7d9b5e41 100644 --- a/src/backend/wayland/state/toolbar/events/session/queued.rs +++ b/src/backend/wayland/state/toolbar/events/session/queued.rs @@ -1,5 +1,10 @@ -use super::*; +use crate::notification; + +use super::AnyhowError; +use crate::backend::wayland::session::SessionCommand; +use crate::backend::wayland::state::WaylandState; use crate::i18n::{tr, trf}; +use crate::ui::toolbar::session_format::session_display_name; impl WaylandState { /// One toast names every queued command that will not run, since each @@ -7,7 +12,7 @@ impl WaylandState { /// desktop notification names the commands that were requested but lost. pub(in crate::backend::wayland) fn fail_queued_session_commands( &mut self, - failures: Vec<(SessionCommand, AnyhowError)>, + failures: &[(SessionCommand, AnyhowError)], closing: bool, ) { if closing { @@ -24,7 +29,7 @@ impl WaylandState { "Wayscriber closed before it could run: {}. Run them again after restarting.", &[&lost.join("; ")], ); - crate::notification::send_notification_async( + notification::send_notification_async( &self.tokio_handle, tr("Session Commands Not Run").to_string(), body, @@ -33,7 +38,7 @@ impl WaylandState { } } - match failures.as_slice() { + match failures { [] => {} [(command, error)] => self.fail_session_command(command, error), _ => { diff --git a/src/backend/wayland/state/toolbar/events/session/tests.rs b/src/backend/wayland/state/toolbar/events/session/tests.rs index 72a156b19..2a0ac08d7 100644 --- a/src/backend/wayland/state/toolbar/events/session/tests.rs +++ b/src/backend/wayland/state/toolbar/events/session/tests.rs @@ -1,3 +1,10 @@ +use std::path::PathBuf; + +use crate::backend::wayland::session::HomeSession; +use crate::config::Config; +use crate::input::state::test_support; +use crate::session::SaveAsOverwrite; + use super::{DialogFramePhase, dialog_frame_accepted, populate_session_snapshot}; use crate::backend::wayland::session::{SessionHome, SessionLaunch}; use crate::backend::wayland::state::RenderOutcome; @@ -8,15 +15,15 @@ use crate::session::SessionTarget; fn queued_commands_that_did_not_run_are_all_named_in_one_toast() { use crate::backend::wayland::handlers::test_support::HandlerFixture; use crate::backend::wayland::session::SessionCommand; - let mut fixture = HandlerFixture::new(crate::config::Config::default()); + let mut fixture = HandlerFixture::new(Config::default()); let reason = || anyhow::anyhow!("the visible session changed"); fixture.state.fail_queued_session_commands( - vec![ + &[ ( SessionCommand::SaveAs( "/sessions/copy.wayscriber-session".into(), - crate::session::SaveAsOverwrite::Deny, + SaveAsOverwrite::Deny, ), reason(), ), @@ -35,12 +42,12 @@ fn queued_commands_that_did_not_run_are_all_named_in_one_toast() { #[test] fn the_session_menu_learns_home_and_whether_it_is_active() { - let input = crate::input::state::test_support::make_test_input_state(); + let input = test_support::make_test_input_state(); let mut snapshot = ToolbarSnapshot::from_input_with_bindings(&input, Default::default()); - let home = std::path::PathBuf::from("/sessions/home.wayscriber-session"); + let home = PathBuf::from("/sessions/home.wayscriber-session"); let away = SessionHome::new( SessionLaunch { - home: crate::backend::wayland::session::HomeSession::Named(home), + home: HomeSession::Named(home), preferred: None, from_daemon: true, }, diff --git a/src/backend/wayland/state/toolbar/events/tests.rs b/src/backend/wayland/state/toolbar/events/tests.rs index aeb0bc55b..0cd7278bb 100644 --- a/src/backend/wayland/state/toolbar/events/tests.rs +++ b/src/backend/wayland/state/toolbar/events/tests.rs @@ -1,3 +1,6 @@ +use std::ffi::OsString; +use std::time::Duration; + use super::session::{ SessionFileDialogMode, SessionFileDialogResult, choose_session_file_from, default_save_as_path, ensure_save_as_extension, forget_missing_recent_session_after_open_error, save_as_file_name, @@ -28,29 +31,43 @@ fn event_dismisses_settings_popover(event: &ToolbarEvent) -> bool { event_dismisses_popover(event, ToolbarPopover::Settings) } use crate::backend::wayland::runtime_ui_state::{user_click_highlight_enabled, user_tool_preview}; -use crate::config::{Action, StatusBarItem, ToolbarLayoutMode, ToolbarSectionFlag}; -use crate::draw::{Color, FontDescriptor}; +use crate::config::test_helpers::ConfigFileSnapshot; +use crate::config::{ + Action, Config, StatusBarItem, ToolbarItemOrderGroup, ToolbarLayoutMode, ToolbarSectionFlag, + test_helpers, toolbar_item_ids, +}; +use crate::draw::frame::ShapeSnapshot; +use crate::draw::{ArrowStyle, Color, FontDescriptor, Shape, TextMeasurer}; use crate::env_vars::XDG_DATA_HOME_ENV; use crate::input::state::test_support::make_test_input_state; -use crate::input::{EraserMode, Tool}; +use crate::input::state::{DrawingState, SpotlightWheelOutcome}; +use crate::input::{EraserMode, Key, MouseButton, Tool}; +use crate::session as stored_session; +use crate::session::catalog::CatalogEvent; +use crate::session::{FrameCounts, SessionInspection, catalog}; +use crate::test_env::LeaseGuard; +use crate::ui::toolbar::{PrecisionEntryTarget, ToolbarItemCustomizeGroup}; +use crate::ui_text::UiTextEngine; +use crate::{test_env, test_temp}; use anyhow::anyhow; use std::fs; use std::path::{Path, PathBuf}; -use std::sync::MutexGuard; use super::feedback::ToolbarPinDurability; struct EnvGuard { - _guard: MutexGuard<'static, ()>, - xdg_data_home: Option, + _guard: LeaseGuard<'static>, + xdg_data_home: Option, } impl EnvGuard { fn set_xdg_data_home(path: &Path) -> Self { - let guard = crate::test_env::lock(); - let xdg_data_home = std::env::var_os(XDG_DATA_HOME_ENV); + let guard = test_env::lock(); + let xdg_data_home = env::var_os(XDG_DATA_HOME_ENV); + // SAFETY: serialized by the test environment mutex in `guard`, which + // the returned value keeps until it restores the variable. unsafe { - std::env::set_var(XDG_DATA_HOME_ENV, path); + env::set_var(XDG_DATA_HOME_ENV, path); } Self { _guard: guard, @@ -61,9 +78,13 @@ impl EnvGuard { impl Drop for EnvGuard { fn drop(&mut self) { - match self.xdg_data_home.take() { - Some(value) => unsafe { std::env::set_var(XDG_DATA_HOME_ENV, value) }, - None => unsafe { std::env::remove_var(XDG_DATA_HOME_ENV) }, + // SAFETY: fields drop only after this body returns, so `_guard` still + // holds the test environment mutex here. + unsafe { + match self.xdg_data_home.take() { + Some(value) => env::set_var(XDG_DATA_HOME_ENV, value), + None => env::remove_var(XDG_DATA_HOME_ENV), + } } } } @@ -191,7 +212,7 @@ fn runtime_toolbar_events_do_not_directly_save_config() { b: 0.3, a: 1.0, }, - action: Some(crate::config::Action::SetColorRed), + action: Some(Action::SetColorRed), index: 0, }, ToolbarEvent::SetThickness(8.0), @@ -209,12 +230,10 @@ fn runtime_toolbar_events_do_not_directly_save_config() { ToolbarEvent::ToggleFill(true), ToolbarEvent::ApplyPreset(1), ToolbarEvent::OpenSession, - ToolbarEvent::OpenRecentSession(std::path::PathBuf::from("/tmp/recent.wayscriber-session")), + ToolbarEvent::OpenRecentSession(PathBuf::from("/tmp/recent.wayscriber-session")), ToolbarEvent::OpenHomeSession, ToolbarEvent::SaveSessionAs, - ToolbarEvent::SaveSessionAsConfirm(std::path::PathBuf::from( - "/tmp/existing.wayscriber-session", - )), + ToolbarEvent::SaveSessionAsConfirm(PathBuf::from("/tmp/existing.wayscriber-session")), ToolbarEvent::SaveSessionAsCancel, ToolbarEvent::SessionInfo, ToolbarEvent::ClearSession, @@ -236,8 +255,8 @@ fn runtime_toolbar_events_do_not_directly_save_config() { #[test] fn backend_session_dispatch_finalizes_spotlight_history_and_its_deadline() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -247,7 +266,7 @@ fn backend_session_dispatch_finalizes_spotlight_history_and_its_deadline() { let shape_id = input_state .boards .active_frame_mut() - .add_shape(crate::draw::Shape::Spotlight { + .add_shape(Shape::Spotlight { cx: 200, cy: 200, rx: 60, @@ -256,9 +275,9 @@ fn backend_session_dispatch_finalizes_spotlight_history_and_its_deadline() { }); assert_eq!( input_state.nudge_spotlight_magnification_at_with(&test_text_measurer, 200, 200, 1), - crate::input::state::SpotlightWheelOutcome::Adjusted + SpotlightWheelOutcome::Adjusted ); - let mut deadline = Some(std::time::Instant::now() + std::time::Duration::from_secs(1)); + let mut deadline = Some(Instant::now() + Duration::from_secs(1)); assert_eq!( handle_toolbar_event_preflight( @@ -279,7 +298,7 @@ fn backend_session_dispatch_finalizes_spotlight_history_and_its_deadline() { .expect("spotlight") .shape { - crate::draw::Shape::Spotlight { magnification, .. } => magnification, + Shape::Spotlight { magnification, .. } => magnification, ref other => panic!("expected a spotlight, got {other:?}"), }; assert_eq!( @@ -401,8 +420,8 @@ fn named_section_visibility_persists_under_its_own_target() { /// restores on exit -- not the mode's. #[test] fn presenter_mode_persists_the_users_values_not_its_own() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -440,8 +459,8 @@ fn presenter_mode_persists_the_users_values_not_its_own() { /// persists its runtime value even while the mode holds the enabled flag. #[test] fn presenter_mode_still_follows_the_highlight_ring_preference() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -464,7 +483,7 @@ fn presenter_mode_still_follows_the_highlight_ring_preference() { /// already equal to its own override and lose it at the next seed refresh. #[test] fn overlay_preference_toggles_leave_both_configs_untouched() { - crate::config::test_helpers::with_temp_config_home(|home| { + test_helpers::with_temp_config_home(|home| { let path = home.join("wayscriber").join("config.toml"); fs::create_dir_all(path.parent().expect("config dir")).expect("config dir"); fs::write( @@ -472,9 +491,9 @@ fn overlay_preference_toggles_leave_both_configs_untouched() { "# authored by hand\n[ui.toolbar]\nuse_icons = true\nlayout_mode = \"regular\"\n", ) .expect("test config should be written"); - let snapshot = crate::config::test_helpers::ConfigFileSnapshot::capture(&path); + let snapshot = ConfigFileSnapshot::capture(&path); - let config = crate::config::Config::default(); + let config = Config::default(); let mut input_state = make_test_input_state(); for event in [ ToolbarEvent::ToggleIconMode(false), @@ -497,7 +516,7 @@ fn overlay_preference_toggles_leave_both_configs_untouched() { input_state.apply_toolbar_event(event); } - let untouched = crate::config::Config::default(); + let untouched = Config::default(); assert_eq!(config.ui.toolbar.use_icons, untouched.ui.toolbar.use_icons); assert_eq!( config.ui.toolbar.show_more_colors, @@ -553,7 +572,7 @@ fn the_toolbar_rebind_gesture_opens_capture_for_the_controls_action() { Action::ClearCanvas, ), ] { - let action = crate::ui::toolbar::model::action_for_event(&event) + let action = model::action_for_event(&event) .unwrap_or_else(|| panic!("{event:?} should name an action")); assert_eq!(action, expected, "{event:?} names the wrong action"); @@ -571,14 +590,14 @@ fn the_toolbar_rebind_gesture_opens_capture_for_the_controls_action() { input_state.take_pending_keybinding_edits().is_empty(), "arming capture must not queue an edit on its own" ); - input_state.on_key_press(crate::input::Key::Escape); + input_state.on_key_press(Key::Escape); } } #[test] fn toolbar_rebind_capture_finalizes_a_held_arrow_bend_before_its_early_return() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -588,7 +607,7 @@ fn toolbar_rebind_capture_finalizes_a_held_arrow_bend_before_its_early_return() let shape_id = input_state .boards .active_frame_mut() - .add_shape(crate::draw::Shape::Arrow { + .add_shape(Shape::Arrow { x1: 0, y1: 100, x2: 400, @@ -598,14 +617,14 @@ fn toolbar_rebind_capture_finalizes_a_held_arrow_bend_before_its_early_return() arrow_length: 20.0, arrow_angle: 30.0, head_at_end: true, - style: crate::draw::ArrowStyle::Curved, + style: ArrowStyle::Curved, bend: 0.0, label: None, }); input_state.set_selection(vec![shape_id]); - input_state.state = crate::input::state::DrawingState::BendingArrow { + input_state.state = DrawingState::BendingArrow { shape_id, - snapshot: crate::draw::frame::ShapeSnapshot { + snapshot: ShapeSnapshot { shape: input_state .boards .active_frame() @@ -626,15 +645,15 @@ fn toolbar_rebind_capture_finalizes_a_held_arrow_bend_before_its_early_return() ); assert_eq!(input_state.keybinding_capture_action(), Some(Action::Undo)); assert!( - matches!(input_state.state, crate::input::state::DrawingState::Idle), + matches!(input_state.state, DrawingState::Idle), "rebind capture returned while the bend gesture was still active" ); // Escape belongs to the newly-opened capture modal, and the later pointer // release must not revive or recommit the already-finalized bend. - input_state.on_key_press(crate::input::Key::Escape); + input_state.on_key_press(Key::Escape); assert!(input_state.keybinding_capture_action().is_none()); - input_state.on_mouse_release(crate::input::MouseButton::Left, 200, 20); + input_state.on_mouse_release(MouseButton::Left, 200, 20); input_state.handle_action_with_resources(test_text_resources, Action::Undo); match input_state .boards @@ -643,7 +662,7 @@ fn toolbar_rebind_capture_finalizes_a_held_arrow_bend_before_its_early_return() .expect("arrow") .shape { - crate::draw::Shape::Arrow { bend, .. } => assert_eq!(bend, 0.0), + Shape::Arrow { bend, .. } => assert_eq!(bend, 0.0), ref other => panic!("expected an arrow, got {other:?}"), } } @@ -665,7 +684,7 @@ fn the_toolbar_controls_actions_resolve_to_their_keybindings_section() { "keybindings/drawing?search=clear canvas", ), ] { - let action = crate::ui::toolbar::model::action_for_event(&event) + let action = model::action_for_event(&event) .unwrap_or_else(|| panic!("{event:?} should name an action")); assert_eq!( keybindings_destination_for_action(action) @@ -696,18 +715,15 @@ fn command_palette_and_shortcut_capture_block_shared_toolbar_events() { fn a_click_right_after_restoring_the_strip_is_dropped() { let mut input_state = make_test_input_state(); input_state.apply_toolbar_event(ToolbarEvent::SetTopMinimized(true)); - assert!(!toolbar_event_dropped( - &input_state, - std::time::Instant::now() - )); + assert!(!toolbar_event_dropped(&input_state, Instant::now())); input_state.apply_toolbar_event(ToolbarEvent::SetTopMinimized(false)); - let now = std::time::Instant::now(); + let now = Instant::now(); assert!(toolbar_event_dropped(&input_state, now)); assert!(!toolbar_event_dropped( &input_state, - now + std::time::Duration::from_secs(1) + now + Duration::from_secs(1) )); } @@ -807,23 +823,19 @@ fn save_as_dialog_selection_keeps_explicit_extension() { #[test] fn missing_recent_open_error_forgets_catalog_entry() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let missing = temp.path().join("missing.wayscriber-session"); - crate::session::catalog::upsert_session_event( - &missing, - crate::session::catalog::CatalogEvent::Opened, - ) - .expect("catalog stale recent"); + catalog::upsert_session_event(&missing, CatalogEvent::Opened).expect("catalog stale recent"); - let err = crate::session::validate_named_session_file_for_open(&missing) + let err = stored_session::validate_named_session_file_for_open(&missing) .expect_err("missing session should fail open validation"); assert!(forget_missing_recent_session_after_open_error( &missing, &err )); assert!( - crate::session::catalog::recent_sessions() + catalog::recent_sessions() .expect("recent sessions") .is_empty(), "missing Open Recent target should be removed from catalog" @@ -836,26 +848,22 @@ fn missing_recent_open_error_forgets_catalog_entry() { #[test] fn missing_recent_parent_open_error_forgets_catalog_entry() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let deleted_parent = temp.path().join("deleted-parent"); fs::create_dir(&deleted_parent).expect("deleted parent"); let missing = deleted_parent.join("missing.wayscriber-session"); - crate::session::catalog::upsert_session_event( - &missing, - crate::session::catalog::CatalogEvent::Opened, - ) - .expect("catalog stale recent"); + catalog::upsert_session_event(&missing, CatalogEvent::Opened).expect("catalog stale recent"); fs::remove_dir(&deleted_parent).expect("remove stale parent"); - let err = crate::session::validate_named_session_file_for_open(&missing) + let err = stored_session::validate_named_session_file_for_open(&missing) .expect_err("missing parent should fail open validation"); assert!(forget_missing_recent_session_after_open_error( &missing, &err )); assert!( - crate::session::catalog::recent_sessions() + catalog::recent_sessions() .expect("recent sessions") .is_empty(), "Open Recent target with a removed parent should be removed from catalog" @@ -864,29 +872,26 @@ fn missing_recent_parent_open_error_forgets_catalog_entry() { #[test] fn non_missing_recent_open_error_keeps_catalog_entry() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let directory = temp.path().join("directory.wayscriber-session"); fs::create_dir(&directory).expect("directory-shaped session target"); - crate::session::catalog::upsert_session_event( - &directory, - crate::session::catalog::CatalogEvent::Opened, - ) - .expect("catalog non-regular recent"); + catalog::upsert_session_event(&directory, CatalogEvent::Opened) + .expect("catalog non-regular recent"); - let err = crate::session::validate_named_session_file_for_open(&directory) + let err = stored_session::validate_named_session_file_for_open(&directory) .expect_err("directory session should fail open validation"); assert!(!forget_missing_recent_session_after_open_error( &directory, &err )); - let recents = crate::session::catalog::recent_sessions().expect("recent sessions"); + let recents = catalog::recent_sessions().expect("recent sessions"); assert_eq!(recents.len(), 1); assert_eq!(PathBuf::from(&recents[0].path), directory); } -fn inspection_for_summary(path: &str) -> crate::session::SessionInspection { - crate::session::SessionInspection { +fn inspection_for_summary(path: &str) -> SessionInspection { + SessionInspection { session_path: PathBuf::from(path), exists: true, size_bytes: Some(14_600), @@ -902,7 +907,7 @@ fn inspection_for_summary(path: &str) -> crate::session::SessionInspection { persist_history: true, restore_tool_state: true, history_limit: None, - frame_counts: Some(crate::session::FrameCounts { + frame_counts: Some(FrameCounts { transparent: 3, whiteboard: 2, blackboard: 1, @@ -984,15 +989,15 @@ fn top_overflow_menu_closes_on_any_non_toggle_event() { &ToolbarEvent::NudgePolygonSides(1) )); assert!(event_dismisses_precision_entry(&ToolbarEvent::SelectTool( - crate::input::Tool::Pen + Tool::Pen ))); assert!(event_dismisses_precision_entry(&ToolbarEvent::Undo)); assert!(!event_dismisses_precision_entry( - &ToolbarEvent::OpenPrecisionEntry(crate::ui::toolbar::PrecisionEntryTarget::Thickness) + &ToolbarEvent::OpenPrecisionEntry(PrecisionEntryTarget::Thickness) )); assert!(!event_dismisses_precision_entry( &ToolbarEvent::CommitPrecisionEntry { - target: crate::ui::toolbar::PrecisionEntryTarget::Thickness, + target: PrecisionEntryTarget::Thickness, value: 4.0, } )); @@ -1079,24 +1084,22 @@ fn settings_popover_survives_its_own_controls_and_dismisses_on_everything_else() ToolbarEvent::TogglePagesSection(true), ToolbarEvent::ToggleStepSection(true), ToolbarEvent::SetToolbarItemCustomizationOpen(true), - ToolbarEvent::SetToolbarItemCustomizationGroup(Some( - crate::ui::toolbar::ToolbarItemCustomizeGroup::TopTools, - )), - ToolbarEvent::SetToolbarItemHidden(crate::config::toolbar_item_ids::TOP_TOOL_PEN, true), + ToolbarEvent::SetToolbarItemCustomizationGroup(Some(ToolbarItemCustomizeGroup::TopTools)), + ToolbarEvent::SetToolbarItemHidden(toolbar_item_ids::TOP_TOOL_PEN, true), ToolbarEvent::MoveToolbarItem { - group: crate::config::ToolbarItemOrderGroup::TopTools, - id: crate::config::toolbar_item_ids::TOP_TOOL_PEN, + group: ToolbarItemOrderGroup::TopTools, + id: toolbar_item_ids::TOP_TOOL_PEN, delta: 1, }, ToolbarEvent::StartToolbarItemDrag { - group: crate::config::ToolbarItemOrderGroup::TopTools, - id: crate::config::toolbar_item_ids::TOP_TOOL_PEN, + group: ToolbarItemOrderGroup::TopTools, + id: toolbar_item_ids::TOP_TOOL_PEN, }, ToolbarEvent::DragToolbarItemOver { - group: crate::config::ToolbarItemOrderGroup::TopTools, + group: ToolbarItemOrderGroup::TopTools, target_index: 1, }, - ToolbarEvent::ResetToolbarItemOrder(crate::config::ToolbarItemOrderGroup::TopTools), + ToolbarEvent::ResetToolbarItemOrder(ToolbarItemOrderGroup::TopTools), ToolbarEvent::ResetToolbarItemHiddenOverrides, ToolbarEvent::OpenCommandPalette, ToolbarEvent::OpenConfigurator, @@ -1149,8 +1152,8 @@ fn section_toggle_event(flag: ToolbarSectionFlag, show: bool) -> ToolbarEvent { #[test] fn backend_session_dispatch_finalizes_a_held_arrow_bend() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -1164,7 +1167,7 @@ fn backend_session_dispatch_finalizes_a_held_arrow_bend() { let shape_id = input_state .boards .active_frame_mut() - .add_shape(crate::draw::Shape::Arrow { + .add_shape(Shape::Arrow { x1: 0, y1: 100, x2: 400, @@ -1174,14 +1177,14 @@ fn backend_session_dispatch_finalizes_a_held_arrow_bend() { arrow_length: 20.0, arrow_angle: 30.0, head_at_end: true, - style: crate::draw::ArrowStyle::Curved, + style: ArrowStyle::Curved, bend: 0.0, label: None, }); input_state.set_selection(vec![shape_id]); - input_state.state = crate::input::state::DrawingState::BendingArrow { + input_state.state = DrawingState::BendingArrow { shape_id, - snapshot: crate::draw::frame::ShapeSnapshot { + snapshot: ShapeSnapshot { shape: input_state .boards .active_frame() @@ -1206,7 +1209,7 @@ fn backend_session_dispatch_finalizes_a_held_arrow_bend() { ); assert!( - matches!(input_state.state, crate::input::state::DrawingState::Idle), + matches!(input_state.state, DrawingState::Idle), "the backend barrier left the bend gesture running" ); // Committed rather than discarded, so the undo stack can take it back. @@ -1218,7 +1221,7 @@ fn backend_session_dispatch_finalizes_a_held_arrow_bend() { .expect("arrow") .shape { - crate::draw::Shape::Arrow { bend, .. } => assert_eq!(bend, 0.0), + Shape::Arrow { bend, .. } => assert_eq!(bend, 0.0), ref other => panic!("expected an arrow, got {other:?}"), } } diff --git a/src/backend/wayland/state/toolbar/fade.rs b/src/backend/wayland/state/toolbar/fade.rs index efa3caa9f..1fa4b0ad3 100644 --- a/src/backend/wayland/state/toolbar/fade.rs +++ b/src/backend/wayland/state/toolbar/fade.rs @@ -10,7 +10,13 @@ use std::time::{Duration, Instant}; -use super::*; +use crate::config::TopDisplayMode; +use crate::input::state::InputState; +use crate::util::Rect; + +use super::super::WaylandState; +use super::geometry; +use crate::backend::wayland::toolbar::layout::top_size; use crate::draw::Color; use crate::input::Tool; use crate::ui::toolbar::snapshot::fade::TopStripFadeInputs; @@ -25,7 +31,7 @@ pub(super) struct StripRevealKey { } impl StripRevealKey { - fn of(input: &crate::input::state::InputState) -> Self { + fn of(input: &InputState) -> Self { let tool = input.tool_override(); Self { tool, @@ -54,7 +60,7 @@ impl WaylandState { // step must damage their rect and request a canvas redraw itself. if before != after && self.inline_toolbars_render_active() { if let Some((x, y, w, h)) = self.toolbar_chrome.inline_rect() - && let Some(rect) = crate::util::Rect::new( + && let Some(rect) = Rect::new( x.floor() as i32 - 1, y.floor() as i32 - 1, w.ceil() as i32 + 2, @@ -84,7 +90,7 @@ impl WaylandState { // already the quiet form, and a hidden strip has nothing to fade. let reduced_chrome = !input.toolbar_top_visible() || input.toolbar_top_minimized() - || input.toolbar_top_display_mode() == crate::config::TopDisplayMode::Micro; + || input.toolbar_top_display_mode() == TopDisplayMode::Micro; let menus_open = top_menus_open(input); let idle_fade_enabled = top_strip_idle_fade_enabled(input, self.first_run_holds_toolbar()); let on_strip = self.toolbar_chrome.strip_engaged() || self.toolbar.top_pointer_present(); @@ -150,32 +156,35 @@ impl WaylandState { /// Keep the toolbar visible while first-run guidance holds it, without /// changing the idle-fade preference. -fn top_strip_idle_fade_enabled( - input: &crate::input::state::InputState, - first_run_holds_toolbar: bool, -) -> bool { +fn top_strip_idle_fade_enabled(input: &InputState, first_run_holds_toolbar: bool) -> bool { input.ui_visibility.idle_fade && !first_run_holds_toolbar } /// True while any top-strip-anchored menu or popover is open. Open menus /// hold the idle fade: the strip (and the popover hosted on its surface) /// must stay visible while one is up, even with the pointer away. -fn top_menus_open(input: &crate::input::state::InputState) -> bool { +fn top_menus_open(input: &InputState) -> bool { input.toolbar_top_menu().is_open() || input.is_color_picker_popup_open() } #[cfg(test)] mod tests { + use crate::draw::Color; + use crate::ui::toolbar::snapshot::fade::{TopStripFade, TopStripFadeInputs}; + + use std::time::{Duration, Instant}; + use super::{StripRevealKey, top_menus_open, top_strip_idle_fade_enabled}; use crate::input::Tool; - use crate::input::state::{TopMenuState, test_support::make_test_input_state}; + use crate::input::state::TopMenuState; + use crate::input::state::test_support::make_test_input_state; /// Opacity after `idle_for` without drawing, pointer away, no menus open. fn idle_opacity(idle_fade_enabled: bool) -> f64 { - let now = std::time::Instant::now(); - let mut fade = crate::ui::toolbar::snapshot::fade::TopStripFade::new(); - let inputs = crate::ui::toolbar::snapshot::fade::TopStripFadeInputs { - idle_for: std::time::Duration::from_secs(10), + let now = Instant::now(); + let mut fade = TopStripFade::new(); + let inputs = TopStripFadeInputs { + idle_for: Duration::from_secs(10), pointer_near: false, menus_open: false, reduced_chrome: false, @@ -183,7 +192,7 @@ mod tests { }; fade.update(&inputs, now); - fade.update(&inputs, now + std::time::Duration::from_secs(10)) + fade.update(&inputs, now + Duration::from_secs(10)) } /// While first-run guidance holds the toolbar, the strip stays fully @@ -245,7 +254,7 @@ mod tests { let marker = StripRevealKey::of(&input); assert_ne!(marker, initial, "tool shortcut"); - let color = crate::draw::Color { + let color = Color { r: 0.1, g: 0.8, b: 0.3, diff --git a/src/backend/wayland/state/toolbar/geometry.rs b/src/backend/wayland/state/toolbar/geometry.rs index 7386f0189..ae8d1e565 100644 --- a/src/backend/wayland/state/toolbar/geometry.rs +++ b/src/backend/wayland/state/toolbar/geometry.rs @@ -146,6 +146,11 @@ pub(super) fn point_in_top_strip_reveal_zone( #[cfg(test)] mod tests { + use crate::backend::wayland::toolbar::layout; + use crate::input::state::test_support; + use crate::ui::toolbar::{ToolbarBindingHints, ToolbarSnapshot}; + use crate::ui_text::UiTextEngine; + use super::*; #[test] @@ -156,17 +161,11 @@ mod tests { assert_eq!(budget, 764.0); assert_eq!(base + budget + 12.0, 800.0); - let state = crate::input::state::test_support::make_test_input_state(); - let mut snapshot = crate::ui::toolbar::ToolbarSnapshot::from_input_with_bindings( - &state, - crate::ui::toolbar::ToolbarBindingHints::default(), - ); + let state = test_support::make_test_input_state(); + let mut snapshot = + ToolbarSnapshot::from_input_with_bindings(&state, ToolbarBindingHints::default()); snapshot.top_viewport_max = Some(budget); - let planned = crate::backend::wayland::toolbar::layout::top_size( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - ) - .0 as f64; + let planned = layout::top_size(&UiTextEngine::default(), &snapshot).0 as f64; assert!(planned <= budget, "planned={planned}, budget={budget}"); } diff --git a/src/backend/wayland/state/toolbar/inline/drag.rs b/src/backend/wayland/state/toolbar/inline/drag.rs index 68eba3553..7a8c69b75 100644 --- a/src/backend/wayland/state/toolbar/inline/drag.rs +++ b/src/backend/wayland/state/toolbar/inline/drag.rs @@ -1,4 +1,6 @@ -use super::*; +use crate::backend::wayland::toolbar_intent::ToolbarIntent; + +use super::super::{MoveDragKind, WaylandState}; impl WaylandState { /// Generate a drag intent from the active toolbar move drag state. @@ -8,7 +10,7 @@ impl WaylandState { &self, x: f64, y: f64, - ) -> Option { + ) -> Option { use crate::backend::wayland::toolbar_intent::ToolbarIntent; use crate::ui::toolbar::ToolbarEvent; diff --git a/src/backend/wayland/state/toolbar/inline/focus.rs b/src/backend/wayland/state/toolbar/inline/focus.rs index 9645746fd..d033daf20 100644 --- a/src/backend/wayland/state/toolbar/inline/focus.rs +++ b/src/backend/wayland/state/toolbar/inline/focus.rs @@ -1,4 +1,4 @@ -use super::*; +use super::super::{DrawingState, ToolbarEvent, WaylandState}; use crate::input::Key; impl WaylandState { diff --git a/src/backend/wayland/state/toolbar/inline/input.rs b/src/backend/wayland/state/toolbar/inline/input.rs index dd5ed3e44..91d8761e6 100644 --- a/src/backend/wayland/state/toolbar/inline/input.rs +++ b/src/backend/wayland/state/toolbar/inline/input.rs @@ -1,6 +1,8 @@ use std::time::Instant; -use super::*; +use crate::backend::wayland::toolbar_intent::ToolbarIntent; + +use super::super::{ToolbarEvent, WaylandState, drag_log, intent_to_event}; use crate::backend::wayland::state::contact_owner::{InlinePress, inline_primary_press}; impl WaylandState { @@ -45,7 +47,7 @@ impl WaylandState { pub(in crate::backend::wayland) fn inline_toolbar_hit_at( &self, position: (f64, f64), - ) -> Option<(crate::backend::wayland::toolbar_intent::ToolbarIntent, bool)> { + ) -> Option<(ToolbarIntent, bool)> { if !self.toolbar_chrome.inline_toolbars() || !self.toolbar.is_visible() { return None; } @@ -83,10 +85,7 @@ impl WaylandState { true } - fn inline_toolbar_drag_at( - &self, - position: (f64, f64), - ) -> Option { + fn inline_toolbar_drag_at(&self, position: (f64, f64)) -> Option { if !self.toolbar_chrome.inline_toolbars() || !self.toolbar.is_visible() { return None; } diff --git a/src/backend/wayland/state/toolbar/inline/mod.rs b/src/backend/wayland/state/toolbar/inline/mod.rs index 1f6abc7d3..b9595027a 100644 --- a/src/backend/wayland/state/toolbar/inline/mod.rs +++ b/src/backend/wayland/state/toolbar/inline/mod.rs @@ -1,5 +1,9 @@ -use super::*; +use crate::draw::DirtyFullReason; +use crate::util::Rect; + +use super::super::WaylandState; use crate::backend::wayland::toolbar::ToolbarCursorHint; +use std::time::{Duration, Instant}; mod drag; mod focus; @@ -15,7 +19,7 @@ impl WaylandState { /// leave the canvas alone. pub(in crate::backend::wayland) fn mark_inline_toolbar_rect_damage(&mut self) { if let Some((x, y, w, h)) = self.toolbar_chrome.inline_rect() - && let Some(rect) = crate::util::Rect::new( + && let Some(rect) = Rect::new( x.floor() as i32 - 1, y.floor() as i32 - 1, w.ceil() as i32 + 2, @@ -31,7 +35,7 @@ impl WaylandState { pub(in crate::backend::wayland) fn mark_inline_toolbar_full_damage(&mut self) { self.input_state .dirty_tracker - .mark_full_for(crate::draw::DirtyFullReason::InlineToolbar); + .mark_full_for(DirtyFullReason::InlineToolbar); self.toolbar.mark_dirty(); self.input_state.needs_redraw = true; } diff --git a/src/backend/wayland/state/toolbar/inline/render.rs b/src/backend/wayland/state/toolbar/inline/render.rs index 9e3e0b66b..b2a62105c 100644 --- a/src/backend/wayland/state/toolbar/inline/render.rs +++ b/src/backend/wayland/state/toolbar/inline/render.rs @@ -1,4 +1,8 @@ -use super::*; +use crate::backend::wayland::state::WaylandState; +use crate::backend::wayland::toolbar::hit; +use crate::backend::wayland::toolbar::layout::top_size; +use crate::backend::wayland::toolbar::render::render_top_strip; +use crate::ui::toolbar::ToolbarSnapshot; impl WaylandState { pub(in crate::backend::wayland) fn render_inline_toolbars( @@ -67,7 +71,7 @@ impl WaylandState { top_size.0 as f64, top_size.1 as f64, ); - crate::backend::wayland::toolbar::hit::clip_hit_regions_to_bounds(&mut hits, 0, top_rect); + hit::clip_hit_regions_to_bounds(&mut hits, 0, top_rect); self.toolbar_chrome.set_inline_rendered(hits, top_rect); } } diff --git a/src/backend/wayland/state/toolbar/scroll.rs b/src/backend/wayland/state/toolbar/scroll.rs index 8d72ce47f..b39729374 100644 --- a/src/backend/wayland/state/toolbar/scroll.rs +++ b/src/backend/wayland/state/toolbar/scroll.rs @@ -1,9 +1,13 @@ use smithay_client_toolkit::seat::pointer::AxisScroll; use wayland_client::protocol::wl_surface; -use super::*; +use crate::ui::toolbar::model::StylePillSlider; + +use super::super::WaylandState; +use super::geometry; use crate::backend::wayland::toolbar::layout::top_popover_scroll_bounds; use crate::ui::toolbar::model::StrokeSetting; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; /// Wheel step for the top-strip Canvas/Session/Settings popovers, in pre-scale /// spec units. @@ -150,13 +154,11 @@ impl WaylandState { &self, surface: &wl_surface::WlSurface, position: (f64, f64), - ) -> Option { + ) -> Option { self.toolbar .top_hit_at(surface, position) .or_else(|| self.inline_toolbar_hit_at(position)) - .and_then(|(intent, _)| { - crate::ui::toolbar::model::StylePillSlider::for_wheel(&intent.0) - }) + .and_then(|(intent, _)| StylePillSlider::for_wheel(&intent.0)) } /// The meter setting whose bar is under the pointer, on the toolbar diff --git a/src/backend/wayland/state/toolbar/visibility/access.rs b/src/backend/wayland/state/toolbar/visibility/access.rs index f9f90c38d..83512efc0 100644 --- a/src/backend/wayland/state/toolbar/visibility/access.rs +++ b/src/backend/wayland/state/toolbar/visibility/access.rs @@ -1,4 +1,4 @@ -use super::*; +use super::WaylandState; impl WaylandState { pub(in crate::backend::wayland) fn inline_toolbars_render_active(&self) -> bool { diff --git a/src/backend/wayland/state/toolbar/visibility/mod.rs b/src/backend/wayland/state/toolbar/visibility/mod.rs index 2eaa91a4e..6fa8cdf27 100644 --- a/src/backend/wayland/state/toolbar/visibility/mod.rs +++ b/src/backend/wayland/state/toolbar/visibility/mod.rs @@ -1,4 +1,5 @@ -use super::*; +use super::super::WaylandState; +use smithay_client_toolkit::shell::wlr_layer::KeyboardInteractivity; mod access; mod pointer; diff --git a/src/backend/wayland/state/toolbar/visibility/pointer.rs b/src/backend/wayland/state/toolbar/visibility/pointer.rs index df2d1be60..51f19f58d 100644 --- a/src/backend/wayland/state/toolbar/visibility/pointer.rs +++ b/src/backend/wayland/state/toolbar/visibility/pointer.rs @@ -1,7 +1,12 @@ -use super::*; +use crate::backend::wayland::state::WaylandState; +use crate::backend::wayland::state::helpers::{drag_log, surface_id, toolbar_pointer_lock_enabled}; use crate::env_vars::TOOLBAR_POINTER_LOCK_ENV; +use log::warn; +use smithay_client_toolkit::globals::ProvidesBoundGlobal; use smithay_client_toolkit::seat::pointer::PointerData; -use wayland_client::{Proxy, protocol::wl_surface}; +use wayland_client::protocol::wl_surface; +use wayland_client::{Proxy, QueueHandle}; +use wayland_protocols::wp::pointer_constraints::zv1::client::zwp_pointer_constraints_v1; impl WaylandState { pub(in crate::backend::wayland) fn pointer_lock_active(&self) -> bool { diff --git a/src/backend/wayland/state/toolbar/visibility/sync.rs b/src/backend/wayland/state/toolbar/visibility/sync.rs index 4c37a0a1e..897cd6d43 100644 --- a/src/backend/wayland/state/toolbar/visibility/sync.rs +++ b/src/backend/wayland/state/toolbar/visibility/sync.rs @@ -1,11 +1,22 @@ -use super::*; +use crate::toolbar_gtk::GtkToolbarKind; + +use std::env; + +use super::super::chrome::ConfigureVerdict; +use super::{KeyboardInteractivityPolicyInput, keyboard_interactivity_for}; +use crate::backend::wayland::state::WaylandState; +use crate::backend::wayland::state::helpers::{debug_toolbar_drag_logging_enabled, drag_log}; use crate::env_vars::{XDG_CURRENT_DESKTOP_ENV, XDG_SESSION_DESKTOP_ENV}; +use crate::ui::toolbar::ToolbarSnapshot; +use log::{debug, warn}; use smithay_client_toolkit::shell::WaylandSurface; +use smithay_client_toolkit::shell::wlr_layer::KeyboardInteractivity; +use wayland_client::QueueHandle; fn toolbar_visibility_for_frontend( requested: bool, gtk_active: bool, - gtk_drag_preview: Option, + gtk_drag_preview: Option, chrome_suppressed: bool, ) -> bool { if chrome_suppressed { @@ -14,7 +25,7 @@ fn toolbar_visibility_for_frontend( if !gtk_active { return requested; } - requested && gtk_drag_preview == Some(crate::toolbar_gtk::GtkToolbarKind::Top) + requested && gtk_drag_preview == Some(GtkToolbarKind::Top) } impl WaylandState { @@ -43,10 +54,8 @@ impl WaylandState { return; } - let desktop_env = - std::env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_else(|_| "unknown".into()); - let session_env = - std::env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_else(|_| "unknown".into()); + let desktop_env = env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_else(|_| "unknown".into()); + let session_env = env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_else(|_| "unknown".into()); log::info!( "Layer-shell protocol unavailable; toolbar surfaces will not appear (desktop='{}', session='{}'). Overlay may be limited to the work area on compositors like GNOME.", desktop_env, @@ -248,6 +257,9 @@ impl WaylandState { #[cfg(test)] mod tests { + use crate::input::InputState; + use crate::input::state::test_support; + use super::*; use crate::toolbar_gtk::GtkToolbarKind; @@ -281,8 +293,8 @@ mod tests { #[test] fn command_palette_hides_the_builtin_bars_and_closing_it_restores_them() { - let mut input_state = crate::input::state::test_support::make_test_input_state(); - let visible = |input: &crate::input::InputState| { + let mut input_state = test_support::make_test_input_state(); + let visible = |input: &InputState| { toolbar_visibility_for_frontend( input.toolbar_top_visible(), false, diff --git a/src/backend/wayland/state/zoom.rs b/src/backend/wayland/state/zoom.rs index aab3a9f92..049e6139e 100644 --- a/src/backend/wayland/state/zoom.rs +++ b/src/backend/wayland/state/zoom.rs @@ -1,6 +1,9 @@ -use super::*; +use super::WaylandState; +use super::core::overlay::{OverlaySuppression, OverlaySuppressionKeyboardPolicy}; use crate::backend::wayland::zoom::ZoomCaptureBackend; -use crate::input::{ZoomAnchor, ZoomRequest}; +use crate::input::{ZoomAction, ZoomAnchor, ZoomRequest}; +use anyhow::Result; +use log::{debug, info, warn}; impl WaylandState { pub(in crate::backend::wayland) fn sync_input_zoom_state(&mut self) { diff --git a/src/backend/wayland/surface.rs b/src/backend/wayland/surface.rs index 12c2405fb..d08038f6e 100644 --- a/src/backend/wayland/surface.rs +++ b/src/backend/wayland/surface.rs @@ -6,17 +6,13 @@ use anyhow::{Context, Result}; use log::info; -use smithay_client_toolkit::{ - shell::{WaylandSurface, wlr_layer::LayerSurface, xdg::window::Window}, - shm::{ - Shm, - slot::{Buffer, Slot, SlotPool}, - }, -}; -use wayland_client::{ - Proxy, - protocol::{wl_output, wl_shm, wl_surface}, -}; +use smithay_client_toolkit::shell::WaylandSurface; +use smithay_client_toolkit::shell::wlr_layer::LayerSurface; +use smithay_client_toolkit::shell::xdg::window::Window; +use smithay_client_toolkit::shm::Shm; +use smithay_client_toolkit::shm::slot::{Buffer, Slot, SlotPool}; +use wayland_client::Proxy; +use wayland_client::protocol::{wl_output, wl_shm, wl_surface}; mod output_membership; use output_membership::OutputMembership; @@ -426,17 +422,21 @@ impl SurfaceState { #[cfg(test)] mod tests { + use std::os::unix::net::UnixStream; + + use smithay_client_toolkit::error::GlobalError; + use smithay_client_toolkit::globals::ProvidesBoundGlobal; + use wayland_client::protocol::wl_registry::WlRegistry; + use super::*; struct BufferGlobals(wl_shm::WlShm); - wayland_client::delegate_noop!(BufferGlobals: ignore wayland_client::protocol::wl_registry::WlRegistry); + wayland_client::delegate_noop!(BufferGlobals: ignore WlRegistry); wayland_client::delegate_noop!(BufferGlobals: ignore wl_shm::WlShm); - impl smithay_client_toolkit::globals::ProvidesBoundGlobal for BufferGlobals { - fn bound_global( - &self, - ) -> Result { + impl ProvidesBoundGlobal for BufferGlobals { + fn bound_global(&self) -> Result { Ok(self.0.clone()) } } @@ -445,7 +445,7 @@ mod tests { fn capture_buffer_availability_tracks_real_slot_activation_release_and_pool_reset() { // Only local client objects are needed: SCTK owns the slot/active-buffer // bookkeeping. Keep a private socket peer alive; never use the desktop. - let (client, _peer) = std::os::unix::net::UnixStream::pair().unwrap(); + let (client, _peer) = UnixStream::pair().unwrap(); let connection = wayland_client::Connection::from_socket(client).unwrap(); let queue = connection.new_event_queue::(); let qh = queue.handle(); diff --git a/src/backend/wayland/surface/output_membership.rs b/src/backend/wayland/surface/output_membership.rs index 1894e2d70..a1f1eae44 100644 --- a/src/backend/wayland/surface/output_membership.rs +++ b/src/backend/wayland/surface/output_membership.rs @@ -78,12 +78,14 @@ impl OutputMembership { #[cfg(test)] mod tests { + use crate::config::Config; + use super::*; use crate::backend::wayland::handlers::test_support::HandlerFixture; #[test] fn layer_placement_takes_precedence_and_falls_back_to_remaining_output() { - let fixture = HandlerFixture::with_outputs(crate::config::Config::default(), 2); + let fixture = HandlerFixture::with_outputs(Config::default(), 2); let outputs: Vec<_> = fixture.state.protocol.output().outputs().collect(); let mut membership = OutputMembership::for_layer(Some(outputs[1].clone())); @@ -107,7 +109,7 @@ mod tests { #[test] fn assumed_output_survives_unrelated_removal_until_the_first_enter() { - let fixture = HandlerFixture::with_outputs(crate::config::Config::default(), 3); + let fixture = HandlerFixture::with_outputs(Config::default(), 3); let outputs: Vec<_> = fixture.state.protocol.output().outputs().collect(); let mut membership = OutputMembership::default(); membership.assume(outputs[0].clone()); @@ -124,7 +126,7 @@ mod tests { #[test] fn unrelated_removal_preserves_pre_enter_placement_but_target_loss_releases_it() { - let fixture = HandlerFixture::with_outputs(crate::config::Config::default(), 2); + let fixture = HandlerFixture::with_outputs(Config::default(), 2); let outputs: Vec<_> = fixture.state.protocol.output().outputs().collect(); let mut membership = OutputMembership::default(); membership.request_output(outputs[0].clone()); diff --git a/src/backend/wayland/surface/placement.rs b/src/backend/wayland/surface/placement.rs index 3b0be8992..ac9c1c133 100644 --- a/src/backend/wayland/surface/placement.rs +++ b/src/backend/wayland/surface/placement.rs @@ -1,6 +1,8 @@ //! Layer policy and bounded XDG fullscreen placement for frozen mode. use std::time::{Duration, Instant}; +use smithay_client_toolkit::shell::wlr_layer::Layer; + const XDG_FROZEN_FULLSCREEN_TIMEOUT: Duration = Duration::from_millis(1500); #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -84,13 +86,11 @@ impl SurfacePlacement { self.xdg_fullscreen } - pub(in crate::backend::wayland) fn layer( - &self, - ) -> smithay_client_toolkit::shell::wlr_layer::Layer { + pub(in crate::backend::wayland) fn layer(&self) -> Layer { if self.main_surface_uses_overlay_layer { - smithay_client_toolkit::shell::wlr_layer::Layer::Overlay + Layer::Overlay } else { - smithay_client_toolkit::shell::wlr_layer::Layer::Top + Layer::Top } } @@ -144,9 +144,6 @@ mod tests { assert_eq!(placement.preferred_output_identity(), Some("DP-1")); assert!(placement.xdg_fullscreen()); - assert_eq!( - placement.layer(), - smithay_client_toolkit::shell::wlr_layer::Layer::Overlay - ); + assert_eq!(placement.layer(), Layer::Overlay); } } diff --git a/src/backend/wayland/tablet_types.rs b/src/backend/wayland/tablet_types.rs index dba311706..fea3eef81 100644 --- a/src/backend/wayland/tablet_types.rs +++ b/src/backend/wayland/tablet_types.rs @@ -1,3 +1,5 @@ +use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::Type; + /// Physical tool types as defined by the zwp_tablet_tool_v2 protocol. /// /// See: https://wayland.app/protocols/tablet-v2#zwp_tablet_tool_v2:enum:type @@ -62,16 +64,8 @@ impl TabletToolType { } } -impl - From< - wayland_client::WEnum, - > for TabletToolType -{ - fn from( - tool_type: wayland_client::WEnum< - wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::Type, - >, - ) -> Self { +impl From> for TabletToolType { + fn from(tool_type: wayland_client::WEnum) -> Self { use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::Type; let raw_value: u32 = match tool_type { wayland_client::WEnum::Value(v) => match v { diff --git a/src/backend/wayland/toolbar/hit.rs b/src/backend/wayland/toolbar/hit.rs index ddb257c89..c736c0511 100644 --- a/src/backend/wayland/toolbar/hit.rs +++ b/src/backend/wayland/toolbar/hit.rs @@ -17,7 +17,7 @@ pub struct HitRegion { pub focus_id: Option, pub rect: (f64, f64, f64, f64), // x, y, w, h pub event: ToolbarEvent, - pub kind: crate::backend::wayland::toolbar::events::HitKind, + pub kind: HitKind, pub tooltip: Option, } @@ -161,7 +161,11 @@ enum HitPhase { /// Both entry points below share this: keeping two copies meant every slider /// spec and every new hit kind had to be written twice, in step, forever. fn event_for_hit(hit: &HitRegion, x: f64, y: f64, phase: HitPhase) -> Option { - use crate::backend::wayland::toolbar::events::HitKind::*; + use crate::backend::wayland::toolbar::events::HitKind::{ + Click, DragCustomRedoDelay, DragCustomUndoDelay, DragMoveTop, DragRedoDelay, + DragScrollTopPopover, DragSetFontSize, DragSetMarkerOpacity, DragSetSpotlightMagnification, + DragSetThickness, DragToolbarItem, DragUndoDelay, + }; let event = match hit.kind { DragSetThickness { min, max } => slider_event_for_hit( @@ -390,6 +394,9 @@ pub fn focused_event(hits: &[HitRegion], focus: Option) -> Option HitRegion { @@ -614,8 +621,8 @@ mod tests { #[test] fn secondary_press_resolves_only_quick_color_swatches() { let swatch = click(ToolbarEvent::SetQuickColor { - color: crate::draw::color::RED, - action: Some(crate::config::Action::SetColorRed), + color: color::RED, + action: Some(Action::SetColorRed), index: 6, }); diff --git a/src/backend/wayland/toolbar/layout/spec/top.rs b/src/backend/wayland/toolbar/layout/spec/top.rs index 0f792bdc2..e560e7d21 100644 --- a/src/backend/wayland/toolbar/layout/spec/top.rs +++ b/src/backend/wayland/toolbar/layout/spec/top.rs @@ -1,4 +1,6 @@ -use crate::ui::toolbar::ToolbarSnapshot; +use crate::backend::wayland::toolbar::view::top; +use crate::ui::theme::toolbar; +use crate::ui::toolbar::{ToolbarSnapshot, model}; use crate::ui_text::UiTextEngine; use super::ToolbarLayoutSpec; @@ -7,8 +9,8 @@ impl ToolbarLayoutSpec { pub(in crate::backend::wayland::toolbar) const TOP_SIZE_ICONS: (u32, u32) = (735, 58); /// Minimized top strip: the edge restore tab (`model::RESTORE_TAB_SIZE`). pub(in crate::backend::wayland::toolbar) const TOP_MINIMIZED_SIZE: (u32, u32) = ( - crate::ui::toolbar::model::RESTORE_TAB_SIZE.0 as u32, - crate::ui::toolbar::model::RESTORE_TAB_SIZE.1 as u32, + model::RESTORE_TAB_SIZE.0 as u32, + model::RESTORE_TAB_SIZE.1 as u32, ); /// Micro-mode top strip: one round tool/color chip. pub(in crate::backend::wayland::toolbar) const TOP_MICRO_SIZE: (u32, u32) = (44, 44); @@ -21,8 +23,7 @@ impl ToolbarLayoutSpec { /// The number lives in `theme::toolbar::ISLAND_PAD` so the GTK /// stylesheet interpolates the same island padding — one source for /// both frontends (theme must not depend on this backend module). - pub(in crate::backend::wayland::toolbar) const TOP_ISLAND_PAD: f64 = - crate::ui::theme::toolbar::ISLAND_PAD; + pub(in crate::backend::wayland::toolbar) const TOP_ISLAND_PAD: f64 = toolbar::ISLAND_PAD; pub(in crate::backend::wayland::toolbar) const TOP_START_X: f64 = 19.0; pub(in crate::backend::wayland::toolbar) const TOP_HANDLE_SIZE: f64 = 18.0; pub(in crate::backend::wayland::toolbar) const TOP_HANDLE_Y: f64 = 20.0; @@ -73,7 +74,7 @@ impl ToolbarLayoutSpec { /// standard control gap, so the segment does not crowd the numeral to its /// left (M7-C3). Shares `theme::toolbar::SEGMENT_LEADING_GAP`. pub(in crate::backend::wayland::toolbar) const TOP_STYLE_SEGMENT_LEAD: f64 = - crate::ui::theme::toolbar::SEGMENT_LEADING_GAP; + toolbar::SEGMENT_LEADING_GAP; /// Docked selection-property value button width (cycle buttons and /// the readout between stepper halves). pub(in crate::backend::wayland::toolbar) const TOP_STYLE_SEL_VALUE_W: f64 = 64.0; @@ -85,7 +86,7 @@ impl ToolbarLayoutSpec { pub(in crate::backend::wayland::toolbar) const TOP_STYLE_CAPTION_W: f64 = 56.0; /// Zero dot plus the original 84px bar row, shared by both level meters. pub(in crate::backend::wayland::toolbar) const TOP_STYLE_METER_W: f64 = - 84.0 + crate::ui::toolbar::model::METER_ZERO_SLOT_W; + 84.0 + model::METER_ZERO_SLOT_W; /// The Pen feel chip ("Pen feel ▾"): a fixed slot like the font button's, /// so the planner budgets it whatever the levels behind it. pub(in crate::backend::wayland::toolbar) const TOP_STYLE_PEN_FEEL_W: f64 = 92.0; @@ -109,13 +110,11 @@ impl ToolbarLayoutSpec { let mut height = base_height as f64; // Popovers (shapes grid + options, overflow) and the contextual // highlight-ring row grow the surface below the bar. - height += crate::backend::wayland::toolbar::view::top::top_extra_height(engine, snapshot); + height += top::top_extra_height(engine, snapshot); // Width comes from the same tree walk the builder performs, so the // size math and the builder cannot drift apart. - let width = crate::backend::wayland::toolbar::view::top::top_natural_width( - engine, snapshot, height, - ); + let width = top::top_natural_width(engine, snapshot, height); (width.ceil() as u32, height.ceil() as u32) } diff --git a/src/backend/wayland/toolbar/layout/tests/mod.rs b/src/backend/wayland/toolbar/layout/tests/mod.rs index 4bbfeb909..e1455f5d8 100644 --- a/src/backend/wayland/toolbar/layout/tests/mod.rs +++ b/src/backend/wayland/toolbar/layout/tests/mod.rs @@ -1,3 +1,9 @@ +use crate::backend::wayland::toolbar::view::{WidgetKind, top}; +use crate::config::{ToolbarItemOrderGroup, ToolbarLayoutMode, toolbar_item_ids}; +use crate::input::Tool; +use crate::input::state::test_support::TestInputStateBuilder; +use crate::ui::toolbar::model::{TopStripPlan, TopUtilityButton}; + use super::*; use crate::draw::FontDescriptor; use crate::input::InputState; @@ -5,7 +11,7 @@ use crate::input::state::TopMenuState; use crate::ui::toolbar::{ToolbarBindingHints, ToolbarEvent, ToolbarSnapshot}; fn create_test_input_state() -> InputState { - crate::input::state::test_support::TestInputStateBuilder::default() + TestInputStateBuilder::default() .thickness(3.0) .eraser_size(12.0) .font_descriptor(FontDescriptor { @@ -33,17 +39,11 @@ fn top_size_respects_icon_mode() { // pin and minimize. Height adds the contextual // style pill under the 58px island band (6px gap + 40px pill) while a // drawing tool is active. - assert_eq!( - top_size(&crate::ui_text::UiTextEngine::default(), &snapshot), - (1410, 104) - ); + assert_eq!(top_size(&UiTextEngine::default(), &snapshot), (1410, 104)); state.set_toolbar_use_icons(false); let snapshot = snapshot_from_state(&state); - assert_eq!( - top_size(&crate::ui_text::UiTextEngine::default(), &snapshot).1, - 106 - ); + assert_eq!(top_size(&UiTextEngine::default(), &snapshot).1, 106); } #[test] @@ -54,50 +54,30 @@ fn narrow_viewports_drop_presets_then_overflow_items() { // Unconstrained: presets shown, the pill's full swatch row available, // nothing dropped into the overflow. - let full = crate::backend::wayland::toolbar::view::top::plan_top_strip( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - ); + let full = top::plan_top_strip(&UiTextEngine::default(), &snapshot); assert!(!full.drop_presets); - assert_eq!( - full.swatch_count, - crate::ui::toolbar::model::TopStripPlan::MAX_QUICK_COLORS - ); + assert_eq!(full.swatch_count, TopStripPlan::MAX_QUICK_COLORS); assert!(full.dropped_tools.is_empty() && full.dropped_utilities.is_empty()); - let full_width = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot).0; + let full_width = top_size(&UiTextEngine::default(), &snapshot).0; // Slightly narrow: the non-essential presets island yields first, before // any tool or utility is dropped (M7-C2). snapshot.top_viewport_max = Some(full_width as f64 - 60.0); - let degraded = crate::backend::wayland::toolbar::view::top::plan_top_strip( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - ); + let degraded = top::plan_top_strip(&UiTextEngine::default(), &snapshot); assert!(degraded.drop_presets); assert!(degraded.dropped_tools.is_empty() && degraded.dropped_utilities.is_empty()); - assert!( - top_size(&crate::ui_text::UiTextEngine::default(), &snapshot).0 as f64 - <= full_width as f64 - 60.0 - ); + assert!(top_size(&UiTextEngine::default(), &snapshot).0 as f64 <= full_width as f64 - 60.0); // Very narrow: droppable items move into the overflow menu; the protected // core (Pen, Eraser, Undo/Redo, Clear) stays. Colors and presets have // already left the strip. snapshot.top_viewport_max = Some(700.0); - let tight = crate::backend::wayland::toolbar::view::top::plan_top_strip( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - ); + let tight = top::plan_top_strip(&UiTextEngine::default(), &snapshot); assert!(tight.drop_presets); assert!(!tight.dropped_utilities.is_empty()); - assert!(top_size(&crate::ui_text::UiTextEngine::default(), &snapshot).0 as f64 <= 700.0); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let tree = crate::backend::wayland::toolbar::view::top::build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + assert!(top_size(&UiTextEngine::default(), &snapshot).0 as f64 <= 700.0); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let tree = top::build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); for id in [ "top.tool.pen", "top.tool.eraser", @@ -115,13 +95,8 @@ fn narrow_viewports_drop_presets_then_overflow_items() { // Opening the overflow reveals Clear first, then the dropped items. snapshot.top_overflow_open = true; - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let tree = crate::backend::wayland::toolbar::view::top::build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let tree = top::build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); let overflow_ids: Vec<&str> = tree .nodes() .iter() @@ -147,20 +122,14 @@ fn overflow_contains_only_visible_items_and_is_structural() { let mut state = create_test_input_state(); state.set_toolbar_use_icons(true); let mut items = state.toolbar_items().clone(); - items.set_hidden(crate::config::toolbar_item_ids::TOP_UTILITY_HIGHLIGHT, true); - items.set_hidden( - crate::config::toolbar_item_ids::TOP_UTILITY_SCREENSHOT, - true, - ); - items.set_hidden(crate::config::toolbar_item_ids::TOP_CHROME_OVERFLOW, true); + items.set_hidden(toolbar_item_ids::TOP_UTILITY_HIGHLIGHT, true); + items.set_hidden(toolbar_item_ids::TOP_UTILITY_SCREENSHOT, true); + items.set_hidden(toolbar_item_ids::TOP_CHROME_OVERFLOW, true); state.test_set_toolbar_items(items); let mut snapshot = snapshot_from_state(&state); snapshot.top_viewport_max = Some(700.0); - let plan = crate::backend::wayland::toolbar::view::top::plan_top_strip( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - ); + let plan = top::plan_top_strip(&UiTextEngine::default(), &snapshot); assert!( !plan.dropped_tools.is_empty() || !plan.dropped_utilities.is_empty(), "the 700px budget must force items into the overflow: {plan:?}" @@ -168,22 +137,17 @@ fn overflow_contains_only_visible_items_and_is_structural() { assert!( !plan .dropped_utilities - .contains(&crate::ui::toolbar::model::TopUtilityButton::Screenshot) + .contains(&TopUtilityButton::Screenshot) ); assert!( !plan .dropped_utilities - .contains(&crate::ui::toolbar::model::TopUtilityButton::Highlight) + .contains(&TopUtilityButton::Highlight) ); snapshot.top_overflow_open = true; - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let tree = crate::backend::wayland::toolbar::view::top::build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let tree = top::build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); assert!(tree.node_by_id(&"top.chrome.overflow".into()).is_some()); assert!( tree.node_by_id(&"top.overflow.top.utility.screenshot".into()) @@ -202,7 +166,7 @@ fn top_strip_fits_480_pixels_in_icon_and_text_modes() { state.set_toolbar_use_icons(use_icons); let mut snapshot = snapshot_from_state(&state); snapshot.top_viewport_max = Some(480.0); - let (width, _) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let (width, _) = top_size(&UiTextEngine::default(), &snapshot); assert!( width <= 480, "{} mode planned width {width} exceeds 480", @@ -218,14 +182,11 @@ fn compact_top_strip_moves_shape_pen_to_the_overflow_before_other_pens() { let mut snapshot = snapshot_from_state(&state); snapshot.top_viewport_max = Some(320.0); - let plan = crate::backend::wayland::toolbar::view::top::plan_top_strip( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - ); + let plan = top::plan_top_strip(&UiTextEngine::default(), &snapshot); assert!(plan.compact); - assert!(plan.dropped_tools.contains(&crate::input::Tool::LiveShape)); - assert!(!plan.dropped_tools.contains(&crate::input::Tool::Pen)); + assert!(plan.dropped_tools.contains(&Tool::LiveShape)); + assert!(!plan.dropped_tools.contains(&Tool::Pen)); } #[test] @@ -238,9 +199,9 @@ fn compact_top_strip_respects_budget_without_the_old_floor() { for budget in [376, 340, 320] { snapshot.top_viewport_max = Some(budget as f64); assert!( - top_size(&crate::ui_text::UiTextEngine::default(), &snapshot).0 <= budget, + top_size(&UiTextEngine::default(), &snapshot).0 <= budget, "planned width {} exceeds {budget}", - top_size(&crate::ui_text::UiTextEngine::default(), &snapshot).0 + top_size(&UiTextEngine::default(), &snapshot).0 ); } } @@ -250,37 +211,31 @@ fn reordered_overflow_items_keep_visual_order() { let mut state = create_test_input_state(); state.set_toolbar_use_icons(true); let mut items = state.toolbar_items().clone(); - items.set_hidden( - crate::config::toolbar_item_ids::TOP_UTILITY_SCREENSHOT, - false, - ); + items.set_hidden(toolbar_item_ids::TOP_UTILITY_SCREENSHOT, false); items.move_item_to_index( - crate::config::ToolbarItemOrderGroup::TopControls, - crate::config::toolbar_item_ids::TOP_UTILITY_HIGHLIGHT, + ToolbarItemOrderGroup::TopControls, + toolbar_item_ids::TOP_UTILITY_HIGHLIGHT, 0, ); items.move_item_to_index( - crate::config::ToolbarItemOrderGroup::TopControls, - crate::config::toolbar_item_ids::TOP_UTILITY_SCREENSHOT, + ToolbarItemOrderGroup::TopControls, + toolbar_item_ids::TOP_UTILITY_SCREENSHOT, 1, ); state.test_set_toolbar_items(items); let mut snapshot = snapshot_from_state(&state); snapshot.top_viewport_max = Some(560.0); - let plan = crate::backend::wayland::toolbar::view::top::plan_top_strip( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - ); + let plan = top::plan_top_strip(&UiTextEngine::default(), &snapshot); let highlight = plan .dropped_utilities .iter() - .position(|item| *item == crate::ui::toolbar::model::TopUtilityButton::Highlight) + .position(|item| *item == TopUtilityButton::Highlight) .expect("highlight dropped"); let screenshot = plan .dropped_utilities .iter() - .position(|item| *item == crate::ui::toolbar::model::TopUtilityButton::Screenshot) + .position(|item| *item == TopUtilityButton::Screenshot) .expect("screenshot dropped"); assert!(highlight < screenshot); } @@ -289,7 +244,7 @@ fn reordered_overflow_items_keep_visual_order() { fn shapes_popover_hosts_the_relocated_tool_options() { let mut state = create_test_input_state(); state.set_toolbar_use_icons(true); - state.set_tool_override(Some(crate::input::Tool::RegularPolygon)); + state.set_tool_override(Some(Tool::RegularPolygon)); state.test_set_toolbar_menu_state( TopMenuState::ShapePicker, state.toolbar_top_popover_scroll(), @@ -297,23 +252,15 @@ fn shapes_popover_hosts_the_relocated_tool_options() { let snapshot = snapshot_from_state(&state); assert!(snapshot.shape_picker_open); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); assert!(h > 58, "open popover grows the surface: {h}"); - let tree = crate::backend::wayland::toolbar::view::top::build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let tree = top::build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); // The grid renders inside popover chrome with a caret. let panel = tree .node_by_id(&"top.shapes.panel".into()) .expect("shapes popover panel"); - assert!(matches!( - panel.kind, - crate::backend::wayland::toolbar::view::WidgetKind::Popover { .. } - )); + assert!(matches!(panel.kind, WidgetKind::Popover { .. })); // The old mini-checkbox lane's controls live inside the popover now. let fill = tree @@ -344,14 +291,9 @@ fn shapes_popover_hosts_the_relocated_tool_options() { // is gone. The pill carries its own Fill toggle for shape tools. state.test_set_toolbar_menu_state(TopMenuState::Closed, state.toolbar_top_popover_scroll()); let snapshot = snapshot_from_state(&state); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); assert_eq!(h, 104); - let tree = crate::backend::wayland::toolbar::view::top::build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let tree = top::build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); assert!(tree.node_by_id(&"top.utility.fill".into()).is_none()); } @@ -361,24 +303,16 @@ fn highlight_ring_row_grows_the_bar_only_while_active() { state.set_toolbar_use_icons(true); let snapshot = snapshot_from_state(&state); // Band (58) plus the contextual style pill (6 + 40) — no ring lane yet. - assert_eq!( - top_size(&crate::ui_text::UiTextEngine::default(), &snapshot).1, - 104 - ); + assert_eq!(top_size(&UiTextEngine::default(), &snapshot).1, 104); state.set_highlight_tool(true); let snapshot = snapshot_from_state(&state); assert!(snapshot.highlight_tool_active); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); // The highlight tool has no style properties, so the pill yields and // only the ring lane grows the 58px band. assert!(h > 58, "ring row grows the bar: {h}"); - let tree = crate::backend::wayland::toolbar::view::top::build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let tree = top::build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); assert!( tree.node_by_id(&"top.island.style".into()).is_none(), "no style pill while the highlight tool is active" @@ -402,13 +336,8 @@ fn highlight_ring_and_top_popovers_use_separate_lanes() { state.toolbar_top_popover_scroll(), ); let snapshot = snapshot_from_state(&state); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let tree = crate::backend::wayland::toolbar::view::top::build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let tree = top::build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); let ring = tree .node_by_id(&"top.utility.highlight-ring".into()) .expect("ring row"); @@ -422,37 +351,26 @@ fn highlight_ring_and_top_popovers_use_separate_lanes() { state.toolbar_top_popover_scroll(), ); let mut items = state.toolbar_items().clone(); - items.set_hidden( - crate::config::toolbar_item_ids::TOP_UTILITY_SCREENSHOT, - false, - ); + items.set_hidden(toolbar_item_ids::TOP_UTILITY_SCREENSHOT, false); state.test_set_toolbar_items(items); let mut snapshot = snapshot_from_state(&state); snapshot.top_viewport_max = (480..=1120).rev().find_map(|budget| { snapshot.top_viewport_max = Some(budget as f64); - let plan = crate::backend::wayland::toolbar::view::top::plan_top_strip( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - ); + let plan = top::plan_top_strip(&UiTextEngine::default(), &snapshot); let has_dropped_items = !plan.dropped_tools.is_empty() || !plan.dropped_utilities.is_empty(); (has_dropped_items && !plan .dropped_utilities - .contains(&crate::ui::toolbar::model::TopUtilityButton::Highlight)) + .contains(&TopUtilityButton::Highlight)) .then_some(budget as f64) }); assert!( snapshot.top_viewport_max.is_some(), "overflow budget retaining highlight" ); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let tree = crate::backend::wayland::toolbar::view::top::build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let tree = top::build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); let ring = tree .node_by_id(&"top.utility.highlight-ring".into()) .expect("ring row"); @@ -474,12 +392,12 @@ fn top_size_scales_with_toolbar_scale() { state.set_toolbar_use_icons(true); state.test_set_toolbar_appearance(state.toolbar_use_icons(), 1.0); let snapshot = snapshot_from_state(&state); - let base_size = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let base_size = top_size(&UiTextEngine::default(), &snapshot); // Scale 1.5x should increase size proportionally state.test_set_toolbar_appearance(state.toolbar_use_icons(), 1.5); let snapshot = snapshot_from_state(&state); - let scaled_size = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let scaled_size = top_size(&UiTextEngine::default(), &snapshot); assert_eq!( scaled_size.0, (base_size.0 as f64 * 1.5).ceil() as u32, @@ -494,7 +412,7 @@ fn top_size_scales_with_toolbar_scale() { // Scale 0.75x should decrease size state.test_set_toolbar_appearance(state.toolbar_use_icons(), 0.75); let snapshot = snapshot_from_state(&state); - let small_size = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let small_size = top_size(&UiTextEngine::default(), &snapshot); assert!( small_size.0 < base_size.0, "Scaled down width should be smaller" @@ -511,18 +429,18 @@ fn scale_size_handles_non_finite_values() { state.set_toolbar_use_icons(true); state.test_set_toolbar_appearance(state.toolbar_use_icons(), 1.0); let snapshot = snapshot_from_state(&state); - let base_size = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let base_size = top_size(&UiTextEngine::default(), &snapshot); // NaN should fall back to 1.0 state.test_set_toolbar_appearance(state.toolbar_use_icons(), f64::NAN); let snapshot = snapshot_from_state(&state); - let nan_size = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let nan_size = top_size(&UiTextEngine::default(), &snapshot); assert_eq!(nan_size, base_size, "NaN scale should fall back to 1.0"); // Infinity should fall back to 1.0 state.test_set_toolbar_appearance(state.toolbar_use_icons(), f64::INFINITY); let snapshot = snapshot_from_state(&state); - let inf_size = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let inf_size = top_size(&UiTextEngine::default(), &snapshot); assert_eq!( inf_size, base_size, "Infinity scale should fall back to 1.0" @@ -531,7 +449,7 @@ fn scale_size_handles_non_finite_values() { // Negative infinity should fall back to 1.0 state.test_set_toolbar_appearance(state.toolbar_use_icons(), f64::NEG_INFINITY); let snapshot = snapshot_from_state(&state); - let neg_inf_size = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let neg_inf_size = top_size(&UiTextEngine::default(), &snapshot); assert_eq!( neg_inf_size, base_size, "Neg infinity scale should fall back to 1.0" @@ -546,21 +464,21 @@ fn scale_size_clamps_extreme_values() { // Test upper bound clamping (max 3.0) state.test_set_toolbar_appearance(state.toolbar_use_icons(), 10.0); let snapshot = snapshot_from_state(&state); - let huge_size = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let huge_size = top_size(&UiTextEngine::default(), &snapshot); state.test_set_toolbar_appearance(state.toolbar_use_icons(), 3.0); let snapshot = snapshot_from_state(&state); - let max_size = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let max_size = top_size(&UiTextEngine::default(), &snapshot); assert_eq!(huge_size, max_size, "Scale > 3.0 should clamp to 3.0"); // Test lower bound clamping (min 0.5) state.test_set_toolbar_appearance(state.toolbar_use_icons(), 0.1); let snapshot = snapshot_from_state(&state); - let tiny_size = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let tiny_size = top_size(&UiTextEngine::default(), &snapshot); state.test_set_toolbar_appearance(state.toolbar_use_icons(), 0.5); let snapshot = snapshot_from_state(&state); - let min_size = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let min_size = top_size(&UiTextEngine::default(), &snapshot); assert_eq!(tiny_size, min_size, "Scale < 0.5 should clamp to 0.5"); } @@ -569,18 +487,15 @@ fn a_narrowing_strip_moves_the_laser_to_the_overflow_before_any_other_tool() { let mut state = create_test_input_state(); state.set_toolbar_use_icons(true); let mut snapshot = snapshot_from_state(&state); - let full_width = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot).0; + let full_width = top_size(&UiTextEngine::default(), &snapshot).0; let first_dropped = (200..full_width).rev().find_map(|budget| { snapshot.top_viewport_max = Some(f64::from(budget)); - let plan = crate::backend::wayland::toolbar::view::top::plan_top_strip( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - ); + let plan = top::plan_top_strip(&UiTextEngine::default(), &snapshot); (!plan.dropped_tools.is_empty()).then_some(plan.dropped_tools) }); - assert_eq!(first_dropped, Some(vec![crate::input::Tool::Laser])); + assert_eq!(first_dropped, Some(vec![Tool::Laser])); } /// Advanced's inline shapes widen the strip; under width pressure they yield @@ -592,10 +507,10 @@ fn advanced_inline_shapes_yield_before_arrow_and_line() { let mut state = create_test_input_state(); state.set_toolbar_use_icons(true); let mut regular = snapshot_from_state(&state); - regular.layout_mode = crate::config::ToolbarLayoutMode::Regular; + regular.layout_mode = ToolbarLayoutMode::Regular; let mut advanced = regular.clone(); - advanced.layout_mode = crate::config::ToolbarLayoutMode::Advanced; - let engine = crate::ui_text::UiTextEngine::default(); + advanced.layout_mode = ToolbarLayoutMode::Advanced; + let engine = UiTextEngine::default(); let regular_width = top_size(&engine, ®ular).0; let advanced_width = top_size(&engine, &advanced).0; @@ -605,7 +520,7 @@ fn advanced_inline_shapes_yield_before_arrow_and_line() { let mut saw_arrow_drop = false; for budget in (500..advanced_width).step_by(20) { advanced.top_viewport_max = Some(budget as f64); - let plan = crate::backend::wayland::toolbar::view::top::plan_top_strip(&engine, &advanced); + let plan = top::plan_top_strip(&engine, &advanced); if plan.dropped_tools.contains(&Tool::Arrow) { saw_arrow_drop = true; for tool in advanced_shapes { diff --git a/src/backend/wayland/toolbar/main/lifecycle.rs b/src/backend/wayland/toolbar/main/lifecycle.rs index 285236b18..9d48aa694 100644 --- a/src/backend/wayland/toolbar/main/lifecycle.rs +++ b/src/backend/wayland/toolbar/main/lifecycle.rs @@ -1,10 +1,12 @@ +use smithay_client_toolkit::shell::wlr_layer::LayerSurface; + +use crate::backend::wayland::toolbar; use crate::ui_text::UiTextEngine; use log::info; -use smithay_client_toolkit::{ - compositor::CompositorState, - shell::wlr_layer::{LayerShell, LayerSurfaceConfigure}, -}; -use wayland_client::{QueueHandle, protocol::wl_output}; +use smithay_client_toolkit::compositor::CompositorState; +use smithay_client_toolkit::shell::wlr_layer::{LayerShell, LayerSurfaceConfigure}; +use wayland_client::QueueHandle; +use wayland_client::protocol::wl_output; use super::structs::ToolbarSurfaceManager; use crate::backend::wayland::state::WaylandState; @@ -22,7 +24,7 @@ impl ToolbarSurfaceManager { output: Option<&wl_output::WlOutput>, snapshot: &ToolbarSnapshot, ) { - let top_size = crate::backend::wayland::toolbar::top_size(engine, snapshot); + let top_size = toolbar::top_size(engine, snapshot); if self.is_top_visible() { if self.top.layer_surface.is_none() { @@ -48,7 +50,7 @@ impl ToolbarSurfaceManager { pub fn handle_configure( &mut self, configure: &LayerSurfaceConfigure, - layer: &smithay_client_toolkit::shell::wlr_layer::LayerSurface, + layer: &LayerSurface, ) -> bool { if self.top.is_layer(layer) { return self.top.handle_configure(configure); diff --git a/src/backend/wayland/toolbar/main/render.rs b/src/backend/wayland/toolbar/main/render.rs index f5964d6fc..c38ba1a49 100644 --- a/src/backend/wayland/toolbar/main/render.rs +++ b/src/backend/wayland/toolbar/main/render.rs @@ -1,3 +1,4 @@ +use crate::backend::wayland::toolbar; use crate::ui_text::UiTextEngine; use smithay_client_toolkit::shm::Shm; @@ -29,9 +30,7 @@ impl ToolbarSurfaceManager { top_hover_start, render_profile, |ctx, w, h, snap, hits, hov, hov_start| { - crate::backend::wayland::toolbar::render_top_strip( - engine, ctx, w, h, snap, hits, hov, hov_start, - ) + toolbar::render_top_strip(engine, ctx, w, h, snap, hits, hov, hov_start) }, ) { self.top.report_render_failure(&err); diff --git a/src/backend/wayland/toolbar/main/state.rs b/src/backend/wayland/toolbar/main/state.rs index 21b8006e4..c567074ec 100644 --- a/src/backend/wayland/toolbar/main/state.rs +++ b/src/backend/wayland/toolbar/main/state.rs @@ -1,4 +1,5 @@ -use smithay_client_toolkit::{compositor::CompositorState, shell::wlr_layer::LayerSurface}; +use smithay_client_toolkit::compositor::CompositorState; +use smithay_client_toolkit::shell::wlr_layer::LayerSurface; use wayland_client::protocol::wl_surface; use super::structs::ToolbarSurfaceManager; diff --git a/src/backend/wayland/toolbar/mod.rs b/src/backend/wayland/toolbar/mod.rs index c07a5dba1..3c0530e26 100644 --- a/src/backend/wayland/toolbar/mod.rs +++ b/src/backend/wayland/toolbar/mod.rs @@ -12,6 +12,8 @@ pub use layout::top_size; pub use main::*; pub use render::render_top_strip; +use crate::label_format; + pub fn format_binding_label(label: &str, binding: Option<&str>) -> String { - crate::label_format::format_binding_label(label, binding) + label_format::format_binding_label(label, binding) } diff --git a/src/backend/wayland/toolbar/render.rs b/src/backend/wayland/toolbar/render.rs index cd03bf00b..9ef333301 100644 --- a/src/backend/wayland/toolbar/render.rs +++ b/src/backend/wayland/toolbar/render.rs @@ -2,8 +2,9 @@ mod paint; mod top_strip; mod widgets; +use std::time::Duration; + pub use top_strip::render_top_strip; /// Delay before showing toolbar tooltips. -pub(in crate::backend::wayland) const TOOLTIP_DELAY: std::time::Duration = - std::time::Duration::from_millis(250); +pub(in crate::backend::wayland) const TOOLTIP_DELAY: Duration = Duration::from_millis(250); diff --git a/src/backend/wayland/toolbar/render/paint.rs b/src/backend/wayland/toolbar/render/paint.rs index ce6824c58..ebd2405e1 100644 --- a/src/backend/wayland/toolbar/render/paint.rs +++ b/src/backend/wayland/toolbar/render/paint.rs @@ -5,11 +5,16 @@ //! paint time against each node's drawn rect (matching the legacy renderer), //! so pointer motion never rebuilds a tree. +use std::f64::consts; + use crate::backend::wayland::toolbar::view::{ ButtonStyle, ShortcutBadgePlacement, WidgetKind, WidgetNode, WidgetTree, }; +use crate::toolbar_icons::MicroChipStyle; use crate::ui::theme::swatch::{chrome_rgb, swatch_edge_stroke}; +use crate::ui::theme::toolbar; use crate::ui_text::{UiTextEngine, UiTextStyle}; +use crate::{draw, toolbar_icons, ui}; use super::widgets::constants::{ COLOR_ACCENT, COLOR_BADGE_BACKGROUND, COLOR_BADGE_BORDER, COLOR_ICON_DEFAULT, COLOR_LABEL_HINT, @@ -105,11 +110,11 @@ fn paint_preset_color_swatch( let sy = y + h - size - PRESET_SLOT_SWATCH_INSET; let swatch_path = |ctx: &cairo::Context| draw_round_rect(ctx, sx, sy, size, size, PRESET_SLOT_SWATCH_RADIUS); - crate::ui::checkerboard_behind(ctx, color.3, swatch_path); + ui::checkerboard_behind(ctx, color.3, swatch_path); ctx.set_source_rgba(color.0, color.1, color.2, color.3); swatch_path(ctx); let _ = ctx.fill(); - let luminance = crate::draw::perceived_luminance(color.0, color.1, color.2); + let luminance = draw::perceived_luminance(color.0, color.1, color.2); set_color( ctx, if luminance < 0.3 { @@ -129,7 +134,7 @@ fn draw_slider_knob(ctx: &cairo::Context, rect: (f64, f64, f64, f64), t: f64) { let knob_r = (h / 2.0).min(7.0); let knob_x = x + knob_r + t.clamp(0.0, 1.0) * (w - knob_r * 2.0); set_color(ctx, COLOR_TRACK_KNOB); - ctx.arc(knob_x, y + h / 2.0, knob_r, 0.0, std::f64::consts::PI * 2.0); + ctx.arc(knob_x, y + h / 2.0, knob_r, 0.0, consts::PI * 2.0); let _ = ctx.fill(); } @@ -333,10 +338,10 @@ fn paint_node( draw_meter_bar(ctx, node.rect, *filled, is_hover, *enabled); } WidgetKind::MeterDot { active, enabled } => { - crate::toolbar_icons::draw_meter_dot(ctx, node.rect, *active, is_hover, *enabled); + toolbar_icons::draw_meter_dot(ctx, node.rect, *active, is_hover, *enabled); } WidgetKind::SmoothingPreview { level } => { - crate::toolbar_icons::draw_smoothing_preview(ctx, node.rect, *level); + toolbar_icons::draw_smoothing_preview(ctx, node.rect, *level); } WidgetKind::Slider { t, spec } => { // Track and knob: a rounded track with the accent knob riding the @@ -346,28 +351,23 @@ fn paint_node( set_color(ctx, COLOR_TRACK_BACKGROUND); draw_round_rect(ctx, x, track_y, w, track_h, track_h / 2.0); let _ = ctx.fill(); - crate::toolbar_icons::draw_slider_ticks(ctx, node.rect, spec.tick_positions()); + toolbar_icons::draw_slider_ticks(ctx, node.rect, spec.tick_positions()); draw_slider_knob(ctx, node.rect, *t); } WidgetKind::OpacitySlider { t, paint } => { - crate::toolbar_icons::draw_opacity_track(ctx, node.rect, paint.rgb, paint.alpha_stops); + toolbar_icons::draw_opacity_track(ctx, node.rect, paint.rgb, paint.alpha_stops); draw_slider_knob(ctx, node.rect, *t); } WidgetKind::ArrowStylePreview { style } => { - crate::toolbar_icons::draw_arrow_style_preview( + toolbar_icons::draw_arrow_style_preview( ctx, node.rect, *style, - crate::ui::theme::toolbar::COLOR_TEXT_PRIMARY, + toolbar::COLOR_TEXT_PRIMARY, ); } WidgetKind::OpacitySwatch { paint } => { - crate::toolbar_icons::draw_opacity_swatch( - ctx, - node.rect, - paint.rgb, - paint.stroke_alpha, - ); + toolbar_icons::draw_opacity_swatch(ctx, node.rect, paint.rgb, paint.stroke_alpha); } WidgetKind::Swatch { color, selected } => { // Rounded square inset one pixel so the accent selection ring @@ -377,7 +377,7 @@ fn paint_node( let swatch_path = |ctx: &cairo::Context| { draw_round_rect(ctx, x + 1.0, y + 1.0, w - 2.0, h - 2.0, 5.0) }; - crate::ui::checkerboard_behind(ctx, color.3, swatch_path); + ui::checkerboard_behind(ctx, color.3, swatch_path); ctx.set_source_rgba(color.0, color.1, color.2, color.3); swatch_path(ctx); let _ = ctx.fill(); @@ -477,13 +477,13 @@ fn paint_node( ring_color, ring_width, } => { - crate::toolbar_icons::draw_micro_chip( + toolbar_icons::draw_micro_chip( ctx, x, y, w.min(h), glyph.0, - &crate::toolbar_icons::MicroChipStyle { + &MicroChipStyle { ring_color: *ring_color, ring_width: *ring_width, icon_color: if is_hover { @@ -522,12 +522,12 @@ fn paint_node( } WidgetKind::VScrollbar { t, thumb } => { // A soft track with the theme's proportional slider thumb. - set_color(ctx, crate::ui::theme::toolbar::COLOR_SCROLLBAR_TRACK); + set_color(ctx, toolbar::COLOR_SCROLLBAR_TRACK); draw_round_rect(ctx, x, y, w, h, w / 2.0); let _ = ctx.fill(); let thumb_h = (h * thumb.clamp(0.0, 1.0)).max(w * 2.0).min(h); let thumb_y = y + (h - thumb_h) * t.clamp(0.0, 1.0); - set_color(ctx, crate::ui::theme::toolbar::COLOR_SCROLLBAR_SLIDER); + set_color(ctx, toolbar::COLOR_SCROLLBAR_SLIDER); draw_round_rect(ctx, x, thumb_y, w, thumb_h, w / 2.0); let _ = ctx.fill(); } @@ -537,6 +537,11 @@ fn paint_node( #[cfg(test)] mod tests { + use crate::backend::wayland::toolbar::view::node::{IconFn, Interaction}; + use crate::domain::color; + use crate::ui::toolbar::ToolbarEvent; + use crate::ui::toolbar::model::{SemanticToolIcon, TopToolbarIcon}; + use super::*; use crate::backend::wayland::toolbar::view::node::LabelSpec; use cairo::{Context, Format, ImageSurface}; @@ -677,7 +682,7 @@ mod tests { #[test] fn a_dark_swatch_gets_a_light_ring_against_the_dark_bar() { - let black = crate::domain::color::PALETTE_BLACK; + let black = color::PALETTE_BLACK; let (r, g, b) = swatch_edge_on_panel((black.r, black.g, black.b, black.a)); let edge = (u32::from(r) + u32::from(g) + u32::from(b)) / 3; assert!( @@ -686,7 +691,7 @@ mod tests { ); // A swatch that already stands out keeps the quiet hairline. - let red = crate::domain::color::PALETTE_RED; + let red = color::PALETTE_RED; let (r, g, b) = swatch_edge_on_panel((red.r, red.g, red.b, red.a)); assert!( r > 200 && g < 120 && b < 120, @@ -703,13 +708,9 @@ mod tests { ctx.set_source_rgb(0.0, 0.0, 0.0); let _ = ctx.paint(); let glyph = filled.then(|| { - crate::backend::wayland::toolbar::view::node::IconFn( - crate::toolbar_icons::top_toolbar_icon_painter( - crate::ui::toolbar::model::TopToolbarIcon::Tool( - crate::ui::toolbar::model::SemanticToolIcon::Pen, - ), - ), - ) + IconFn(toolbar_icons::top_toolbar_icon_painter( + TopToolbarIcon::Tool(SemanticToolIcon::Pen), + )) }); let node = WidgetNode::new( "test.preset", @@ -720,12 +721,7 @@ mod tests { label: label.to_string(), active: false, }, - Some( - crate::backend::wayland::toolbar::view::node::Interaction::click( - crate::ui::toolbar::ToolbarEvent::SavePreset(1), - None, - ), - ), + Some(Interaction::click(ToolbarEvent::SavePreset(1), None)), ); paint_node(&UiTextEngine::default(), &ctx, &node, hover); } diff --git a/src/backend/wayland/toolbar/render/top_strip/tests.rs b/src/backend/wayland/toolbar/render/top_strip/tests.rs index 01bb1e68e..6f039891d 100644 --- a/src/backend/wayland/toolbar/render/top_strip/tests.rs +++ b/src/backend/wayland/toolbar/render/top_strip/tests.rs @@ -1,3 +1,9 @@ +use std::time::Duration; + +use crate::backend::wayland::toolbar; +use crate::input::state::test_support; +use crate::ui::toolbar::snapshot::fade; + use super::*; use crate::backend::wayland::toolbar::view::WidgetTree; use crate::backend::wayland::toolbar::view::node::{ @@ -91,14 +97,14 @@ fn retained_widget_text_matches_fresh_targets() { #[test] fn retained_top_strip_preserves_fade_hits_and_delayed_tooltips_across_targets() { let engine = UiTextEngine::default(); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); input.set_toolbar_use_icons(false); let mut snapshot = ToolbarSnapshot::from_input_with_bindings(&input, ToolbarBindingHints::default()); snapshot.settings_popover_open = true; for fade in [1.0, 0.45] { snapshot.top_fade = fade; - let (width, height) = crate::backend::wayland::toolbar::top_size(&engine, &snapshot); + let (width, height) = toolbar::top_size(&engine, &snapshot); let tree = view::top::build_top_view(&engine, &snapshot, width as f64, height as f64); let planned_hits = tree.to_hit_regions(); let hovered = planned_hits @@ -111,9 +117,7 @@ fn retained_top_strip_preserves_fade_hits_and_delayed_tooltips_across_targets() )); for density in [1, 2, 1] { let mut hits = Vec::new(); - let start = Some( - Instant::now() - super::super::TOOLTIP_DELAY - std::time::Duration::from_secs(1), - ); + let start = Some(Instant::now() - super::super::TOOLTIP_DELAY - Duration::from_secs(1)); let actual = pixels(density, |ctx| { render_top_strip( &engine, @@ -175,7 +179,7 @@ fn retained_top_strip_preserves_fade_hits_and_delayed_tooltips_across_targets() width as f64, height as f64, false, - Some(Instant::now() + std::time::Duration::from_secs(60)), + Some(Instant::now() + Duration::from_secs(60)), ) }); assert!(hidden.iter().all(|byte| *byte == 0)); @@ -186,10 +190,10 @@ fn retained_top_strip_preserves_fade_hits_and_delayed_tooltips_across_targets() #[test] fn idle_hidden_strip_paints_nothing_but_keeps_its_hits() { let engine = UiTextEngine::default(); - let input = crate::input::state::test_support::make_test_input_state(); + let input = test_support::make_test_input_state(); let mut snapshot = ToolbarSnapshot::from_input_with_bindings(&input, ToolbarBindingHints::default()); - let (width, height) = crate::backend::wayland::toolbar::top_size(&engine, &snapshot); + let (width, height) = toolbar::top_size(&engine, &snapshot); let render = |snapshot: &ToolbarSnapshot, hits: &mut Vec| { pixels(1, |ctx| { render_top_strip( @@ -210,7 +214,7 @@ fn idle_hidden_strip_paints_nothing_but_keeps_its_hits() { let shown = render(&snapshot, &mut shown_hits); assert!(shown.iter().any(|byte| *byte != 0)); - snapshot.top_fade = crate::ui::toolbar::snapshot::fade::TOP_STRIP_HIDDEN_LEVEL; + snapshot.top_fade = fade::TOP_STRIP_HIDDEN_LEVEL; let mut hidden_hits = Vec::new(); let hidden = render(&snapshot, &mut hidden_hits); assert!(hidden.iter().all(|byte| *byte == 0), "no ghosted strip"); diff --git a/src/backend/wayland/toolbar/render/widgets/buttons.rs b/src/backend/wayland/toolbar/render/widgets/buttons.rs index 93ce0fb94..c53a57a50 100644 --- a/src/backend/wayland/toolbar/render/widgets/buttons.rs +++ b/src/backend/wayland/toolbar/render/widgets/buttons.rs @@ -1,3 +1,5 @@ +use crate::toolbar_icons; + use super::constants::{ COLOR_ACCENT_BRIGHT, COLOR_ACCENT_GLOW, COLOR_BUTTON_ACTIVE, COLOR_BUTTON_DEFAULT, COLOR_BUTTON_DESTRUCTIVE_HOVER, COLOR_BUTTON_DISABLED, COLOR_BUTTON_HOVER, COLOR_CLOSE_DEFAULT, @@ -57,7 +59,7 @@ pub(in crate::backend::wayland::toolbar::render) fn draw_drag_handle( }, ); let icon_size = w.min(h); - crate::toolbar_icons::draw_icon_drag( + toolbar_icons::draw_icon_drag( ctx, x + (w - icon_size) / 2.0, y + (h - icon_size) / 2.0, @@ -108,14 +110,7 @@ pub(in crate::backend::wayland::toolbar::render) fn draw_minimize_button( size: f64, hover: bool, ) { - draw_collapse_button( - ctx, - x, - y, - size, - hover, - crate::toolbar_icons::draw_icon_minimize, - ); + draw_collapse_button(ctx, x, y, size, hover, toolbar_icons::draw_icon_minimize); } fn draw_collapse_button( @@ -187,9 +182,9 @@ pub(in crate::backend::wayland::toolbar::render) fn draw_pin_button( let icon_x = x + (size - icon_size) / 2.0; let icon_y = y + (size - icon_size) / 2.0; if pinned { - crate::toolbar_icons::draw_icon_pin(ctx, icon_x, icon_y, icon_size); + toolbar_icons::draw_icon_pin(ctx, icon_x, icon_y, icon_size); } else { - crate::toolbar_icons::draw_icon_unpin(ctx, icon_x, icon_y, icon_size); + toolbar_icons::draw_icon_unpin(ctx, icon_x, icon_y, icon_size); } } diff --git a/src/backend/wayland/toolbar/surfaces/lifecycle.rs b/src/backend/wayland/toolbar/surfaces/lifecycle.rs index 1b351b1de..00aa6b658 100644 --- a/src/backend/wayland/toolbar/surfaces/lifecycle.rs +++ b/src/backend/wayland/toolbar/surfaces/lifecycle.rs @@ -1,12 +1,11 @@ use log::{debug, info}; -use smithay_client_toolkit::{ - compositor::CompositorState, - shell::{ - WaylandSurface, - wlr_layer::{KeyboardInteractivity, Layer, LayerShell, LayerSurfaceConfigure}, - }, +use smithay_client_toolkit::compositor::CompositorState; +use smithay_client_toolkit::shell::WaylandSurface; +use smithay_client_toolkit::shell::wlr_layer::{ + KeyboardInteractivity, Layer, LayerShell, LayerSurfaceConfigure, }; -use wayland_client::{QueueHandle, protocol::wl_output}; +use wayland_client::QueueHandle; +use wayland_client::protocol::wl_output; use super::structs::ToolbarSurface; use crate::backend::wayland::state::WaylandState; diff --git a/src/backend/wayland/toolbar/surfaces/render.rs b/src/backend/wayland/toolbar/surfaces/render.rs index 8935c724a..041bb3cad 100644 --- a/src/backend/wayland/toolbar/surfaces/render.rs +++ b/src/backend/wayland/toolbar/surfaces/render.rs @@ -2,10 +2,12 @@ use std::time::Instant; use anyhow::{Result, anyhow}; use log::debug; -use smithay_client_toolkit::{ - shell::WaylandSurface, - shm::{Shm, slot::SlotPool}, -}; +use smithay_client_toolkit::shell::WaylandSurface; +use smithay_client_toolkit::shm::Shm; +use smithay_client_toolkit::shm::slot::SlotPool; +use wayland_client::protocol::wl_shm::Format; + +use crate::util::Rect; use super::structs::ToolbarSurface; use crate::backend::wayland::toolbar::hit::HitRegion; @@ -71,7 +73,7 @@ impl ToolbarSurface { phys_w as i32, phys_h as i32, (phys_w * 4) as i32, - wayland_client::protocol::wl_shm::Format::Argb8888, + Format::Argb8888, ) .map_err(|err| anyhow!("failed to create a {phys_w}x{phys_h} buffer: {err}"))?; @@ -136,7 +138,7 @@ impl ToolbarSurface { drop(ctx); drop(surface); if let Some(profile) = render_profile - && let Some(full) = crate::util::Rect::new(0, 0, phys_w as i32, phys_h as i32) + && let Some(full) = Rect::new(0, 0, phys_w as i32, phys_h as i32) { profile.remap_argb8888_regions( canvas, diff --git a/src/backend/wayland/toolbar/surfaces/state.rs b/src/backend/wayland/toolbar/surfaces/state.rs index 33b4a9bca..8c6d5206b 100644 --- a/src/backend/wayland/toolbar/surfaces/state.rs +++ b/src/backend/wayland/toolbar/surfaces/state.rs @@ -1,5 +1,10 @@ +use smithay_client_toolkit::compositor::Region; + +use crate::backend::wayland::toolbar::view::top; +use crate::ui::toolbar::ToolbarSnapshot; use crate::ui_text::UiTextEngine; -use smithay_client_toolkit::{compositor::CompositorState, shell::WaylandSurface}; +use smithay_client_toolkit::compositor::CompositorState; +use smithay_client_toolkit::shell::WaylandSurface; use wayland_client::protocol::wl_output; use super::structs::ToolbarSurface; @@ -113,11 +118,7 @@ impl ToolbarSurface { /// Restrict the top surface's input region to the bar band plus any /// open popover panels, in surface coordinates; full-surface otherwise. - pub fn sync_top_input_region( - &mut self, - engine: &UiTextEngine, - snapshot: &crate::ui::toolbar::ToolbarSnapshot, - ) { + pub fn sync_top_input_region(&mut self, engine: &UiTextEngine, snapshot: &ToolbarSnapshot) { if self.width == 0 || self.height == 0 { return; } @@ -126,7 +127,7 @@ impl ToolbarSurface { } else { 1.0 }; - let rects = crate::backend::wayland::toolbar::view::top::top_input_rects( + let rects = top::top_input_rects( engine, snapshot, self.width as f64 / ui_scale, @@ -163,7 +164,7 @@ impl ToolbarSurface { match self.input_rects.as_deref() { None => surface.set_input_region(None), Some(rects) => { - let Ok(region) = smithay_client_toolkit::compositor::Region::new(compositor) else { + let Ok(region) = Region::new(compositor) else { return; }; for &(x, y, w, h) in rects { diff --git a/src/backend/wayland/toolbar/surfaces/structs.rs b/src/backend/wayland/toolbar/surfaces/structs.rs index 1286c0ad8..1e93bfd86 100644 --- a/src/backend/wayland/toolbar/surfaces/structs.rs +++ b/src/backend/wayland/toolbar/surfaces/structs.rs @@ -1,13 +1,11 @@ -use smithay_client_toolkit::{ - shell::{ - WaylandSurface, - wlr_layer::{Anchor, LayerSurface}, - }, - shm::slot::SlotPool, -}; -use wayland_client::{Proxy, protocol::wl_surface}; +use smithay_client_toolkit::shell::WaylandSurface; +use smithay_client_toolkit::shell::wlr_layer::{Anchor, LayerSurface}; +use smithay_client_toolkit::shm::slot::SlotPool; +use wayland_client::Proxy; +use wayland_client::protocol::wl_surface; use crate::backend::wayland::toolbar::events::ToolbarCursorHint; +use crate::backend::wayland::toolbar::hit; use crate::backend::wayland::toolbar::hit::HitRegion; #[derive(Debug)] @@ -88,11 +86,10 @@ impl ToolbarSurface { /// Get cursor hint for the current hover position. pub fn cursor_hint(&self) -> Option { let (hx, hy) = self.hover?; - let hint = - crate::backend::wayland::toolbar::hit::resolve_hit_index(&self.hit_regions, hx, hy) - .map_or(ToolbarCursorHint::Default, |index| { - self.hit_regions[index].kind.cursor_hint() - }); + let hint = hit::resolve_hit_index(&self.hit_regions, hx, hy) + .map_or(ToolbarCursorHint::Default, |index| { + self.hit_regions[index].kind.cursor_hint() + }); Some(hint) } } diff --git a/src/backend/wayland/toolbar/view/node.rs b/src/backend/wayland/toolbar/view/node.rs index 74028dfce..1f9223e47 100644 --- a/src/backend/wayland/toolbar/view/node.rs +++ b/src/backend/wayland/toolbar/view/node.rs @@ -1,10 +1,12 @@ //! Typed widget nodes: what exists, where it is, and what it does. use std::borrow::Cow; -use std::fmt; +use std::{fmt, ptr}; use crate::backend::wayland::toolbar::events::HitKind; +use crate::draw::ArrowStyle; use crate::ui::toolbar::ToolbarEvent; +use crate::ui::toolbar::model::{OpacityPaint, ToolbarSliderSpec}; /// Stable identity of a widget across rebuilds of the tree. /// @@ -53,7 +55,7 @@ pub struct IconFn(pub fn(&cairo::Context, f64, f64, f64)); impl PartialEq for IconFn { fn eq(&self, other: &Self) -> bool { - std::ptr::fn_addr_eq(self.0, other.0) + ptr::fn_addr_eq(self.0, other.0) } } @@ -183,21 +185,13 @@ pub enum WidgetKind { /// `t` in `[0, 1]`. The drag mapping lives on the node's interaction /// (`HitKind::DragSet*`); the paint only shows the current value, plus /// the spec's reference ticks. - Slider { - t: f64, - spec: crate::ui::toolbar::model::ToolbarSliderSpec, - }, + Slider { t: f64, spec: ToolbarSliderSpec }, /// The marker opacity slider: a [`WidgetKind::Slider`] whose track fades /// from clear to solid in the stroke color (`paint.alpha_stops`). - OpacitySlider { - t: f64, - paint: crate::ui::toolbar::model::OpacityPaint, - }, + OpacitySlider { t: f64, paint: OpacityPaint }, /// The marker opacity slider's readout: a stroke at `paint.stroke_alpha` /// over sample text, drawn by the painter both frontends share. - OpacitySwatch { - paint: crate::ui::toolbar::model::OpacityPaint, - }, + OpacitySwatch { paint: OpacityPaint }, /// One bar of a level meter. The node's rect is the bar's hit slot; the /// painter draws a slimmer rounded bar centered in it, accent-filled at /// or below the current level and track-colored above it. @@ -209,7 +203,7 @@ pub enum WidgetKind { SmoothingPreview { level: u8 }, /// An arrow style drawn left to right in the foreground tone, by the /// painter both frontends share (`toolbar_icons::draw_arrow_style_preview`). - ArrowStylePreview { style: crate::draw::ArrowStyle }, + ArrowStylePreview { style: ArrowStyle }, /// Color swatch tile. Swatch { color: (f64, f64, f64, f64), diff --git a/src/backend/wayland/toolbar/view/top.rs b/src/backend/wayland/toolbar/view/top.rs index ade4a209a..61d4e5d34 100644 --- a/src/backend/wayland/toolbar/view/top.rs +++ b/src/backend/wayland/toolbar/view/top.rs @@ -16,7 +16,9 @@ use crate::ui_text::UiTextEngine; use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; +use crate::config::ToolbarLayoutMode; use crate::input::Tool; +use crate::ui::theme::toolbar; use crate::ui::toolbar::{ToolbarSnapshot, model}; use super::tree::WidgetTree; @@ -34,7 +36,7 @@ const TOP_LABEL_FONT_SIZE: f64 = 14.0; /// Caption text size of the tool meters and steppers ("Smooth"). The GTK /// `.meter-caption`/`.stepper-caption` labels read the same token /// (`font_tooltip`), so both toolbars draw the word at one size. -const CAPTION_FONT_SIZE: f64 = crate::ui::theme::toolbar::FONT_SIZE_TOOLTIP; +const CAPTION_FONT_SIZE: f64 = toolbar::FONT_SIZE_TOOLTIP; const MINI_LABEL_FONT_SIZE: f64 = 10.0; // FONT_SIZE_SMALL /// Extra advance consumed by a group divider (the 1px line plus breathing @@ -53,7 +55,7 @@ const TOP_COMPACT_MARGIN_RIGHT: f64 = 8.0; /// The pad shares its number with the GTK `.pill.compact` padding via /// `theme::toolbar::COMPACT_ISLAND_PAD`. const TOP_COMPACT_ISLAND_GAP: f64 = 4.0; -const TOP_COMPACT_ISLAND_PAD: f64 = crate::ui::theme::toolbar::COMPACT_ISLAND_PAD; +const TOP_COMPACT_ISLAND_PAD: f64 = toolbar::COMPACT_ISLAND_PAD; pub(crate) use model::TopStripPlan; @@ -85,7 +87,7 @@ pub fn plan_top_strip(engine: &UiTextEngine, snapshot: &ToolbarSnapshot) -> TopS } let visible_utilities = model::visible_top_utility_buttons( snapshot, - snapshot.layout_mode == crate::config::ToolbarLayoutMode::Simple, + snapshot.layout_mode == ToolbarLayoutMode::Simple, snapshot.use_icons, ); let visible_tools: Vec<_> = diff --git a/src/backend/wayland/toolbar/view/top/arrow_menu/tests.rs b/src/backend/wayland/toolbar/view/top/arrow_menu/tests.rs index f9cc1c59e..188a2e40f 100644 --- a/src/backend/wayland/toolbar/view/top/arrow_menu/tests.rs +++ b/src/backend/wayland/toolbar/view/top/arrow_menu/tests.rs @@ -3,6 +3,7 @@ use crate::backend::wayland::toolbar::view::{WidgetKind, WidgetTree}; use crate::draw::ArrowStyle; use crate::input::Tool; use crate::input::state::test_support::make_test_input_state; +use crate::ui::theme::toolbar; use crate::ui::toolbar::{ToolbarBindingHints, ToolbarEvent, ToolbarSnapshot, model}; use crate::ui_text::{UiTextEngine, UiTextStyle}; @@ -81,7 +82,7 @@ fn the_chip_draws_and_names_the_style_and_opens_the_menu() { fn every_chip_label_fits_its_slot() { let engine = UiTextEngine::default(); let style = UiTextStyle { - family: crate::ui::theme::toolbar::FONT_FAMILY_DEFAULT, + family: toolbar::FONT_FAMILY_DEFAULT, slant: cairo::FontSlant::Normal, weight: cairo::FontWeight::Bold, size: 14.0, diff --git a/src/backend/wayland/toolbar/view/top/layout_menu.rs b/src/backend/wayland/toolbar/view/top/layout_menu.rs index 4ef4db93f..74e62d5f5 100644 --- a/src/backend/wayland/toolbar/view/top/layout_menu.rs +++ b/src/backend/wayland/toolbar/view/top/layout_menu.rs @@ -7,6 +7,7 @@ //! `model::layout_menu_entries`, so both frontends list the same presets //! with the same wording. +use crate::config::ToolbarLayoutMode; use crate::i18n::trf; use crate::ui::toolbar::{ToolbarSnapshot, model}; @@ -25,7 +26,7 @@ const BOTTOM_MARGIN: f64 = 4.0; const SURFACE_MARGIN: f64 = 4.0; fn panel_size() -> (f64, f64) { - let rows = model::layout_menu_entries(crate::config::ToolbarLayoutMode::Regular).len() as f64; + let rows = model::layout_menu_entries(ToolbarLayoutMode::Regular).len() as f64; let width = model::LAYOUT_MENU_ROW_W + model::LAYOUT_MENU_PAD * 2.0; let height = model::LAYOUT_MENU_PAD * 2.0 + rows * model::LAYOUT_MENU_ROW_H diff --git a/src/backend/wayland/toolbar/view/top/menus.rs b/src/backend/wayland/toolbar/view/top/menus.rs index 9d0114c89..3cc162094 100644 --- a/src/backend/wayland/toolbar/view/top/menus.rs +++ b/src/backend/wayland/toolbar/view/top/menus.rs @@ -14,6 +14,7 @@ use crate::ui_text::UiTextEngine; use crate::backend::wayland::toolbar::events::HitKind; use crate::backend::wayland::toolbar::format_binding_label; use crate::backend::wayland::toolbar::rows::{grid_layout, row_item_width}; +use crate::ui::theme::toolbar; use crate::ui::toolbar::session_format::{ strip_session_extension, truncate_middle, truncate_start, }; @@ -26,11 +27,11 @@ use super::super::tree::WidgetTree; /// Content width inside the popover padding. Shared with the GTK popover /// viewport via the theme token so the frontends cannot drift. -pub(super) const MENU_CONTENT_W: f64 = crate::ui::theme::toolbar::MENU_CONTENT_W; +pub(super) const MENU_CONTENT_W: f64 = toolbar::MENU_CONTENT_W; /// Content column for the Canvas popover. It matches the text-menu width so /// the Step Undo/Redo cluster has comfortable spacing, and is shared with the /// GTK canvas viewport via the theme token so the frontends cannot drift. -pub(super) const CANVAS_MENU_CONTENT_W: f64 = crate::ui::theme::toolbar::CANVAS_MENU_CONTENT_W; +pub(super) const CANVAS_MENU_CONTENT_W: f64 = toolbar::CANVAS_MENU_CONTENT_W; const MENU_PAD: f64 = 10.0; const MENU_GAP: f64 = 5.0; const MENU_BUTTON_H: f64 = 24.0; @@ -45,7 +46,7 @@ const MENU_PATH_H: f64 = 12.0; const MENU_CONFIRM_LABEL_H: f64 = 18.0; /// Cap on the visible content height; taller content scrolls internally. /// Shared with the GTK `ScrolledWindow` cap via the theme token. -pub(super) const MENU_MAX_CONTENT_H: f64 = crate::ui::theme::toolbar::MENU_MAX_CONTENT_H; +pub(super) const MENU_MAX_CONTENT_H: f64 = toolbar::MENU_MAX_CONTENT_H; const MENU_SCROLLBAR_W: f64 = 6.0; pub(super) const MENU_LABEL_FONT: f64 = 12.0; pub(super) const MENU_META_FONT: f64 = 11.0; @@ -712,7 +713,7 @@ fn append_settings_notices( fn settings_notice_height(engine: &UiTextEngine, text: &str, bold: bool) -> f64 { let style = UiTextStyle { - family: crate::ui::theme::toolbar::FONT_FAMILY_DEFAULT, + family: toolbar::FONT_FAMILY_DEFAULT, slant: cairo::FontSlant::Normal, weight: if bold { cairo::FontWeight::Bold diff --git a/src/backend/wayland/toolbar/view/top/meter.rs b/src/backend/wayland/toolbar/view/top/meter.rs index e2d09e4f6..d4f5d5a71 100644 --- a/src/backend/wayland/toolbar/view/top/meter.rs +++ b/src/backend/wayland/toolbar/view/top/meter.rs @@ -2,6 +2,8 @@ //! level, laid out abutting in a fixed-width row. The Pen feel panel lays the //! same bars out across its wider content column. +use std::iter; + use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; use crate::ui::toolbar::{ToolbarSnapshot, model}; @@ -73,17 +75,21 @@ pub(super) fn meter_bar_nodes( enabled.then(|| Interaction::click(zero.event, Some(zero.tooltip))), ); - std::iter::once(dot).chain(meter.segments.into_iter().enumerate().map( - move |(index, segment)| { - WidgetNode::new( - segment.id, - (x + dot_w + index as f64 * bar_w, y, bar_w, h), - WidgetKind::MeterBar { - filled: segment.filled, - enabled, - }, - enabled.then(|| Interaction::click(segment.event, Some(segment.tooltip))), - ) - }, - )) + iter::once(dot).chain( + meter + .segments + .into_iter() + .enumerate() + .map(move |(index, segment)| { + WidgetNode::new( + segment.id, + (x + dot_w + index as f64 * bar_w, y, bar_w, h), + WidgetKind::MeterBar { + filled: segment.filled, + enabled, + }, + enabled.then(|| Interaction::click(segment.event, Some(segment.tooltip))), + ) + }), + ) } diff --git a/src/backend/wayland/toolbar/view/top/pen_feel/tests.rs b/src/backend/wayland/toolbar/view/top/pen_feel/tests.rs index 947011f96..b797bd7cb 100644 --- a/src/backend/wayland/toolbar/view/top/pen_feel/tests.rs +++ b/src/backend/wayland/toolbar/view/top/pen_feel/tests.rs @@ -3,6 +3,7 @@ use crate::backend::wayland::toolbar::view::{WidgetKind, WidgetTree}; use crate::config::ToolbarStrokeControls; use crate::input::Tool; use crate::input::state::test_support::make_test_input_state; +use crate::ui::theme::toolbar; use crate::ui::toolbar::{ToolbarBindingHints, ToolbarEvent, ToolbarSnapshot, model}; use crate::ui_text::{UiTextEngine, UiTextStyle}; @@ -38,7 +39,7 @@ fn feel_ids(tree: &WidgetTree) -> Vec { fn text_style(size: f64, bold: bool) -> UiTextStyle<'static> { UiTextStyle { - family: crate::ui::theme::toolbar::FONT_FAMILY_DEFAULT, + family: toolbar::FONT_FAMILY_DEFAULT, slant: cairo::FontSlant::Normal, weight: if bold { cairo::FontWeight::Bold @@ -142,7 +143,7 @@ fn the_open_panel_shows_the_sections_the_tool_uses() { let shape_pen = snapshot_for(Tool::LiveShape, ToolbarStrokeControls::Panel, true); let tree = tree_for(&shape_pen); - let mut both = smoothing_only.clone(); + let mut both = smoothing_only; both.extend( [ "top.feel.detection.label", @@ -346,8 +347,8 @@ fn panel_text_fits_its_column() { .expect("text measures") .width() }; - let caption = crate::ui::theme::toolbar::FONT_SIZE_TOOLTIP; - let hint = crate::ui::theme::toolbar::FONT_SIZE_SMALL; + let caption = toolbar::FONT_SIZE_TOOLTIP; + let hint = toolbar::FONT_SIZE_SMALL; for (setting, levels) in [ (model::StrokeSetting::Smoothing, 0..=6), diff --git a/src/backend/wayland/toolbar/view/top/tests.rs b/src/backend/wayland/toolbar/view/top/tests.rs index a5a328a93..8af78f740 100644 --- a/src/backend/wayland/toolbar/view/top/tests.rs +++ b/src/backend/wayland/toolbar/view/top/tests.rs @@ -1,8 +1,30 @@ +use crate::backend::wayland::toolbar::events::HitKind; +use crate::backend::wayland::toolbar::hit; +use crate::config::{ + QuickColorPalette, ToolbarItemOrderConfig, ToolbarItemsConfig, ToolbarStrokeControls, + TopDisplayMode, +}; +use crate::draw::{Color, SpotlightMagnifierSource}; +use crate::input::{SelectionPropertyEntry, SelectionPropertyKind, Tool}; +use crate::toolbar_icons; +use crate::ui::toolbar::model::{ + ToolbarControlKind, ToolbarSessionModel, ToolbarSettingsModel, TopToolbarIcon, +}; +use crate::ui::toolbar::snapshot::fade; +use crate::ui::toolbar::{ + PrecisionEntryTarget, PresetSlotSnapshot, RuntimeUiPersistenceMode, + RuntimeUiPersistenceSnapshot, SessionRecentSnapshot, ToolbarItemCustomizeGroup, session_format, +}; + +use std::path::{Path, PathBuf}; +use std::ptr; + use super::*; use crate::backend::wayland::toolbar::layout::{ToolbarLayoutSpec, top_size}; use crate::backend::wayland::toolbar::view::{ShortcutBadgePlacement, WidgetKind, WidgetTree}; use crate::config::{Action, action_label, toolbar_item_ids as ids}; -use crate::input::state::{TopMenuState, test_support::make_test_input_state}; +use crate::input::state::TopMenuState; +use crate::input::state::test_support::make_test_input_state; use crate::ui::toolbar::{ToolbarBindingHints, ToolbarEvent, ToolbarSnapshot}; fn snapshot() -> ToolbarSnapshot { @@ -11,13 +33,8 @@ fn snapshot() -> ToolbarSnapshot { } fn build(snapshot: &ToolbarSnapshot) -> WidgetTree { - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), snapshot); - build_top_view( - &crate::ui_text::UiTextEngine::default(), - snapshot, - w as f64, - h as f64, - ) + let (w, h) = top_size(&UiTextEngine::default(), snapshot); + build_top_view(&UiTextEngine::default(), snapshot, w as f64, h as f64) } fn node_id_list(tree: &WidgetTree) -> Vec<&str> { @@ -157,10 +174,10 @@ fn presets_render_as_slot_buttons_in_the_presets_island() { let mut snapshot = snapshot(); snapshot.presets = vec![None; 5]; - snapshot.presets[0] = Some(crate::ui::toolbar::PresetSlotSnapshot { + snapshot.presets[0] = Some(PresetSlotSnapshot { name: Some("Red pen".to_string()), tool: Tool::Pen, - color: crate::draw::Color::new(1.0, 0.0, 0.0, 1.0), + color: Color::new(1.0, 0.0, 0.0, 1.0), size: 4.0, eraser_kind: None, eraser_mode: None, @@ -241,20 +258,15 @@ fn presets_render_as_slot_buttons_in_the_presets_island() { )); // The presets island joins the surface input region as its own rect. - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let rects = top_input_rects( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ) - .expect("input rects"); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let rects = top_input_rects(&UiTextEngine::default(), &snapshot, w as f64, h as f64) + .expect("input rects"); assert!(rects.contains(&island.rect)); } #[test] fn idle_hidden_strip_keeps_its_size_but_passes_input_through() { - let engine = crate::ui_text::UiTextEngine::default(); + let engine = UiTextEngine::default(); let mut snapshot = snapshot(); let shown_size = top_size(&engine, &snapshot); let (w, h) = (shown_size.0 as f64, shown_size.1 as f64); @@ -266,7 +278,7 @@ fn idle_hidden_strip_keeps_its_size_but_passes_input_through() { assert_eq!(top_input_rects(&engine, &snapshot, w, h), Some(shown_rects)); // Fully hidden: same surface size (no configure churn), empty region. - snapshot.top_fade = crate::ui::toolbar::snapshot::fade::TOP_STRIP_HIDDEN_LEVEL; + snapshot.top_fade = fade::TOP_STRIP_HIDDEN_LEVEL; assert_eq!(top_size(&engine, &snapshot), shown_size); assert_eq!(top_input_rects(&engine, &snapshot, w, h), Some(Vec::new())); } @@ -447,7 +459,7 @@ fn shape_picker_shows_fill_while_line_is_active() { ); let mut snapshot = ToolbarSnapshot::from_input_with_bindings(&state, ToolbarBindingHints::default()); - snapshot.active_tool = crate::input::Tool::Line; + snapshot.active_tool = Tool::Line; snapshot.tool_override = None; let tree = build(&snapshot); @@ -458,10 +470,10 @@ fn shape_picker_shows_fill_while_line_is_active() { "Fill configures the next shape and must not depend on the active tool" ); - snapshot.resolved_toolbar_items = crate::config::ToolbarItemsConfig { + snapshot.resolved_toolbar_items = ToolbarItemsConfig { hidden: vec![ids::TOP_UTILITY_FILL.as_str().to_string()], shown: Vec::new(), - order: crate::config::ToolbarItemOrderConfig::default(), + order: ToolbarItemOrderConfig::default(), } .resolved(); assert!( @@ -478,14 +490,9 @@ fn input_rects_cover_islands_and_open_popovers_only() { state.test_set_toolbar_menu_state(TopMenuState::Closed, state.toolbar_top_popover_scroll()); let snapshot = ToolbarSnapshot::from_input_with_bindings(&state, ToolbarBindingHints::default()); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let rects = top_input_rects( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ) - .expect("no popover: the islands still restrict input"); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let rects = top_input_rects(&UiTextEngine::default(), &snapshot, w as f64, h as f64) + .expect("no popover: the islands still restrict input"); assert_eq!( rects.len(), 5, @@ -498,12 +505,7 @@ fn input_rects_cover_islands_and_open_popovers_only() { assert!(rects[2].0 + rects[2].2 < rects[3].0); // The style pill is the fifth rect, detached below the band. assert!(rects[4].1 > rects[0].1 + rects[0].3); - let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let tree = build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); let islands: Vec<_> = tree .nodes() .iter() @@ -520,14 +522,9 @@ fn input_rects_cover_islands_and_open_popovers_only() { ); let snapshot = ToolbarSnapshot::from_input_with_bindings(&state, ToolbarBindingHints::default()); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let rects = top_input_rects( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ) - .expect("partial input region"); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let rects = top_input_rects(&UiTextEngine::default(), &snapshot, w as f64, h as f64) + .expect("partial input region"); assert_eq!(rects.len(), 6, "five islands + shapes panel: {rects:?}"); assert_eq!(rects[0].0, 0.0); assert_eq!(rects[0].1, 0.0); @@ -542,12 +539,7 @@ fn input_rects_cover_islands_and_open_popovers_only() { assert!(rects[0].0 + rects[0].2 < rects[1].0); assert!(rects[1].0 + rects[1].2 < rects[2].0); assert!(rects[2].0 + rects[2].2 < rects[3].0); - let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let tree = build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); let panel = tree .node_by_id(&"top.shapes.panel".into()) .expect("panel node"); @@ -568,20 +560,10 @@ fn island_backgrounds_stop_at_bar_band_when_popover_is_open() { ); let snapshot = ToolbarSnapshot::from_input_with_bindings(&state, ToolbarBindingHints::default()); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); - let input_rects = top_input_rects( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ) - .expect("partial input region"); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let tree = build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); + let input_rects = top_input_rects(&UiTextEngine::default(), &snapshot, w as f64, h as f64) + .expect("partial input region"); let islands: Vec<_> = tree .nodes() @@ -605,15 +587,10 @@ fn minimized_strip_is_a_single_restore_tab() { // A comfortable target (the old 64x24 sliver was easy to miss) that // carries the restore glyph and its caption. - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); assert_eq!((w, h), (104, 32)); - let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let tree = build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); let interactive: Vec<_> = tree .nodes() .iter() @@ -641,21 +618,16 @@ fn minimized_strip_is_a_single_restore_tab() { #[test] fn micro_strip_is_a_single_round_chip() { let mut snapshot = snapshot(); - snapshot.top_display_mode = crate::config::TopDisplayMode::Micro; + snapshot.top_display_mode = TopDisplayMode::Micro; - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); assert_eq!( (w, h), ToolbarLayoutSpec::TOP_MICRO_SIZE, "micro mode is one 44px chip" ); - let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let tree = build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); let interactive: Vec<_> = tree .nodes() .iter() @@ -665,7 +637,7 @@ fn micro_strip_is_a_single_round_chip() { assert_eq!(interactive[0].id.as_str(), "top.chrome.micro"); assert!(matches!( interactive[0].interact.as_ref().unwrap().event, - ToolbarEvent::SetTopDisplayMode(crate::config::TopDisplayMode::Full) + ToolbarEvent::SetTopDisplayMode(TopDisplayMode::Full) )); match &interactive[0].kind { WidgetKind::MicroChip { @@ -673,12 +645,11 @@ fn micro_strip_is_a_single_round_chip() { ring_color, ring_width, } => { - let expected_icon = crate::ui::toolbar::model::TopToolbarIcon::Tool( - crate::ui::toolbar::model::semantic_icon_for_tool(snapshot.active_tool), - ); - assert!(std::ptr::fn_addr_eq( + let expected_icon = + TopToolbarIcon::Tool(model::semantic_icon_for_tool(snapshot.active_tool)); + assert!(ptr::fn_addr_eq( glyph.0, - crate::toolbar_icons::top_toolbar_icon_painter(expected_icon) + toolbar_icons::top_toolbar_icon_painter(expected_icon) )); assert_eq!( *ring_color, @@ -690,34 +661,20 @@ fn micro_strip_is_a_single_round_chip() { ), "ring strokes in the current color" ); - assert_eq!( - *ring_width, - crate::ui::toolbar::model::micro_ring_width(snapshot.thickness) - ); + assert_eq!(*ring_width, model::micro_ring_width(snapshot.thickness)); } other => panic!("micro chip kind, got {other:?}"), } // No popovers, ring rows, or partial input regions in micro mode: the // whole 44px surface takes input. - assert!( - top_input_rects( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64 - ) - .is_none() - ); - assert_eq!( - top_extra_height(&crate::ui_text::UiTextEngine::default(), &snapshot), - 0.0 - ); + assert!(top_input_rects(&UiTextEngine::default(), &snapshot, w as f64, h as f64).is_none()); + assert_eq!(top_extra_height(&UiTextEngine::default(), &snapshot), 0.0); // Minimized wins if both states are somehow set. snapshot.top_minimized = true; assert_eq!( - top_size(&crate::ui_text::UiTextEngine::default(), &snapshot), + top_size(&UiTextEngine::default(), &snapshot), ToolbarLayoutSpec::TOP_MINIMIZED_SIZE ); } @@ -728,7 +685,7 @@ fn compact_shape_picker_preserves_its_full_semantic_icon_size() { let mut plan = TopStripPlan::unconstrained(); plan.compact = true; let tree = super::build::build_top_view_planned( - &crate::ui_text::UiTextEngine::default(), + &UiTextEngine::default(), &snapshot, &plan, 800.0, @@ -758,7 +715,7 @@ fn overflow_utility_tooltips_remain_bare_action_labels() { plan.dropped_utilities = vec![model::TopUtilityButton::Text]; let tree = super::build::build_top_view_planned( - &crate::ui_text::UiTextEngine::default(), + &UiTextEngine::default(), &snapshot, &plan, 800.0, @@ -778,12 +735,12 @@ fn overflow_utility_tooltips_remain_bare_action_labels() { /// Snapshot with one tool active and the settings overrides pinned off, so /// each case exercises exactly one pure style-pill morph state (the /// overrides themselves are covered by the spec's unit tests). -fn snapshot_for_tool(tool: crate::input::Tool) -> ToolbarSnapshot { +fn snapshot_for_tool(tool: Tool) -> ToolbarSnapshot { let mut snapshot = snapshot(); snapshot.active_tool = tool; snapshot.tool_override = None; - snapshot.thickness_targets_eraser = tool == crate::input::Tool::Eraser; - snapshot.thickness_targets_marker = tool == crate::input::Tool::Marker; + snapshot.thickness_targets_eraser = tool == Tool::Eraser; + snapshot.thickness_targets_marker = tool == Tool::Marker; snapshot.show_text_controls = false; snapshot.show_marker_opacity_section = false; snapshot @@ -801,7 +758,7 @@ fn style_ids(tree: &WidgetTree) -> Vec { fn style_pill_sliders_reuse_the_shared_drag_hit_kinds() { use crate::backend::wayland::toolbar::events::HitKind; - let snapshot = snapshot_for_tool(crate::input::Tool::Pen); + let snapshot = snapshot_for_tool(Tool::Pen); let tree = build(&snapshot); let chip = tree @@ -835,7 +792,7 @@ fn style_pill_sliders_reuse_the_shared_drag_hit_kinds() { swatch.interact.as_ref().unwrap().event, ToolbarEvent::SetQuickColor { color, action, index } if color == entry.color - && action == crate::config::QuickColorPalette::action_for_index(0) + && action == QuickColorPalette::action_for_index(0) && index == 0 )); @@ -878,17 +835,16 @@ fn style_pill_sliders_reuse_the_shared_drag_hit_kinds() { assert_eq!( numeral.interact.as_ref().map(|interact| &interact.event), Some(&ToolbarEvent::OpenPrecisionEntry( - crate::ui::toolbar::PrecisionEntryTarget::Thickness + PrecisionEntryTarget::Thickness )) ); } #[test] fn spotlight_style_pill_shows_missing_source_status_inline() { - let mut snapshot = snapshot_for_tool(crate::input::Tool::Spotlight); + let mut snapshot = snapshot_for_tool(Tool::Spotlight); snapshot.spotlight_magnification = 2.25; - snapshot.spotlight_magnifier_source = - Some(crate::draw::SpotlightMagnifierSource::IncompleteTransparent); + snapshot.spotlight_magnifier_source = Some(SpotlightMagnifierSource::IncompleteTransparent); let tree = build(&snapshot); let status = tree @@ -899,8 +855,7 @@ fn spotlight_style_pill_shows_missing_source_status_inline() { WidgetKind::Label(label) if label.text == "Freeze screen to preview" )); - snapshot.spotlight_magnifier_source = - Some(crate::draw::SpotlightMagnifierSource::CompleteSolid); + snapshot.spotlight_magnifier_source = Some(SpotlightMagnifierSource::CompleteSolid); assert!( build(&snapshot) .node_by_id(&"top.style.spotlight-magnification.status".into()) @@ -1020,9 +975,7 @@ fn assert_eraser_style_pill() { .as_ref() .map(|interact| &interact.event) { - Some(ToolbarEvent::OpenPrecisionEntry( - crate::ui::toolbar::PrecisionEntryTarget::Thickness, - )) => {} + Some(ToolbarEvent::OpenPrecisionEntry(PrecisionEntryTarget::Thickness)) => {} other => panic!("numeral opens the precise entry, got {other:?}"), } } @@ -1141,22 +1094,12 @@ fn style_pill_geometry_holds_per_tool_and_select_hides_the_pill() { // Select without a selection: the pill yields entirely — no pill node, // no fourth input rect, no extra height for it. let select = snapshot_for_tool(Tool::Select); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &select); - let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &select, - w as f64, - h as f64, - ); + let (w, h) = top_size(&UiTextEngine::default(), &select); + let tree = build_top_view(&UiTextEngine::default(), &select, w as f64, h as f64); assert!(tree.node_by_id(&"top.island.style".into()).is_none()); assert!(style_ids(&tree).is_empty()); - let rects = top_input_rects( - &crate::ui_text::UiTextEngine::default(), - &select, - w as f64, - h as f64, - ) - .expect("island input rects"); + let rects = top_input_rects(&UiTextEngine::default(), &select, w as f64, h as f64) + .expect("island input rects"); assert_eq!( rects.len(), 4, @@ -1167,30 +1110,25 @@ fn style_pill_geometry_holds_per_tool_and_select_hides_the_pill() { // its fourth input rect) back, with the same geometry contract. let mut selection = snapshot_for_tool(Tool::Select); selection.selection_properties = vec![ - crate::input::SelectionPropertyEntry { + SelectionPropertyEntry { label: "Color".to_string(), value: "Red".to_string(), - kind: crate::input::SelectionPropertyKind::Color, - state: crate::input::SelectionPropertyKind::Color.unknown_value(), + kind: SelectionPropertyKind::Color, + state: SelectionPropertyKind::Color.unknown_value(), disabled: false, }, - crate::input::SelectionPropertyEntry { + SelectionPropertyEntry { label: "Thickness".to_string(), value: "3.0px".to_string(), - kind: crate::input::SelectionPropertyKind::Thickness, - state: crate::input::SelectionPropertyKind::Thickness.unknown_value(), + kind: SelectionPropertyKind::Thickness, + state: SelectionPropertyKind::Thickness.unknown_value(), disabled: false, }, ]; selection.selection_has_text = true; selection.selected_text_bold = Some(false); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &selection); - let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &selection, - w as f64, - h as f64, - ); + let (w, h) = top_size(&UiTextEngine::default(), &selection); + let tree = build_top_view(&UiTextEngine::default(), &selection, w as f64, h as f64); let style = tree .node_by_id(&"top.island.style".into()) .expect("selection pill card"); @@ -1209,13 +1147,8 @@ fn style_pill_geometry_holds_per_tool_and_select_hides_the_pill() { "top.style.font-bold", ] ); - let rects = top_input_rects( - &crate::ui_text::UiTextEngine::default(), - &selection, - w as f64, - h as f64, - ) - .expect("island input rects"); + let rects = top_input_rects(&UiTextEngine::default(), &selection, w as f64, h as f64) + .expect("island input rects"); assert_eq!(rects.len(), 5, "selection pill rect: {rects:?}"); let pill_rect = rects[4]; assert_eq!( @@ -1233,13 +1166,8 @@ fn style_pill_geometry_holds_per_tool_and_select_hides_the_pill() { Tool::StepMarker, ] { let snapshot = snapshot_for_tool(tool); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let tree = build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); let tools = tree .node_by_id(&"top.island.tools".into()) .expect("tools island"); @@ -1280,13 +1208,8 @@ fn style_pill_geometry_holds_per_tool_and_select_hides_the_pill() { // The pill joins the surface input region as the fifth rect (after // the tools, presets, history, and chrome band islands). - let rects = top_input_rects( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ) - .expect("island input rects"); + let rects = top_input_rects(&UiTextEngine::default(), &snapshot, w as f64, h as f64) + .expect("island input rects"); assert_eq!(rects.len(), 5, "{tool:?} rects: {rects:?}"); assert_eq!(rects[4], style.rect, "{tool:?} pill input rect"); } @@ -1294,8 +1217,8 @@ fn style_pill_geometry_holds_per_tool_and_select_hides_the_pill() { #[test] fn a_press_on_a_meter_bar_activates_that_bar_not_its_neighbour() { - let mut snapshot = snapshot_for_tool(crate::input::Tool::Pen); - snapshot.stroke_controls = crate::config::ToolbarStrokeControls::Meter; + let mut snapshot = snapshot_for_tool(Tool::Pen); + snapshot.stroke_controls = ToolbarStrokeControls::Meter; snapshot.pen_smoothing = 3; let tree = build(&snapshot); let hits = tree.to_hit_regions(); @@ -1309,25 +1232,23 @@ fn a_press_on_a_meter_bar_activates_that_bar_not_its_neighbour() { first.rect.0 + first.rect.2 / 2.0, first.rect.1 + first.rect.3 / 2.0, ); - let pressed = crate::backend::wayland::toolbar::hit::find_hit(&hits, x, y, |hit| { - crate::backend::wayland::toolbar::hit::intent_for_hit(hit, x, y) - }) - .map(|(intent, _)| intent.into_event()); + let pressed = hit::find_hit(&hits, x, y, |hit| hit::intent_for_hit(hit, x, y)) + .map(|(intent, _)| intent.into_event()); assert_eq!(pressed, Some(ToolbarEvent::SetPenSmoothing(1))); } #[test] fn shape_pen_meters_carry_captions_and_level_bars_inside_the_planned_width() { - let engine = crate::ui_text::UiTextEngine::default(); - let mut snapshot = snapshot_for_tool(crate::input::Tool::LiveShape); - snapshot.stroke_controls = crate::config::ToolbarStrokeControls::Meter; + let engine = UiTextEngine::default(); + let mut snapshot = snapshot_for_tool(Tool::LiveShape); + snapshot.stroke_controls = ToolbarStrokeControls::Meter; let tree = build(&snapshot); let caption_style = crate::ui_text::UiTextStyle { - family: crate::ui::theme::toolbar::FONT_FAMILY_DEFAULT, + family: toolbar::FONT_FAMILY_DEFAULT, slant: cairo::FontSlant::Normal, weight: cairo::FontWeight::Normal, - size: crate::ui::theme::toolbar::FONT_SIZE_TOOLTIP, + size: toolbar::FONT_SIZE_TOOLTIP, }; for (meter_id, text, level, bars) in [ @@ -1411,15 +1332,15 @@ fn shape_pen_meters_carry_captions_and_level_bars_inside_the_planned_width() { #[test] fn shape_pen_steppers_carry_visible_captions_inside_the_planned_width() { - let engine = crate::ui_text::UiTextEngine::default(); - let mut snapshot = snapshot_for_tool(crate::input::Tool::LiveShape); - snapshot.stroke_controls = crate::config::ToolbarStrokeControls::Stepper; + let engine = UiTextEngine::default(); + let mut snapshot = snapshot_for_tool(Tool::LiveShape); + snapshot.stroke_controls = ToolbarStrokeControls::Stepper; let tree = build(&snapshot); let caption_style = crate::ui_text::UiTextStyle { - family: crate::ui::theme::toolbar::FONT_FAMILY_DEFAULT, + family: toolbar::FONT_FAMILY_DEFAULT, slant: cairo::FontSlant::Normal, weight: cairo::FontWeight::Normal, - size: crate::ui::theme::toolbar::FONT_SIZE_TOOLTIP, + size: toolbar::FONT_SIZE_TOOLTIP, }; for (stepper, text) in [ @@ -1507,7 +1428,7 @@ fn overflow_menu_always_carries_the_canvas_session_and_settings_entries() { #[test] fn overflow_popover_anchors_directly_below_its_button_like_gtk() { - let mut snapshot = snapshot_for_tool(crate::input::Tool::Marker); + let mut snapshot = snapshot_for_tool(Tool::Marker); // Classic mode pins every control, so the pill reaches under the panel. snapshot.context_aware_ui = false; snapshot.show_text_controls = true; @@ -1552,9 +1473,9 @@ fn runtime_sized_menu_popovers_do_not_reenter_strip_planning() { _ => unreachable!(), } - let (width, height) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let (width, height) = top_size(&UiTextEngine::default(), &snapshot); let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), + &UiTextEngine::default(), &snapshot, width as f64, height as f64, @@ -1574,7 +1495,7 @@ fn menu_popovers_anchor_directly_below_overflow_button_like_gtk() { ("session", (false, true, false)), ("settings", (false, false, true)), ] { - let mut snapshot = snapshot_for_tool(crate::input::Tool::Marker); + let mut snapshot = snapshot_for_tool(Tool::Marker); // Classic mode pins every control, so the pill reaches under the panel. snapshot.context_aware_ui = false; snapshot.show_text_controls = true; @@ -1824,7 +1745,7 @@ fn canvas_popover_step_section_carries_toggles_steppers_and_delay_sliders() { )); // The step rows and global sliders vanish when their toggles are off. - let mut collapsed = snapshot.clone(); + let mut collapsed = snapshot; collapsed.custom_section_enabled = false; collapsed.show_delay_sliders = false; let collapsed_tree = build(&collapsed); @@ -1873,7 +1794,7 @@ fn canvas_popover_scrolls_when_all_sections_exceed_the_viewport() { snapshot.show_delay_sliders = true; snapshot.delay_actions_enabled = true; - let bounds = top_popover_scroll_bounds(&crate::ui_text::UiTextEngine::default(), &snapshot) + let bounds = top_popover_scroll_bounds(&UiTextEngine::default(), &snapshot) .expect("canvas scroll bounds"); assert!( bounds.0 > bounds.1, @@ -1888,17 +1809,12 @@ fn canvas_popover_scrolls_when_all_sections_exceed_the_viewport() { ); // The Canvas popover joins the input region as an extra rect. - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); let panel = tree .node_by_id(&"top.menu.canvas.panel".into()) .expect("canvas panel"); - let rects = top_input_rects( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ) - .expect("input rects"); + let rects = top_input_rects(&UiTextEngine::default(), &snapshot, w as f64, h as f64) + .expect("input rects"); assert!( rects .iter() @@ -1918,19 +1834,14 @@ fn session_popover_re_hosts_the_session_pane_content() { snapshot.active_session_name = Some("lecture.wayscriber-session".to_string()); snapshot.active_session_path = Some(PathBuf::from("/tmp/lecture.wayscriber-session")); snapshot.recent_sessions = (0..2) - .map(|index| crate::ui::toolbar::SessionRecentSnapshot { + .map(|index| SessionRecentSnapshot { display_name: format!("recent-{index}.wayscriber-session"), path: PathBuf::from(format!("/tmp/recent-{index}.wayscriber-session")), }) .collect(); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let tree = build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); let panel = tree .node_by_id(&"top.menu.session.panel".into()) .expect("session popover panel"); @@ -1957,8 +1868,7 @@ fn session_popover_re_hosts_the_session_pane_content() { fn assert_session_popover_model(tree: &WidgetTree, snapshot: &ToolbarSnapshot) { // Content parity with the Session pane: the same model drives both, so // every model control appears exactly once with the same event. - let model = crate::ui::toolbar::model::ToolbarSessionModel::for_popover(snapshot) - .expect("session model"); + let model = ToolbarSessionModel::for_popover(snapshot).expect("session model"); assert_eq!(model.buttons.len(), 5); for button in &model.buttons { let node = tree @@ -1996,10 +1906,7 @@ fn assert_session_popover_model(tree: &WidgetTree, snapshot: &ToolbarSnapshot) { .expect("home row"); match &node.kind { WidgetKind::TextButton { label, style } => { - assert_eq!( - label.text, - crate::ui::toolbar::session_format::truncate_middle(&home.label, 30) - ); + assert_eq!(label.text, session_format::truncate_middle(&home.label, 30)); assert_eq!(style.disabled, !home.enabled); } other => panic!("home row kind, got {other:?}"), @@ -2044,13 +1951,8 @@ fn assert_session_popover_input_rect( .node_by_id(&"top.menu.session.panel".into()) .expect("session popover panel"); // The popover joins the input region as an extra rect below the band. - let rects = top_input_rects( - &crate::ui_text::UiTextEngine::default(), - snapshot, - w as f64, - h as f64, - ) - .expect("input rects"); + let rects = top_input_rects(&UiTextEngine::default(), snapshot, w as f64, h as f64) + .expect("input rects"); assert!( rects .iter() @@ -2069,7 +1971,7 @@ fn assert_session_overwrite_confirmation(snapshot: &ToolbarSnapshot) { assert!(matches!( &replace.interact.as_ref().unwrap().event, ToolbarEvent::SaveSessionAsConfirm(path) - if path.as_path() == std::path::Path::new("/tmp/existing.wayscriber-session") + if path.as_path() == Path::new("/tmp/existing.wayscriber-session") )); let cancel = tree .node_by_id(&"top.menu.session.confirm-cancel".into()) @@ -2092,12 +1994,11 @@ fn settings_popover_re_hosts_the_settings_pane_content() { .expect("settings popover panel"); assert!(matches!(panel.kind, WidgetKind::Popover { .. })); - let model = crate::ui::toolbar::model::ToolbarSettingsModel::for_popover(&snapshot) - .expect("settings model"); + let model = ToolbarSettingsModel::for_popover(&snapshot).expect("settings model"); // Layout-mode segments render as three exclusive tabs. - let control = crate::ui::toolbar::model::layout_mode_control(snapshot.layout_mode); - let crate::ui::toolbar::model::ToolbarControlKind::Segmented(segmented) = &control.kind else { + let control = model::layout_mode_control(snapshot.layout_mode); + let ToolbarControlKind::Segmented(segmented) = &control.kind else { panic!("layout mode control is segmented"); }; for (index, segment) in segmented.segments().iter().enumerate() { @@ -2150,11 +2051,10 @@ fn settings_popover_re_hosts_the_settings_pane_content() { fn settings_popover_tucks_the_runtime_path_behind_details() { let mut snapshot = snapshot(); snapshot.settings_popover_open = true; - let runtime_path = - std::path::PathBuf::from("/home/user/.local/share/wayscriber/runtime-ui.toml"); - snapshot.runtime_ui_persistence = Some(crate::ui::toolbar::RuntimeUiPersistenceSnapshot { + let runtime_path = PathBuf::from("/home/user/.local/share/wayscriber/runtime-ui.toml"); + snapshot.runtime_ui_persistence = Some(RuntimeUiPersistenceSnapshot { path: runtime_path.clone(), - mode: crate::ui::toolbar::RuntimeUiPersistenceMode::Supported, + mode: RuntimeUiPersistenceMode::Supported, detail: None, recovery_artifacts: Vec::new(), }); @@ -2187,8 +2087,7 @@ fn settings_popover_tucks_the_runtime_path_behind_details() { .map(|interaction| &interaction.event), Some(&ToolbarEvent::SetSettingsDetailsOpen(true)) ); - let model = crate::ui::toolbar::model::ToolbarSettingsModel::for_popover(&snapshot) - .expect("settings model"); + let model = ToolbarSettingsModel::for_popover(&snapshot).expect("settings model"); assert!( model .buttons() @@ -2218,11 +2117,10 @@ fn settings_persistence_notices_wrap_as_full_width_logical_rows() { let mut snapshot = snapshot(); snapshot.settings_popover_open = true; snapshot.settings_details_open = true; - let runtime_path = - std::path::PathBuf::from("/home/user/.local/share/wayscriber/runtime-ui.toml"); - snapshot.runtime_ui_persistence = Some(crate::ui::toolbar::RuntimeUiPersistenceSnapshot { + let runtime_path = PathBuf::from("/home/user/.local/share/wayscriber/runtime-ui.toml"); + snapshot.runtime_ui_persistence = Some(RuntimeUiPersistenceSnapshot { path: runtime_path.clone(), - mode: crate::ui::toolbar::RuntimeUiPersistenceMode::Supported, + mode: RuntimeUiPersistenceMode::Supported, detail: None, recovery_artifacts: Vec::new(), }); @@ -2258,11 +2156,10 @@ fn settings_popover_customization_rows_keep_reorder_and_drag_events() { let mut snapshot = snapshot(); snapshot.settings_popover_open = true; snapshot.customize_items_open = true; - snapshot.customize_items_group = Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopTools); + snapshot.customize_items_group = Some(ToolbarItemCustomizeGroup::TopTools); let tree = build(&snapshot); - let model = crate::ui::toolbar::model::ToolbarSettingsModel::for_popover(&snapshot) - .expect("settings model"); + let model = ToolbarSettingsModel::for_popover(&snapshot).expect("settings model"); let overrides = model.item_overrides(); assert!(!overrides.is_empty()); @@ -2284,7 +2181,7 @@ fn settings_popover_customization_rows_keep_reorder_and_drag_events() { .expect("row drag region"); assert!(matches!( drag.interact.as_ref().unwrap().kind, - crate::backend::wayland::toolbar::events::HitKind::DragToolbarItem { id, .. } + HitKind::DragToolbarItem { id, .. } if id == item.id )); let up = tree @@ -2327,24 +2224,17 @@ fn tall_popover_content_caps_the_panel_and_scrolls_internally() { let mut snapshot = snapshot(); snapshot.settings_popover_open = true; snapshot.customize_items_open = true; - snapshot.customize_items_group = - Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopControls); + snapshot.customize_items_group = Some(ToolbarItemCustomizeGroup::TopControls); // An explicitly short output exercises internal scrolling independently // of how much content fits under the normal-screen fallback cap. snapshot.top_available_height = Some(360.0); - let (natural, viewport) = - top_popover_scroll_bounds(&crate::ui_text::UiTextEngine::default(), &snapshot) - .expect("settings scroll bounds"); + let (natural, viewport) = top_popover_scroll_bounds(&UiTextEngine::default(), &snapshot) + .expect("settings scroll bounds"); assert!(natural > viewport, "short output must cap tall content"); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let tree = build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); let panel = tree .node_by_id(&"top.menu.settings.panel".into()) .expect("settings popover panel"); @@ -2358,9 +2248,7 @@ fn tall_popover_content_caps_the_panel_and_scrolls_internally() { .node_by_id(&"top.menu.settings.scrollbar".into()) .expect("internal scrollbar"); let interaction = scrollbar.interact.as_ref().expect("scroll drag"); - let crate::backend::wayland::toolbar::events::HitKind::DragScrollTopPopover { max_scroll } = - interaction.kind - else { + let HitKind::DragScrollTopPopover { max_scroll } = interaction.kind else { panic!("scrollbar kind, got {:?}", interaction.kind); }; assert!(max_scroll > 0.0); @@ -2388,12 +2276,7 @@ fn tall_popover_content_caps_the_panel_and_scrolls_internally() { let unscrolled_first = tree.node_by_id(&"top.menu.settings.button.0".into()); assert!(unscrolled_first.is_some(), "top row visible before scroll"); snapshot.top_popover_scroll = max_scroll + 500.0; // clamps to max - let scrolled = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let scrolled = build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); let scrolled_panel = scrolled .node_by_id(&"top.menu.settings.panel".into()) .expect("panel"); @@ -2422,34 +2305,26 @@ fn tall_content_fills_a_tall_screen_without_scrolling() { let mut snapshot = snapshot(); snapshot.settings_popover_open = true; snapshot.customize_items_open = true; - snapshot.customize_items_group = - Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopControls); + snapshot.customize_items_group = Some(ToolbarItemCustomizeGroup::TopControls); // Without a known output height, the fixed fallback cap still scrolls. - let (natural, capped) = - top_popover_scroll_bounds(&crate::ui_text::UiTextEngine::default(), &snapshot) - .expect("bounds while the settings popover is open"); + let (natural, capped) = top_popover_scroll_bounds(&UiTextEngine::default(), &snapshot) + .expect("bounds while the settings popover is open"); assert_eq!(capped, super::menus::MENU_MAX_CONTENT_H); assert!(natural > capped, "content overflows the fallback cap"); // A tall screen: the popover grows to the full natural height, no scroll. snapshot.top_available_height = Some(natural + 600.0); - let (natural2, viewport) = - top_popover_scroll_bounds(&crate::ui_text::UiTextEngine::default(), &snapshot) - .expect("bounds with a known tall output"); + let (natural2, viewport) = top_popover_scroll_bounds(&UiTextEngine::default(), &snapshot) + .expect("bounds with a known tall output"); assert_eq!(natural2, natural, "content is unchanged"); assert!( (viewport - natural).abs() < 1e-9, "viewport should equal natural height on a tall screen: {viewport} vs {natural}" ); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let tree = build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); assert!( tree.node_by_id(&"top.menu.settings.scrollbar".into()) .is_none(), @@ -2473,26 +2348,19 @@ fn tall_content_scrolls_on_a_short_screen() { let mut snapshot = snapshot(); snapshot.settings_popover_open = true; snapshot.customize_items_open = true; - snapshot.customize_items_group = - Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopControls); + snapshot.customize_items_group = Some(ToolbarItemCustomizeGroup::TopControls); // A short output forces the screen-aware cap below the natural height. snapshot.top_available_height = Some(360.0); - let (natural, viewport) = - top_popover_scroll_bounds(&crate::ui_text::UiTextEngine::default(), &snapshot) - .expect("bounds with a known short output"); + let (natural, viewport) = top_popover_scroll_bounds(&UiTextEngine::default(), &snapshot) + .expect("bounds with a known short output"); assert!( viewport < natural, "content must still scroll when the screen is too short" ); - let (w, h) = top_size(&crate::ui_text::UiTextEngine::default(), &snapshot); - let tree = build_top_view( - &crate::ui_text::UiTextEngine::default(), - &snapshot, - w as f64, - h as f64, - ); + let (w, h) = top_size(&UiTextEngine::default(), &snapshot); + let tree = build_top_view(&UiTextEngine::default(), &snapshot, w as f64, h as f64); let scrollbar = tree .node_by_id(&"top.menu.settings.scrollbar".into()) .expect("internal scrollbar on a short screen"); @@ -2519,14 +2387,10 @@ fn popover_height_budget_is_relative_to_the_toolbar_surface_origin() { let mut snapshot = snapshot(); snapshot.settings_popover_open = true; snapshot.customize_items_open = true; - snapshot.customize_items_group = - Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopControls); + snapshot.customize_items_group = Some(ToolbarItemCustomizeGroup::TopControls); snapshot.top_available_height = Some(360.0); - assert_eq!( - top_size(&crate::ui_text::UiTextEngine::default(), &snapshot).1, - 360 - ); + assert_eq!(top_size(&UiTextEngine::default(), &snapshot).1, 360); } /// The wheel path scrolls the open popover against the same bounds the @@ -2535,18 +2399,16 @@ fn popover_height_budget_is_relative_to_the_toolbar_surface_origin() { fn top_popover_scroll_bounds_serve_the_wheel_path() { let mut snapshot = snapshot(); assert!( - top_popover_scroll_bounds(&crate::ui_text::UiTextEngine::default(), &snapshot).is_none(), + top_popover_scroll_bounds(&UiTextEngine::default(), &snapshot).is_none(), "no bounds while no menu popover is open" ); snapshot.settings_popover_open = true; snapshot.customize_items_open = true; - snapshot.customize_items_group = - Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopControls); + snapshot.customize_items_group = Some(ToolbarItemCustomizeGroup::TopControls); snapshot.top_available_height = Some(360.0); - let (natural, viewport) = - top_popover_scroll_bounds(&crate::ui_text::UiTextEngine::default(), &snapshot) - .expect("bounds while the settings popover is open"); + let (natural, viewport) = top_popover_scroll_bounds(&UiTextEngine::default(), &snapshot) + .expect("bounds while the settings popover is open"); assert!( natural > viewport, "the Top-controls list overflows a short-screen viewport" @@ -2558,7 +2420,7 @@ fn top_popover_scroll_bounds_serve_the_wheel_path() { let scrollbar = tree .node_by_id(&"top.menu.settings.scrollbar".into()) .expect("internal scrollbar"); - let crate::backend::wayland::toolbar::events::HitKind::DragScrollTopPopover { max_scroll } = + let HitKind::DragScrollTopPopover { max_scroll } = scrollbar.interact.as_ref().expect("scroll drag").kind else { panic!("scrollbar kind"); @@ -2571,23 +2433,22 @@ fn top_popover_scroll_bounds_serve_the_wheel_path() { snapshot.customize_items_open = false; snapshot.customize_items_group = None; snapshot.session_popover_open = true; - let (natural, viewport) = - top_popover_scroll_bounds(&crate::ui_text::UiTextEngine::default(), &snapshot) - .expect("bounds while the session popover is open"); + let (natural, viewport) = top_popover_scroll_bounds(&UiTextEngine::default(), &snapshot) + .expect("bounds while the session popover is open"); assert_eq!(natural, viewport, "short content never scrolls"); } #[test] fn hidden_items_produce_no_nodes() { let mut snapshot = snapshot(); - snapshot.resolved_toolbar_items = crate::config::ToolbarItemsConfig { + snapshot.resolved_toolbar_items = ToolbarItemsConfig { hidden: vec![ ids::TOP_TOOL_PEN.as_str().to_string(), ids::TOP_GROUP_QUICK_COLORS.as_str().to_string(), ids::TOP_UTILITY_UNDO.as_str().to_string(), ], shown: Vec::new(), - order: crate::config::ToolbarItemOrderConfig::default(), + order: ToolbarItemOrderConfig::default(), } .resolved(); diff --git a/src/backend/wayland/toolbar/view/top/tests/engine.rs b/src/backend/wayland/toolbar/view/top/tests/engine.rs index 6571d8b49..8b8e1287d 100644 --- a/src/backend/wayland/toolbar/view/top/tests/engine.rs +++ b/src/backend/wayland/toolbar/view/top/tests/engine.rs @@ -13,9 +13,9 @@ fn retained_planner_preserves_wrapped_notices_surface_size_and_input_regions() { "/tmp/設定/Καλημέρα/long-directory-name/another-long-directory-name/runtime-ui.toml", "/tmp/ui.toml", ] { - snapshot.runtime_ui_persistence = Some(crate::ui::toolbar::RuntimeUiPersistenceSnapshot { + snapshot.runtime_ui_persistence = Some(RuntimeUiPersistenceSnapshot { path: path.into(), - mode: crate::ui::toolbar::RuntimeUiPersistenceMode::Supported, + mode: RuntimeUiPersistenceMode::Supported, detail: None, recovery_artifacts: Vec::new(), }); @@ -60,7 +60,7 @@ fn retained_planner_preserves_wrapped_notices_surface_size_and_input_regions() { engine.layout( &ctx, crate::ui_text::UiTextStyle { - family: crate::ui::theme::toolbar::FONT_FAMILY_DEFAULT, + family: toolbar::FONT_FAMILY_DEFAULT, slant: cairo::FontSlant::Normal, weight: cairo::FontWeight::Normal, size: super::super::menus::MENU_META_FONT, @@ -84,8 +84,7 @@ fn retained_planner_matches_fresh_under_width_pressure_and_scroll_bounds() { let mut snapshot = snapshot(); snapshot.settings_popover_open = true; snapshot.customize_items_open = true; - snapshot.customize_items_group = - Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopControls); + snapshot.customize_items_group = Some(ToolbarItemCustomizeGroup::TopControls); snapshot.top_available_height = Some(360.0); for width in [1200.0, 420.0, 1200.0] { snapshot.top_viewport_max = Some(width); diff --git a/src/backend/wayland/toolbar/view/tree.rs b/src/backend/wayland/toolbar/view/tree.rs index 24190c5ca..b0dd64512 100644 --- a/src/backend/wayland/toolbar/view/tree.rs +++ b/src/backend/wayland/toolbar/view/tree.rs @@ -2,10 +2,9 @@ #[cfg(test)] use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; #[cfg(test)] use crate::backend::wayland::toolbar::hit::rect_contains_with_min_target; -use crate::backend::wayland::toolbar::hit::rect_with_min_target; +use crate::backend::wayland::toolbar::hit::{HitRegion, rect_with_min_target}; use super::node::{WidgetId, WidgetNode}; diff --git a/src/backend/wayland/zoom/capture.rs b/src/backend/wayland/zoom/capture.rs index f9e09ecd9..44f920d8b 100644 --- a/src/backend/wayland/zoom/capture.rs +++ b/src/backend/wayland/zoom/capture.rs @@ -1,10 +1,10 @@ use anyhow::{Context, Result}; use log::{debug, info, warn}; -use smithay_client_toolkit::shm::{ - Shm, - slot::{Buffer, SlotPool}, -}; -use wayland_client::{Dispatch, QueueHandle, WEnum, protocol::wl_shm}; +use smithay_client_toolkit::shm::Shm; +use smithay_client_toolkit::shm::slot::{Buffer, SlotPool}; +use tokio::runtime::Handle; +use wayland_client::protocol::wl_shm; +use wayland_client::{Dispatch, QueueHandle, WEnum}; use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::{ Event as FrameEvent, Flags, ZwlrScreencopyFrameV1, }; @@ -151,7 +151,7 @@ impl ZoomState { backend: ZoomCaptureBackend, shm: &Shm, qh: &QueueHandle, - tokio_handle: &tokio::runtime::Handle, + tokio_handle: &Handle, ) -> Result<()> where State: @@ -212,8 +212,8 @@ impl ZoomState { Ok(()) } - pub fn handle_frame_event(&mut self, event: FrameEvent, input_state: &mut InputState) { - match event { + pub fn handle_frame_event(&mut self, event: &FrameEvent, input_state: &mut InputState) { + match *event { FrameEvent::Buffer { format, width, @@ -377,6 +377,8 @@ impl ZoomState { #[cfg(test)] mod tests { + use crate::backend::wayland::{RuntimeWakeSource, zoom}; + use super::*; use wayland_client::protocol::wl_output; @@ -411,7 +413,7 @@ mod tests { ) .expect("known output geometry"); let context = CaptureContext::new(7, geometry, 3); - let image = crate::backend::wayland::frozen::FrozenImage { + let image = FrozenImage { width: 4, height: 3, stride: 16, @@ -433,7 +435,7 @@ mod tests { ) .expect("rotated output geometry"); let context = CaptureContext::new(7, geometry, 3); - let image = crate::backend::wayland::frozen::FrozenImage { + let image = FrozenImage { width: 3, height: 2, stride: 12, @@ -472,11 +474,11 @@ mod tests { #[tokio::test] async fn portal_capture_waits_for_suppression_preflight() { - let wake = crate::backend::wayland::RuntimeWakeSource::new().expect("runtime wake"); + let wake = RuntimeWakeSource::try_new().expect("runtime wake"); let mut state = ZoomState::new_with_runtime_wake(None, wake.handle()); state - .start_capture(crate::backend::wayland::zoom::ZoomCaptureBackend::WlrScreencopy) + .start_capture(zoom::ZoomCaptureBackend::WlrScreencopy) .expect("queue portal zoom capture"); assert!(state.preflight_pending()); @@ -489,7 +491,7 @@ mod tests { #[test] fn preflight_layout_snapshot_goes_stale_when_geometry_changes() { - let wake = crate::backend::wayland::RuntimeWakeSource::new().expect("runtime wake"); + let wake = RuntimeWakeSource::try_new().expect("runtime wake"); let mut state = ZoomState::new_with_runtime_wake(None, wake.handle()); state.snapshot_preflight_layout(); assert!(state.preflight_layout_is_current()); diff --git a/src/backend/wayland/zoom/mod.rs b/src/backend/wayland/zoom/mod.rs index b17a2b86d..18ab56e8b 100644 --- a/src/backend/wayland/zoom/mod.rs +++ b/src/backend/wayland/zoom/mod.rs @@ -11,6 +11,9 @@ pub(in crate::backend::wayland) use state::{ ZoomWaiter, ZoomWaiterOwner, ZoomWaiterRegistry, }; +use crate::backend::wayland::frozen::{FrozenImage, ScreenImageProvenance}; +use crate::capture::types::CaptureError; + const MIN_ZOOM_SCALE: f64 = 1.0; const MAX_ZOOM_SCALE: f64 = 8.0; @@ -20,8 +23,8 @@ type PortalCaptureResult = Result< ( Option, u64, - crate::backend::wayland::frozen::ScreenImageProvenance, - Result, + ScreenImageProvenance, + Result, ), - crate::capture::types::CaptureError, + CaptureError, >; diff --git a/src/backend/wayland/zoom/portal.rs b/src/backend/wayland/zoom/portal.rs index 45e93a48e..d73243098 100644 --- a/src/backend/wayland/zoom/portal.rs +++ b/src/backend/wayland/zoom/portal.rs @@ -1,24 +1,24 @@ use anyhow::Result; use log::warn; use std::time::{Duration, Instant}; +use tokio::runtime::Handle; use crate::backend::wayland::frozen::ScreenImageProvenance; use crate::backend::wayland::frozen_geometry::require_verified_capture_source; use crate::backend::wayland::portal_capture::{ capture_via_portal_fullscreen_bytes, portal_output_matches, }; +use crate::backend::wayland::portal_raster; use crate::backend::wayland::portal_task::{PortalPoll, PortalTask}; use crate::capture::sources::frozen::decode_image_to_argb; use crate::capture::types::CaptureError; use crate::input::InputState; +use crate::notification; use super::state::{ZoomSourceOutcome, ZoomState}; impl ZoomState { - pub(super) fn capture_via_portal( - &mut self, - tokio_handle: &tokio::runtime::Handle, - ) -> Result<()> { + pub(super) fn capture_via_portal(&mut self, tokio_handle: &Handle) -> Result<()> { if self.portal.is_running() { warn!("Zoom portal capture already running; ignoring new request"); return Ok(()); @@ -43,7 +43,7 @@ impl ZoomState { geo.transform, ) .expect("verified output geometry has a positive scale"); - crate::notification::send_notification_async( + notification::send_notification_async( tokio_handle, "Zoom capture".to_string(), "Requesting screen capture...".to_string(), @@ -57,9 +57,7 @@ impl ZoomState { let (data, width, height) = decode_image_to_argb(&bytes).map_err(|error| { CaptureError::ImageError(format!("Decode failed: {error}")) })?; - let image = crate::backend::wayland::portal_raster::crop_portal_raster( - data, width, height, &geo, - ); + let image = portal_raster::crop_portal_raster(&data, width, height, &geo); Ok((Some(target_output_id), layout_generation, provenance, image)) } @@ -209,6 +207,15 @@ impl ZoomState { #[cfg(test)] mod tests { + use std::future; + + use tokio::task; + use wayland_client::protocol::wl_output::Transform; + + use crate::backend::wayland::RuntimeWakeSource; + use crate::backend::wayland::capture_preflight::CaptureLayoutRetry; + use crate::capture::DesktopBackdropGeometry; + use super::*; use crate::backend::wayland::frozen::FrozenImage; use crate::backend::wayland::frozen_geometry::OutputGeometry; @@ -225,13 +232,7 @@ mod tests { } fn provenance(output_id: u32, layout_generation: u64) -> ScreenImageProvenance { - ScreenImageProvenance::new( - output_id, - layout_generation, - 1, - wayland_client::protocol::wl_output::Transform::Normal, - ) - .unwrap() + ScreenImageProvenance::new(output_id, layout_generation, 1, Transform::Normal).unwrap() } fn stale_report() -> Option { @@ -241,16 +242,14 @@ mod tests { }) } - fn crop_geometry( - origin: (u32, u32), - ) -> crate::backend::wayland::frozen_geometry::OutputGeometry { - crate::backend::wayland::frozen_geometry::OutputGeometry { + fn crop_geometry(origin: (u32, u32)) -> OutputGeometry { + OutputGeometry { logical_x: 0, logical_y: 0, logical_width: 2, logical_height: 1, scale: 1, - transform: wayland_client::protocol::wl_output::Transform::Normal, + transform: Transform::Normal, overlay_buffer_size: (2, 1), pixel_size: Some((2, 1)), screenshot_origin: Some(origin), @@ -262,11 +261,11 @@ mod tests { #[tokio::test] async fn portal_start_requires_verifiable_geometry_and_output_identity() -> anyhow::Result<()> { - let wake = crate::backend::wayland::RuntimeWakeSource::new()?; + let wake = RuntimeWakeSource::try_new()?; let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); let error = zoom - .capture_via_portal(&tokio::runtime::Handle::current()) + .capture_via_portal(&Handle::current()) .expect_err("missing geometry must fail closed"); assert!(error.to_string().contains("geometry is unavailable")); assert!(!zoom.portal.is_running()); @@ -274,7 +273,7 @@ mod tests { zoom.set_active_geometry(Some(crop_geometry((0, 0)))); let error = zoom - .capture_via_portal(&tokio::runtime::Handle::current()) + .capture_via_portal(&Handle::current()) .expect_err("missing output identity must fail closed"); assert!(error.to_string().contains("identity is unavailable")); assert!(!zoom.portal.is_running()); @@ -289,11 +288,11 @@ mod tests { Some((3, 2)), (3, 2), 2, - wayland_client::protocol::wl_output::Transform::Normal, + Transform::Normal, Some((5, 3)), ) .expect("fractional geometry") - .with_desktop_backdrop_geometry(Some(crate::capture::DesktopBackdropGeometry { + .with_desktop_backdrop_geometry(Some(DesktopBackdropGeometry { logical_x: 0, logical_y: 0, logical_width: 3, @@ -306,13 +305,8 @@ mod tests { screenshot_height: Some(3), })); - let image = crate::backend::wayland::portal_raster::crop_portal_raster( - vec![7; 5 * 3 * 4], - 5, - 3, - &geometry, - ) - .expect("fractional output screenshot"); + let image = portal_raster::crop_portal_raster(&[7; 5 * 3 * 4], 5, 3, &geometry) + .expect("fractional output screenshot"); assert_eq!((image.width, image.height), (5, 3)); assert_eq!(image.stride, 20); @@ -359,19 +353,14 @@ mod tests { Some((active.logical_width as i32, active.logical_height as i32)), (active.logical_width, active.logical_height), if index == 0 { 1 } else { 2 }, - wayland_client::protocol::wl_output::Transform::Normal, + Transform::Normal, Some((8, 4)), ) .unwrap() .with_desktop_backdrop_geometry(DesktopBackdropGeometry::from_outputs(active, &outputs)) .with_portal_outputs(Some(outputs.to_vec())); - let image = crate::backend::wayland::portal_raster::crop_portal_raster( - pixels.clone(), - 24, - 8, - &geometry, - ) - .expect("KDE mixed-scale crop"); + let image = portal_raster::crop_portal_raster(&pixels, 24, 8, &geometry) + .expect("KDE mixed-scale crop"); assert_eq!((image.width, image.height), (8, 4)); assert_eq!( image.data, @@ -383,8 +372,8 @@ mod tests { #[test] fn portal_crop_rejects_a_different_screenshot_layout() { - let geometry = crop_geometry((0, 0)).with_desktop_backdrop_geometry(Some( - crate::capture::DesktopBackdropGeometry { + let geometry = + crop_geometry((0, 0)).with_desktop_backdrop_geometry(Some(DesktopBackdropGeometry { logical_x: 0, logical_y: 0, logical_width: 2, @@ -395,18 +384,9 @@ mod tests { crop_y: Some(0), screenshot_width: Some(2), screenshot_height: Some(1), - }, - )); + })); - assert!( - crate::backend::wayland::portal_raster::crop_portal_raster( - vec![0; 3 * 4], - 3, - 1, - &geometry - ) - .is_err() - ); + assert!(portal_raster::crop_portal_raster(&[0; 3 * 4], 3, 1, &geometry).is_err()); } async fn poll_until_finished(zoom: &mut ZoomState, input: &mut InputState) { @@ -423,21 +403,21 @@ mod tests { if !zoom.portal.is_running() { return; } - tokio::task::yield_now().await; + task::yield_now().await; } panic!("zoom portal task did not finish"); } #[tokio::test] async fn success_activates_zoom_with_the_matching_image() { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let id = zoom.begin_identified_capture(); zoom.set_active_output(None, Some(1)); zoom.request_activation(); zoom.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async { Ok((Some(1), 0, provenance(1, 0), Ok(image(3)))) }, )); @@ -464,17 +444,17 @@ mod tests { #[tokio::test] async fn domain_error_and_task_panic_restore_the_zoom_lifecycle() { for panic_task in [false, true] { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let id = zoom.begin_identified_capture(); zoom.request_activation(); zoom.portal.start(if panic_task { - PortalTask::spawn(&tokio::runtime::Handle::current(), wake.handle(), async { + PortalTask::spawn(&Handle::current(), wake.handle(), async { panic!("expected zoom portal panic") }) } else { - PortalTask::spawn(&tokio::runtime::Handle::current(), wake.handle(), async { + PortalTask::spawn(&Handle::current(), wake.handle(), async { Err(CaptureError::PermissionDenied) }) }); @@ -499,12 +479,12 @@ mod tests { #[tokio::test] async fn portal_dismissal_emits_a_correlated_cancelled_terminal() { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let id = zoom.begin_identified_capture(); zoom.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async { Err(CaptureError::Cancelled("dismissed".to_string())) }, )); @@ -526,17 +506,17 @@ mod tests { async fn disconnect_and_deadline_expiry_restore_without_a_producer_result() { let now = Instant::now(); for timed_out in [false, true] { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let id = zoom.begin_identified_capture(); zoom.request_activation(); zoom.portal.start(if timed_out { PortalTask::spawn_at_for_test( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), now.checked_sub(PORTAL_CAPTURE_TIMEOUT).unwrap(), - std::future::pending(), + future::pending(), ) } else { PortalTask::disconnected_for_test(now) @@ -562,7 +542,7 @@ mod tests { #[tokio::test] async fn stale_output_preserves_the_current_zoom_image_and_activation() { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let id = zoom.begin_identified_capture(); @@ -571,7 +551,7 @@ mod tests { zoom.set_active_output(None, Some(2)); zoom.request_activation(); zoom.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async { Ok((Some(1), 0, provenance(1, 0), Ok(image(9)))) }, )); @@ -596,11 +576,11 @@ mod tests { #[tokio::test] async fn exhausted_layout_retry_preserves_the_current_zoom_image_and_activation() { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let id = zoom.begin_identified_capture(); - zoom.layout_retry = crate::backend::wayland::capture_preflight::CaptureLayoutRetry::Spent; + zoom.layout_retry = CaptureLayoutRetry::Spent; zoom.set_image(image(4)); let generation = zoom.image_generation(); zoom.set_active_geometry(Some(crop_geometry((0, 0)))); @@ -608,7 +588,7 @@ mod tests { let layout_generation = zoom.layout_generations.active_output; zoom.request_activation(); zoom.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async move { Ok(( @@ -641,7 +621,7 @@ mod tests { #[tokio::test] async fn matching_layout_still_applies_after_an_identical_geometry_refresh() { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let id = zoom.begin_identified_capture(); @@ -651,7 +631,7 @@ mod tests { let layout_generation = zoom.layout_generations.active_output; zoom.request_activation(); zoom.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async move { Ok(( @@ -684,11 +664,11 @@ mod tests { #[tokio::test] async fn exhausted_topology_retry_discards_a_single_output_portal_image() { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); let id = zoom.begin_identified_capture(); - zoom.layout_retry = crate::backend::wayland::capture_preflight::CaptureLayoutRetry::Spent; + zoom.layout_retry = CaptureLayoutRetry::Spent; zoom.set_image(image(4)); let generation = zoom.image_generation(); zoom.set_active_geometry(Some(crop_geometry((0, 0)).with_known_output_count(Some(1)))); @@ -696,7 +676,7 @@ mod tests { let layout_generation = zoom.layout_generations.active_output; zoom.request_activation(); zoom.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async move { Ok(( @@ -728,18 +708,17 @@ mod tests { #[tokio::test] async fn supersession_is_ignored_and_explicit_abort_owns_task_cancellation() { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); let id = zoom.begin_identified_capture(); zoom.request_activation(); zoom.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), - std::future::pending(), + future::pending(), )); - zoom.capture_via_portal(&tokio::runtime::Handle::current()) - .unwrap(); + zoom.capture_via_portal(&Handle::current()).unwrap(); assert!(zoom.portal.is_running()); assert!(zoom.abort_capture()); assert!(!zoom.portal.is_running()); diff --git a/src/backend/wayland/zoom/retry.rs b/src/backend/wayland/zoom/retry.rs index c5d3be2e2..73b41cc5c 100644 --- a/src/backend/wayland/zoom/retry.rs +++ b/src/backend/wayland/zoom/retry.rs @@ -84,6 +84,12 @@ impl ZoomState { #[cfg(test)] mod tests { + use tokio::runtime::Handle; + use tokio::task; + + use crate::backend::wayland::{RuntimeWakeSource, zoom}; + use crate::input::InputState; + use super::*; use crate::backend::wayland::frozen::{FrozenImage, ScreenImageProvenance}; use crate::backend::wayland::frozen_geometry::OutputGeometry; @@ -117,13 +123,13 @@ mod tests { } } - async fn drain(zoom: &mut ZoomState, input: &mut crate::input::InputState) { + async fn drain(zoom: &mut ZoomState, input: &mut InputState) { for _ in 0..100 { zoom.poll_portal_capture(input, Instant::now(), Some(1)); if !zoom.portal.is_running() { return; } - tokio::task::yield_now().await; + task::yield_now().await; } panic!("portal task did not finish"); @@ -132,12 +138,12 @@ mod tests { #[tokio::test] async fn stale_raster_retries_once_with_the_same_waiter_and_activation() { for second_change in [false, true] { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); zoom.set_active_output(None, Some(1)); zoom.set_active_geometry(Some(geometry(0))); - zoom.start_capture(crate::backend::wayland::zoom::ZoomCaptureBackend::Portal) + zoom.start_capture(zoom::ZoomCaptureBackend::Portal) .unwrap(); assert_eq!( zoom.take_preflight_pending(), @@ -153,7 +159,7 @@ mod tests { let old_generation = zoom.layout_generations.desktop; zoom.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async move { Ok(( @@ -193,7 +199,7 @@ mod tests { let fresh_generation = zoom.layout_generations.desktop; zoom.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async move { Ok(( @@ -243,7 +249,7 @@ mod tests { #[tokio::test] async fn cancellation_and_stable_raster_failure_do_not_schedule_retry() { for cancelled in [false, true] { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut zoom = ZoomState::new_with_runtime_wake(None, wake.handle()); let mut input = make_test_input_state(); zoom.set_active_output(None, Some(1)); @@ -251,7 +257,7 @@ mod tests { let id = zoom.begin_identified_capture(); let generation = zoom.layout_generations.active_output; zoom.portal.start(PortalTask::spawn( - &tokio::runtime::Handle::current(), + &Handle::current(), wake.handle(), async move { if cancelled { diff --git a/src/backend/wayland/zoom/state.rs b/src/backend/wayland/zoom/state.rs index 0ebb21918..29089f452 100644 --- a/src/backend/wayland/zoom/state.rs +++ b/src/backend/wayland/zoom/state.rs @@ -373,6 +373,8 @@ impl ZoomState { #[cfg(test)] mod tests { + use crate::backend::wayland::zoom; + use super::*; use crate::input::state::test_support::make_test_input_state; @@ -385,7 +387,7 @@ mod tests { assert_eq!(state.take_source_terminal(), None); state - .start_capture(crate::backend::wayland::zoom::ZoomCaptureBackend::WlrScreencopy) + .start_capture(zoom::ZoomCaptureBackend::WlrScreencopy) .expect("first capture starts"); let first = state.current_capture_id().expect("first capture id"); assert!(state.abort_capture()); @@ -399,7 +401,7 @@ mod tests { ); state - .start_capture(crate::backend::wayland::zoom::ZoomCaptureBackend::WlrScreencopy) + .start_capture(zoom::ZoomCaptureBackend::WlrScreencopy) .expect("second capture starts"); let second = state.current_capture_id().expect("second capture id"); assert!(second > first); @@ -410,13 +412,13 @@ mod tests { let mut state = ZoomState::new(None); state - .start_capture(crate::backend::wayland::zoom::ZoomCaptureBackend::WlrScreencopy) + .start_capture(zoom::ZoomCaptureBackend::WlrScreencopy) .expect("first capture starts"); let first = state.current_capture_id().expect("first capture id"); assert!(state.abort_capture()); let error = state - .start_capture(crate::backend::wayland::zoom::ZoomCaptureBackend::WlrScreencopy) + .start_capture(zoom::ZoomCaptureBackend::WlrScreencopy) .expect_err("an undrained terminal blocks the next capture"); assert_eq!( @@ -466,7 +468,7 @@ mod tests { provenance, ); state - .start_capture(crate::backend::wayland::zoom::ZoomCaptureBackend::Portal) + .start_capture(zoom::ZoomCaptureBackend::Portal) .unwrap(); let generation = state.image_generation(); let mut changed = geometry.with_known_output_count(Some(2)); @@ -555,7 +557,7 @@ mod tests { state.finish_preflight_failure( &mut input_state, - CapturePreflightError::Backend(message.to_string()), + &CapturePreflightError::Backend(message.to_string()), ); assert_eq!( diff --git a/src/backend/wayland/zoom/state/source.rs b/src/backend/wayland/zoom/state/source.rs index ec281aace..4fcdb7156 100644 --- a/src/backend/wayland/zoom/state/source.rs +++ b/src/backend/wayland/zoom/state/source.rs @@ -1,5 +1,11 @@ //! Identified Zoom requests, waiters, terminal reports and cancellation cleanup. -use super::*; +use crate::i18n; + +use super::ZoomState; +use crate::backend::wayland::capture_preflight::{ + CaptureLayoutRetry, CapturePreflight, CapturePreflightError, +}; +use crate::input::InputState; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(in crate::backend::wayland) struct ZoomCaptureId(u64); @@ -135,10 +141,14 @@ impl ZoomState { let report = report.or_else(|| { matches!(outcome, ZoomSourceOutcome::StaleLayout).then(|| ZoomTerminalReport { source: "zoom", - message: CapturePreflightError::StaleLayout.message(crate::i18n::tr("Zoom")), + message: CapturePreflightError::StaleLayout.message(i18n::tr("Zoom")), }) }); - debug_assert!(self.source_terminal.is_none()); + debug_assert!( + self.source_terminal.is_none(), + "a zoom source finished while an earlier terminal is unclaimed: {:?}", + self.source_terminal + ); if self.source_terminal.is_none() { self.source_terminal = Some(ZoomSourceTerminal { id, @@ -151,9 +161,9 @@ impl ZoomState { pub(in crate::backend::wayland) fn finish_preflight_failure( &mut self, input_state: &mut InputState, - error: CapturePreflightError, + error: &CapturePreflightError, ) { - let message = error.message(crate::i18n::tr("Zoom")); + let message = error.message(i18n::tr("Zoom")); let report = ZoomTerminalReport { source: "zoom", message: message.clone(), diff --git a/src/base64.rs b/src/base64.rs index cffccf25d..8fa3daae0 100644 --- a/src/base64.rs +++ b/src/base64.rs @@ -1,4 +1,5 @@ -use std::{error::Error, fmt}; +use std::error::Error; +use std::fmt; const STANDARD_ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; diff --git a/src/build_info.rs b/src/build_info.rs index 194c13498..97b00a9ed 100644 --- a/src/build_info.rs +++ b/src/build_info.rs @@ -1,14 +1,19 @@ +use crate::update_check; + +#[must_use] pub fn version() -> &'static str { option_env!("WAYSCRIBER_RELEASE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")) } /// Short git hash this binary was built from, or `"unknown"` outside a /// checkout (release tarballs, vendored distro builds). +#[must_use] pub fn commit_hash() -> &'static str { env!("WAYSCRIBER_GIT_HASH") } /// Commit date (`YYYY-MM-DD`) of that build, when it could be determined. +#[must_use] pub fn build_date() -> Option<&'static str> { let date = env!("WAYSCRIBER_BUILD_DATE"); (!date.is_empty()).then_some(date) @@ -16,6 +21,7 @@ pub fn build_date() -> Option<&'static str> { /// How this copy was installed, as declared by the packager at build time /// (`apt`, `rpm`, `aur`, `nix`, `tarball`, `source`, ...). +#[must_use] pub fn install_source() -> Option<&'static str> { - crate::update_check::install_source() + update_check::install_source() } diff --git a/src/canvas_export/mod.rs b/src/canvas_export/mod.rs index 6fd95e1fa..97540302b 100644 --- a/src/canvas_export/mod.rs +++ b/src/canvas_export/mod.rs @@ -27,6 +27,9 @@ pub(crate) use region::{ mod tests { use std::sync::Arc; + use crate::draw; + use crate::draw::{EraserBrush, EraserKind, RenderCaches, TextMeasurer}; + use super::*; use crate::canvas_export::page::draw_canvas_page_with_measurer; use crate::canvas_export::png::render_canvas_surface; @@ -200,8 +203,8 @@ mod tests { { let ctx = cairo::Context::new(&surface).expect("context"); draw_canvas_page_with_measurer( - &crate::draw::TextMeasurer::default(), - &mut crate::draw::RenderCtx::new(&ctx, &mut crate::draw::RenderCaches::default()), + &TextMeasurer::default(), + &mut crate::draw::RenderCtx::new(&ctx, &mut RenderCaches::default()), &page_snapshot(frame), 2.0, ) @@ -517,9 +520,9 @@ mod tests { }); frame.add_shape(Shape::EraserStroke { points: vec![(6, 6)], - brush: crate::draw::EraserBrush { + brush: EraserBrush { size: 6.0, - kind: crate::draw::EraserKind::Circle, + kind: EraserKind::Circle, }, }); let mut export = snapshot( @@ -553,9 +556,9 @@ mod tests { }); frame.add_shape(Shape::EraserStroke { points: vec![(6, 6)], - brush: crate::draw::EraserBrush { + brush: EraserBrush { size: 6.0, - kind: crate::draw::EraserKind::Circle, + kind: EraserKind::Circle, }, }); let mut surface = render_canvas_surface(&snapshot( @@ -586,13 +589,13 @@ mod tests { cy: 60, rx: 14, ry: 14, - magnification: crate::draw::DEFAULT_SPOTLIGHT_MAGNIFICATION, + magnification: draw::DEFAULT_SPOTLIGHT_MAGNIFICATION, }); frame.add_shape(Shape::EraserStroke { points: vec![(40, 60), (110, 60)], - brush: crate::draw::EraserBrush { + brush: EraserBrush { size: 12.0, - kind: crate::draw::EraserKind::Circle, + kind: EraserKind::Circle, }, }); diff --git a/src/canvas_export/page.rs b/src/canvas_export/page.rs index efed43d37..1726524c3 100644 --- a/src/canvas_export/page.rs +++ b/src/canvas_export/page.rs @@ -1,10 +1,13 @@ use std::sync::Arc; use crate::capture::CaptureError; +use crate::domain::{BoardGrid, BoardGridKind}; +use crate::draw; use crate::draw::{ - BlurRectParams, Color, EraserReplayContext, Frame, RenderCtx, Shape, SpotlightMagnifierOutcome, - SpotlightMagnifierScratch, SpotlightMagnifierSource, SpotlightPass, render_eraser_stroke, - render_spotlight_magnification_pass, render_spotlight_pass, spotlight_regions_for_frame, + BlurRectParams, BoardPaper, Color, EraserReplayContext, Frame, RenderCtx, Shape, + SpotlightMagnifierOutcome, SpotlightMagnifierScratch, SpotlightMagnifierSource, SpotlightPass, + TextMeasurer, render_eraser_stroke, render_spotlight_magnification_pass, render_spotlight_pass, + spotlight_regions_for_frame, }; use crate::screen_pixels::ScreenImage; @@ -45,7 +48,7 @@ pub enum CanvasExportBackdropSnapshot { Solid(Color), BoardPaper { color: Color, - grid: crate::domain::BoardGrid, + grid: BoardGrid, }, PersistedImage { data: Arc<[u8]>, @@ -58,8 +61,9 @@ pub enum CanvasExportBackdropSnapshot { } impl CanvasExportBackdropSnapshot { - pub fn board_paper(color: Color, grid: crate::domain::BoardGrid) -> Self { - if grid.kind == crate::domain::BoardGridKind::None { + #[must_use] + pub fn board_paper(color: Color, grid: BoardGrid) -> Self { + if grid.kind == BoardGridKind::None { Self::Solid(color) } else { Self::BoardPaper { color, grid } @@ -67,7 +71,7 @@ impl CanvasExportBackdropSnapshot { } /// Loupe availability for this backdrop, answered without decoding it. /// - /// Mirrors what [`ExportBackdrop::new`] will produce for the same variant, + /// Mirrors what [`ExportBackdrop::try_new`] will produce for the same variant, /// which is what lets the main-thread preflight refuse a page before a /// render worker is ever submitted. pub(crate) fn magnifier_source(&self) -> SpotlightMagnifierSource { @@ -90,6 +94,7 @@ pub struct CanvasExportRect { } impl CanvasExportRect { + #[must_use] pub fn new(x: f64, y: f64, width: f64, height: f64) -> Option { if !x.is_finite() || !y.is_finite() @@ -110,13 +115,13 @@ impl CanvasExportRect { } pub fn draw_canvas_page_with_measurer( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, render: &mut RenderCtx<'_, '_>, page: &CanvasPageExportSnapshot, output_scale: f64, ) -> Result<(), CaptureError> { let ctx = render.cairo; - let backdrop = ExportBackdrop::new(&page.backdrop)?; + let backdrop = ExportBackdrop::try_new(&page.backdrop)?; let source = CanvasExportRect { x: page.origin_x as f64, y: page.origin_y as f64, @@ -154,7 +159,7 @@ pub fn draw_canvas_page_with_measurer( #[allow(clippy::too_many_arguments)] pub(crate) fn draw_canvas_page_region( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, render: &mut RenderCtx<'_, '_>, page: &CanvasPageExportSnapshot, backdrop: &ExportBackdrop, @@ -229,10 +234,10 @@ impl ExportBackdrop { /// one number for the whole of it would be a guess. pub(crate) fn solid_luminance(&self) -> Option { self.bg_color - .map(|color| crate::draw::perceived_luminance(color.r, color.g, color.b)) + .map(|color| draw::perceived_luminance(color.r, color.g, color.b)) } - pub(crate) fn new(snapshot: &CanvasExportBackdropSnapshot) -> Result { + pub(crate) fn try_new(snapshot: &CanvasExportBackdropSnapshot) -> Result { match snapshot { CanvasExportBackdropSnapshot::Transparent => Ok(Self { surface: None, @@ -400,14 +405,14 @@ impl ExportBackdrop { SpotlightMagnifierSource::from_backdrop( self.surface .is_some() - .then_some(crate::draw::IMMUTABLE_RASTER_SOURCE_TOKEN), + .then_some(draw::IMMUTABLE_RASTER_SOURCE_TOKEN), self.bg_color.is_some(), ) } } fn draw_canvas_page_contents( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, render: &mut RenderCtx<'_, '_>, page: &CanvasPageExportSnapshot, backdrop: &ExportBackdrop, @@ -419,11 +424,11 @@ fn draw_canvas_page_contents( backdrop.paint(ctx); } let paper = match page.backdrop { - CanvasExportBackdropSnapshot::BoardPaper { color, grid } => Some( - crate::draw::BoardPaper::for_context(color, grid, ctx).map_err(|err| { + CanvasExportBackdropSnapshot::BoardPaper { color, grid } => { + Some(BoardPaper::for_context(color, grid, ctx).map_err(|err| { CaptureError::ImageError(format!("Failed to render board paper: {err}")) - })?, - ), + })?) + } _ => None, }; if let Some(paper) = &paper { @@ -522,7 +527,7 @@ fn draw_canvas_page_contents( pub(crate) fn frame_has_magnified_spotlight(frame: &Frame) -> bool { spotlight_regions_for_frame(frame) .iter() - .any(|region| crate::draw::spotlight_magnification_is_active(region.magnification)) + .any(|region| draw::spotlight_magnification_is_active(region.magnification)) } /// Refuses an export whose own backdrop cannot feed the loupes on its frame. @@ -605,7 +610,7 @@ mod tests { width: 2, height: 2, stride: 8, - data: vec![0xFF; 16], + data: vec![0xff; 16], }); let pixels = image.data.as_ptr(); let backdrop = ExportBackdrop::from_region_source( @@ -629,11 +634,11 @@ mod backdrop_luminance_tests { #[test] fn a_solid_page_reports_its_own_brightness_for_text_to_contrast_with() { - let white = ExportBackdrop::new(&CanvasExportBackdropSnapshot::Solid(Color::new( + let white = ExportBackdrop::try_new(&CanvasExportBackdropSnapshot::Solid(Color::new( 1.0, 1.0, 1.0, 1.0, ))) .expect("backdrop"); - let black = ExportBackdrop::new(&CanvasExportBackdropSnapshot::Solid(Color::new( + let black = ExportBackdrop::try_new(&CanvasExportBackdropSnapshot::Solid(Color::new( 0.0, 0.0, 0.0, 1.0, ))) .expect("backdrop"); @@ -645,7 +650,7 @@ mod backdrop_luminance_tests { #[test] fn a_transparent_page_has_no_colour_to_report() { let backdrop = - ExportBackdrop::new(&CanvasExportBackdropSnapshot::Transparent).expect("backdrop"); + ExportBackdrop::try_new(&CanvasExportBackdropSnapshot::Transparent).expect("backdrop"); assert_eq!(backdrop.solid_luminance(), None); } @@ -655,14 +660,14 @@ mod backdrop_luminance_tests { // The screen probes and gets ~1.0; the PDF page cannot be probed and // falls back to this. Both must reach the same decision, or an exported // board looks different from the board it was exported from. - let whiteboard = ExportBackdrop::new(&CanvasExportBackdropSnapshot::Solid(Color::new( + let whiteboard = ExportBackdrop::try_new(&CanvasExportBackdropSnapshot::Solid(Color::new( 1.0, 1.0, 1.0, 1.0, ))) .expect("backdrop"); let red = Color::new(0.96, 0.2, 0.25, 1.0); - let on_screen = crate::draw::text_outline_color(red, Some(1.0)); - let in_pdf = crate::draw::text_outline_color(red, whiteboard.solid_luminance()); + let on_screen = draw::text_outline_color(red, Some(1.0)); + let in_pdf = draw::text_outline_color(red, whiteboard.solid_luminance()); assert_eq!(on_screen, in_pdf); assert!(in_pdf.r < 0.5, "and it is the dark halo"); diff --git a/src/canvas_export/pdf.rs b/src/canvas_export/pdf.rs index d50da323b..98cecb0c8 100644 --- a/src/canvas_export/pdf.rs +++ b/src/canvas_export/pdf.rs @@ -1,6 +1,8 @@ use crate::capture::CaptureError; -use crate::config::{PdfExportConfig, PdfFitMode, PdfOrientation, PdfPageSize}; -use crate::draw::{RenderCaches, RenderCtx}; +use crate::config::{PdfExportConfig, PdfFitMode, PdfLabelConfig, PdfOrientation, PdfPageSize}; +use crate::draw; +use crate::draw::{RenderCaches, RenderCtx, TextMeasurer}; +use crate::ui_text::UiTextEngine; use super::page::{ CanvasExportBackdropSnapshot, CanvasExportRect, CanvasPageExportSnapshot, ExportBackdrop, @@ -17,7 +19,7 @@ const LETTER_HEIGHT: f64 = 792.0; #[derive(Debug, Clone)] pub struct BoardPdfExportSnapshot { pub pages: Vec, - pub labels: crate::config::PdfLabelConfig, + pub labels: PdfLabelConfig, } #[derive(Debug, Clone)] @@ -76,6 +78,7 @@ pub struct PdfPageMetadata { impl PdfPageMetadata { #[allow(clippy::too_many_arguments)] + #[must_use] pub fn new( app_board_index: usize, app_board_count: usize, @@ -144,8 +147,8 @@ pub fn render_board_pdf(snapshot: &BoardPdfExportSnapshot) -> Result, Ca .map_err(|err| CaptureError::ImageError(format!("Failed to create PDF context: {err}")))?; let mut caches = RenderCaches::default(); - let measurer = crate::draw::TextMeasurer::default(); - let ui_text = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_text = UiTextEngine::default(); for page in &snapshot.pages { let layout = page.layout; validate_page_size(layout.page_width, layout.page_height)?; @@ -155,7 +158,7 @@ pub fn render_board_pdf(snapshot: &BoardPdfExportSnapshot) -> Result, Ca CaptureError::ImageError(format!("Failed to set PDF page size: {err}")) })?; paint_pdf_page_background(&ctx, &page.page, layout.page_width, layout.page_height); - let backdrop = ExportBackdrop::new(&page.page.backdrop)?; + let backdrop = ExportBackdrop::try_new(&page.page.backdrop)?; if frame_has_magnified_spotlight(&page.page.frame) { render_magnified_page_raster( &measurer, @@ -206,7 +209,7 @@ pub fn render_board_pdf(snapshot: &BoardPdfExportSnapshot) -> Result, Ca } fn render_magnified_page_raster( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, render: &mut RenderCtx<'_, '_>, page: &CanvasPageExportSnapshot, backdrop: &ExportBackdrop, @@ -240,7 +243,7 @@ fn render_magnified_page_raster( )?; drop(raster_ctx); - crate::draw::with_saved_state(pdf_ctx, || { + draw::with_saved_state(pdf_ctx, || { pdf_ctx.rectangle( layout.destination_rect.x, layout.destination_rect.y, diff --git a/src/canvas_export/pdf/tests.rs b/src/canvas_export/pdf/tests.rs index 16ccd1740..a8ce312a1 100644 --- a/src/canvas_export/pdf/tests.rs +++ b/src/canvas_export/pdf/tests.rs @@ -1,3 +1,11 @@ +use std::process::Output; +use std::{fs, thread}; + +use crate::config::PdfLabelContentMode; +use crate::image_decode::EncodedImageFormat; +use crate::screen_pixels::EmbeddedImageLimits; +use crate::{image_decode, test_temp}; + use super::*; use crate::config::{PdfExportConfig, PdfFitMode, PdfOrientation, PdfPageSize}; use crate::draw::{FontDescriptor, Frame, RED, Shape, WHITE}; @@ -135,9 +143,9 @@ fn rendered_pdf_reports_exact_page_count_and_ordered_sizes() { labels: Default::default(), }) .expect("pdf"); - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = temp.path().join("out.pdf"); - std::fs::write(&path, bytes).expect("write pdf"); + fs::write(&path, bytes).expect("write pdf"); let output = Command::new("pdfinfo") .env("LC_ALL", "C") @@ -178,11 +186,11 @@ fn text_pdf(text_halo_enabled: bool) -> Vec { #[test] fn pdf_export_honours_the_text_halo_setting() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); for enabled in [false, true] { let path = temp.path().join(format!("halo-{enabled}.pdf")); let prefix = temp.path().join(format!("halo-{enabled}")); - std::fs::write(&path, text_pdf(enabled)).expect("write text PDF"); + fs::write(&path, text_pdf(enabled)).expect("write text PDF"); checked_pdf_output( Command::new("pdftoppm") .env("LC_ALL", "C") @@ -192,11 +200,11 @@ fn pdf_export_honours_the_text_halo_setting() { .output() .expect("pdftoppm is required for PDF artifact tests (install poppler)"), ); - let bytes = std::fs::read(prefix.with_extension("png")).expect("rasterized PDF"); - let image = crate::image_decode::decode_rgba( - crate::image_decode::EncodedImageFormat::Png, + let bytes = fs::read(prefix.with_extension("png")).expect("rasterized PDF"); + let image = image_decode::decode_rgba( + EncodedImageFormat::Png, &bytes, - crate::screen_pixels::EmbeddedImageLimits::default().into(), + EmbeddedImageLimits::default().into(), ) .expect("decode rasterized PDF"); assert_eq!((image.width, image.height), (400, 120)); @@ -282,24 +290,24 @@ fn worker_exports_three_page_pdf_from_unicode_metadata() { .collect(); let snapshot = BoardPdfExportSnapshot { pages, - labels: crate::config::PdfLabelConfig { + labels: PdfLabelConfig { enabled: true, - content: crate::config::PdfLabelContentMode::BoardName, + content: PdfLabelContentMode::BoardName, ..Default::default() }, }; // Only value snapshots cross the worker boundary; text resources are // constructed by the export root on the worker thread. - let bytes = std::thread::spawn(move || render_board_pdf(&snapshot)) + let bytes = thread::spawn(move || render_board_pdf(&snapshot)) .join() .unwrap() .unwrap(); assert!(bytes.starts_with(b"%PDF-")); // Cairo can compress page dictionaries into PDF object streams. Ask the // same PDF reader used by the existing page-layout test to inspect them. - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = temp.path().join("worker-pages.pdf"); - std::fs::write(&path, bytes).expect("write worker PDF"); + fs::write(&path, bytes).expect("write worker PDF"); let info = checked_pdf_output( Command::new("pdfinfo") .env("LC_ALL", "C") @@ -314,7 +322,7 @@ fn worker_exports_three_page_pdf_from_unicode_metadata() { ); } -fn checked_pdf_output(output: std::process::Output) -> String { +fn checked_pdf_output(output: Output) -> String { assert!( output.status.success(), "PDF reader failed: {}", diff --git a/src/canvas_export/pdf_labels.rs b/src/canvas_export/pdf_labels.rs index b93b56afa..8c414a2e2 100644 --- a/src/canvas_export/pdf_labels.rs +++ b/src/canvas_export/pdf_labels.rs @@ -4,6 +4,7 @@ use crate::config::{ PDF_LABEL_PAGE_NAME, PDF_LABEL_PAGES, PdfLabelConfig, PdfLabelContentMode, PdfLabelPosition, validate_pdf_label_template, }; +use crate::ui_text::UiTextStyle; use super::pdf::PdfPageMetadata; use crate::ui_text::UiTextEngine; @@ -190,7 +191,7 @@ fn label_value<'a>(name: &str, metadata: &'a PdfPageMetadata) -> Option<&'a str> /// API's idea of its width. fn ellipsize_to_width( engine: &UiTextEngine, - style: &crate::ui_text::UiTextStyle<'_>, + style: &UiTextStyle<'_>, text: &str, max_width: f64, ) -> Option { @@ -281,7 +282,7 @@ mod tests { assert_eq!(text, "5/12"); } - fn label_style() -> crate::ui_text::UiTextStyle<'static> { + fn label_style() -> UiTextStyle<'static> { crate::ui_text::UiTextStyle { family: "Sans", slant: cairo::FontSlant::Normal, diff --git a/src/canvas_export/png.rs b/src/canvas_export/png.rs index 14d99123a..d6b812b69 100644 --- a/src/canvas_export/png.rs +++ b/src/canvas_export/png.rs @@ -1,5 +1,5 @@ use crate::capture::{CaptureError, ImageFormatMetadata, RenderedImage}; -use crate::draw::{Frame, RenderCaches, RenderCtx}; +use crate::draw::{Frame, RenderCaches, RenderCtx, TextMeasurer}; use crate::render_profiles::RenderColorProfile; use crate::util::Rect; @@ -78,7 +78,7 @@ pub(crate) fn render_canvas_surface( snapshot: &CanvasExportSnapshot, ) -> Result { let mut caches = RenderCaches::default(); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let viewport = snapshot.viewport; let scale = viewport.scale.max(1); let physical_width = viewport.logical_width.saturating_mul(scale as u32); diff --git a/src/canvas_export/region.rs b/src/canvas_export/region.rs index 80c6dfc4d..f7e8e37b5 100644 --- a/src/canvas_export/region.rs +++ b/src/canvas_export/region.rs @@ -6,15 +6,18 @@ use crate::canvas_export::page::{ CanvasExportBackdropSnapshot, CanvasExportRect, CanvasPageExportSnapshot, ExportBackdrop, SpotlightPassSnapshot, draw_canvas_page_region, }; +#[cfg(test)] +use crate::capture; use crate::capture::CaptureError; #[cfg(test)] use crate::capture::RenderedImage; -use crate::draw::Frame; +use crate::draw; +use crate::draw::{Frame, TextMeasurer}; use crate::screen_pixels::{ImagePixelRect, PackedArgb32, ScreenImage}; #[derive(Debug, Clone)] pub(crate) struct CanvasRegionSource { - pub image: std::sync::Arc, + pub image: Arc, /// World-space bounds occupied by the entire captured image. pub logical_bounds: CanvasExportRect, } @@ -67,8 +70,8 @@ impl CanvasRegionSource { let mut right = selection_right; let mut bottom = selection_bottom; - for region in crate::draw::spotlight_regions_for_frame(frame) { - if !crate::draw::spotlight_magnification_is_active(region.magnification) { + for region in draw::spotlight_regions_for_frame(frame) { + if !draw::spotlight_magnification_is_active(region.magnification) { continue; } let region_left = (((region.cx - region.rx.abs() - self.logical_bounds.x) * scale_x) @@ -163,7 +166,7 @@ pub(crate) fn render_canvas_region_pixels( snapshot: CanvasRegionExportSnapshot, ) -> Result { let mut caches = RenderCaches::default(); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let working_selection = snapshot .source .magnifier_working_selection(snapshot.selection, &snapshot.frame) @@ -258,17 +261,19 @@ pub(crate) fn render_canvas_region_pixels( pub(crate) fn render_canvas_region_png( snapshot: CanvasRegionExportSnapshot, ) -> Result { - crate::capture::png::encode_packed_argb32_png(&render_canvas_region_pixels(snapshot)?) + capture::png::encode_packed_argb32_png(&render_canvas_region_pixels(snapshot)?) } #[cfg(test)] mod tests { + use crate::capture::{CutAxis, CutBand}; + use super::*; use crate::draw::{BlurStyle, EraserBrush, EraserKind, FontDescriptor, Frame, RED, Shape}; fn solid_source(width: u32, height: u32, pixel: u32) -> CanvasRegionSource { CanvasRegionSource { - image: std::sync::Arc::new(ScreenImage { + image: Arc::new(ScreenImage { data: (0..width.saturating_mul(height)) .flat_map(|_| pixel.to_ne_bytes()) .collect(), @@ -297,7 +302,7 @@ mod tests { let source = CanvasRegionSource { image: Arc::new(ScreenImage { data: (0..width.saturating_mul(height)) - .flat_map(|_| 0xFFFF_FFFF_u32.to_ne_bytes()) + .flat_map(|_| 0xffff_ffff_u32.to_ne_bytes()) .collect(), width, height, @@ -347,7 +352,7 @@ mod tests { #[test] fn region_renderer_maps_world_shapes_into_native_crop_pixels_and_clips() { - let backdrop = 0xFF20_3040; + let backdrop = 0xff20_3040; let mut frame = Frame::new(); frame.add_shape(Shape::Rect { x: 11, @@ -380,13 +385,13 @@ mod tests { assert_eq!((rendered.width, rendered.height), (8, 8)); assert_eq!(decoded_pixel(&rendered, 0, 0), backdrop); - assert_eq!(decoded_pixel(&rendered, 3, 3), 0xFFFF_0000); + assert_eq!(decoded_pixel(&rendered, 3, 3), 0xffff_0000); assert_eq!(decoded_pixel(&rendered, 7, 7), backdrop); } #[test] fn selected_output_is_cropped_while_shape_mapping_uses_the_full_source() { - let backdrop = 0xFF20_3040; + let backdrop = 0xff20_3040; let mut frame = Frame::new(); frame.add_shape(Shape::Rect { x: 11, @@ -408,16 +413,16 @@ mod tests { .expect("region renders"); assert_eq!((rendered.width, rendered.height), (4, 4)); - assert_eq!(decoded_pixel(&rendered, 1, 1), 0xFFFF_0000); + assert_eq!(decoded_pixel(&rendered, 1, 1), 0xffff_0000); assert_eq!(decoded_pixel(&rendered, 3, 3), backdrop); } #[test] fn empty_frame_preserves_every_selected_screen_pixel() { let source = CanvasRegionSource { - image: std::sync::Arc::new(ScreenImage { + image: Arc::new(ScreenImage { data: (0_u32..16) - .flat_map(|pixel| (0xFF00_0000 | pixel).to_ne_bytes()) + .flat_map(|pixel| (0xff00_0000 | pixel).to_ne_bytes()) .collect(), width: 4, height: 4, @@ -435,16 +440,16 @@ mod tests { }) .expect("raw crop renders"); - assert_eq!(decoded_pixel(&rendered, 0, 0), 0xFF00_0005); - assert_eq!(decoded_pixel(&rendered, 1, 0), 0xFF00_0006); - assert_eq!(decoded_pixel(&rendered, 0, 1), 0xFF00_0009); - assert_eq!(decoded_pixel(&rendered, 1, 1), 0xFF00_000A); + assert_eq!(decoded_pixel(&rendered, 0, 0), 0xff00_0005); + assert_eq!(decoded_pixel(&rendered, 1, 0), 0xff00_0006); + assert_eq!(decoded_pixel(&rendered, 0, 1), 0xff00_0009); + assert_eq!(decoded_pixel(&rendered, 1, 1), 0xff00_000a); } #[test] fn region_capture_magnifies_its_immutable_screen_source() { - let black = 0xFF00_0000u32; - let red = 0xFFFF_0000u32; + let black = 0xff00_0000u32; + let red = 0xffff_0000u32; let mut pixels = vec![black; 8 * 8]; for y in 0..8 { pixels[y * 8 + 2] = red; @@ -485,8 +490,8 @@ mod tests { #[test] fn region_capture_samples_toward_a_spotlight_center_outside_the_crop() { - let black = 0xFF00_0000u32; - let red = 0xFFFF_0000u32; + let black = 0xff00_0000u32; + let red = 0xffff_0000u32; let mut pixels = vec![black; 8 * 8]; for y in 0..8 { pixels[y * 8 + 3] = red; @@ -523,13 +528,13 @@ mod tests { // The sampled coordinate lands three quarters across the red source // texel, so Cairo's bilinear filter produces 75% red over black. - assert_eq!(decoded_pixel(&rendered, 0, 4), 0xFFBF_0000); + assert_eq!(decoded_pixel(&rendered, 0, 4), 0xffbf_0000); } #[test] fn malformed_full_source_fails_closed() { - let mut source = solid_source(8, 8, 0xFFFF_FFFF); - std::sync::Arc::get_mut(&mut source.image) + let mut source = solid_source(8, 8, 0xffff_ffff); + Arc::get_mut(&mut source.image) .expect("test owns source") .data .pop(); @@ -545,7 +550,7 @@ mod tests { #[test] fn eraser_replays_the_original_screen_pixel_at_a_nonzero_world_origin() { - let backdrop = 0xFF20_3040; + let backdrop = 0xff20_3040; let mut frame = Frame::new(); frame.add_shape(Shape::Rect { x: 10, @@ -574,7 +579,7 @@ mod tests { .expect("region renders"); assert_eq!(decoded_pixel(&rendered, 4, 4), backdrop); - assert_eq!(decoded_pixel(&rendered, 1, 1), 0xFFFF_0000); + assert_eq!(decoded_pixel(&rendered, 1, 1), 0xffff_0000); } #[test] @@ -584,14 +589,14 @@ mod tests { let mut data = Vec::with_capacity(width * height * 4); for _y in 0..height { for x in 0..width { - let channel = if (x / 2) % 2 == 0 { 0x10 } else { 0xE0 }; + let channel = if (x / 2) % 2 == 0 { 0x10 } else { 0xe0 }; data.extend_from_slice( - &u32::from_be_bytes([0xFF, channel, channel, channel]).to_ne_bytes(), + &u32::from_be_bytes([0xff, channel, channel, channel]).to_ne_bytes(), ); } } let source = CanvasRegionSource { - image: std::sync::Arc::new(ScreenImage { + image: Arc::new(ScreenImage { data, width: width as u32, height: height as u32, @@ -663,7 +668,7 @@ mod tests { thick: 1.0, }); let snapshot = CanvasRegionExportSnapshot { - source: solid_source(8, 8, 0xFF20_3040), + source: solid_source(8, 8, 0xff20_3040), selection: ImagePixelRect::new(0, 0, 8, 8, (8, 8)).unwrap(), frame, text_halo_enabled: true, @@ -700,21 +705,21 @@ mod tests { thick: 1.0, }); let snapshot = CanvasRegionExportSnapshot { - source: solid_source(8, 8, 0xFF20_3040), + source: solid_source(8, 8, 0xff20_3040), selection: ImagePixelRect::new(0, 0, 8, 8, (8, 8)).unwrap(), frame, text_halo_enabled: true, spotlight: SpotlightPassSnapshot::default(), }; let base = render_canvas_region_pixels(snapshot).expect("flattened"); - let cut = crate::capture::CutBand::new(crate::capture::CutAxis::Columns, 3, 5).unwrap(); - let composed = crate::capture::apply_band_cuts(&base, &[cut]).expect("cut"); + let cut = CutBand::try_new(CutAxis::Columns, 3, 5).unwrap(); + let composed = capture::apply_band_cuts(&base, &[cut]).expect("cut"); assert_eq!((composed.width(), composed.height()), (6, 8)); - assert_eq!(pixel_at(&composed, 2, 0), 0xFFFF_0000); - assert_eq!(pixel_at(&composed, 3, 0), 0xFFFF_0000); - assert_eq!(pixel_at(&base, 2, 0), 0xFFFF_0000); - assert_eq!(pixel_at(&base, 3, 0), 0xFFFF_0000); - assert_eq!(pixel_at(&base, 5, 0), 0xFFFF_0000); + assert_eq!(pixel_at(&composed, 2, 0), 0xffff_0000); + assert_eq!(pixel_at(&composed, 3, 0), 0xffff_0000); + assert_eq!(pixel_at(&base, 2, 0), 0xffff_0000); + assert_eq!(pixel_at(&base, 3, 0), 0xffff_0000); + assert_eq!(pixel_at(&base, 5, 0), 0xffff_0000); } #[test] @@ -731,7 +736,7 @@ mod tests { let pixels = packed_argb32_from_surface(&mut surface, "stride-test").expect("packed"); assert_eq!(pixels.stride(), width * 4); assert_eq!(pixels.data().len(), (width * height * 4) as usize); - assert_eq!(pixel_at(&pixels, 0, 0), 0xFFFF_0000); - assert_eq!(pixel_at(&pixels, 2, 1), 0xFFFF_0000); + assert_eq!(pixel_at(&pixels, 0, 0), 0xffff_0000); + assert_eq!(pixel_at(&pixels, 2, 1), 0xffff_0000); } } diff --git a/src/canvas_export/tests/board_grid.rs b/src/canvas_export/tests/board_grid.rs index c704ed7d2..eda6b89af 100644 --- a/src/canvas_export/tests/board_grid.rs +++ b/src/canvas_export/tests/board_grid.rs @@ -1,3 +1,9 @@ +use std::fs; +use std::fs::File; +use std::io::Cursor; + +use crate::test_temp; + use super::*; use crate::domain::{BoardGrid, BoardGridKind}; use crate::draw::{EraserBrush, EraserKind, Frame, RED, Shape, WHITE}; @@ -47,11 +53,9 @@ fn board_grid_png_erasers_restore_pattern_and_snapshot_is_independent() { }); let png = render_canvas_png(&edited).unwrap(); let mut before = - cairo::ImageSurface::create_from_png(&mut std::io::Cursor::new(&baseline.bytes)) - .unwrap(); + cairo::ImageSurface::create_from_png(&mut Cursor::new(&baseline.bytes)).unwrap(); let mut after = - cairo::ImageSurface::create_from_png(&mut std::io::Cursor::new(&png.bytes)) - .unwrap(); + cairo::ImageSurface::create_from_png(&mut Cursor::new(&png.bytes)).unwrap(); let before = before.data().unwrap(); let after = after.data().unwrap(); for y in 33..53 { @@ -91,7 +95,7 @@ fn board_grid_pdf_stays_vector_without_erasers_and_leaves_margins_plain() { let mut surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 200, 160).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); paint_pdf_page_background(&ctx, &page, 200.0, 160.0); - let backdrop = ExportBackdrop::new(&page.backdrop).unwrap(); + let backdrop = ExportBackdrop::try_new(&page.backdrop).unwrap(); draw_canvas_page_region( &TextMeasurer::default(), &mut RenderCtx::new(&ctx, &mut RenderCaches::default()), @@ -160,10 +164,10 @@ fn board_grid_pdf_stays_vector_without_erasers_and_leaves_margins_plain() { fn check_pdf_pixels(pdf: &[u8], expected: &cairo::ImageSurface) { use std::process::Command; - let folder = crate::test_temp::tempdir().unwrap(); + let folder = test_temp::tempdir().unwrap(); let path = folder.path().join("paper.pdf"); let prefix = folder.path().join("paper"); - std::fs::write(&path, pdf).unwrap(); + fs::write(&path, pdf).unwrap(); let output = Command::new("pdftoppm") .args(["-png", "-r", "72", "-singlefile"]) .arg(&path) @@ -176,7 +180,7 @@ fn check_pdf_pixels(pdf: &[u8], expected: &cairo::ImageSurface) { String::from_utf8_lossy(&output.stderr) ); let mut image = cairo::ImageSurface::create_from_png( - &mut std::fs::File::open(prefix.with_extension("png")).unwrap(), + &mut File::open(prefix.with_extension("png")).unwrap(), ) .unwrap(); assert_eq!( diff --git a/src/canvas_export/tests/cache_tests.rs b/src/canvas_export/tests/cache_tests.rs index ab055c5f2..93cfa69c7 100644 --- a/src/canvas_export/tests/cache_tests.rs +++ b/src/canvas_export/tests/cache_tests.rs @@ -1,5 +1,10 @@ +use std::fs; +use std::fs::File; use std::sync::Arc; +use crate::draw::{FontDescriptor, TextMeasurer}; +use crate::{draw, test_temp}; + use super::page::{ CanvasExportBackdropSnapshot, CanvasPageExportSnapshot, draw_canvas_page_with_measurer, }; @@ -23,7 +28,7 @@ fn pixels(page: &CanvasPageExportSnapshot, caches: &mut RenderCaches) -> Vec { let cairo = cairo::Context::new(&surface).unwrap(); draw_canvas_page_with_measurer( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut RenderCtx::new(&cairo, caches), page, 1.0, @@ -192,9 +197,9 @@ fn pdf_pages_keep_distinct_blurred_backdrops_when_rasterized() { labels: Default::default(), }) .unwrap(); - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("pages.pdf"); - std::fs::write(&path, pdf).unwrap(); + fs::write(&path, pdf).unwrap(); let prefix = temp.path().join("page"); let result = Command::new("pdftoppm") .args(["-png", "-r", "72"]) @@ -208,7 +213,7 @@ fn pdf_pages_keep_distinct_blurred_backdrops_when_rasterized() { String::from_utf8_lossy(&result.stderr) ); for (index, expected) in [(1, 0xffff0000u32), (2, 0xff0000ffu32)] { - let mut file = std::fs::File::open(temp.path().join(format!("page-{index}.png"))).unwrap(); + let mut file = File::open(temp.path().join(format!("page-{index}.png"))).unwrap(); let mut image = cairo::ImageSurface::create_from_png(&mut file).unwrap(); let offset = 10 * image.stride() as usize + 10 * 4; let data = image.data().unwrap(); @@ -256,7 +261,7 @@ fn png_and_region_entries_preserve_embedded_image_and_text_pixels() { x: 42, y: 54, text: "Cache".into(), - color: crate::draw::BLACK, + color: draw::BLACK, size: 24.0, font_descriptor: Default::default(), background_enabled: false, @@ -270,7 +275,7 @@ fn png_and_region_entries_preserve_embedded_image_and_text_pixels() { origin_x: 10, origin_y: 20, }, - backdrop: CanvasExportBackdropSnapshot::Solid(crate::draw::WHITE), + backdrop: CanvasExportBackdropSnapshot::Solid(draw::WHITE), board: BoardExportSnapshot { frame: frame.clone_without_history(), }, @@ -356,16 +361,13 @@ fn mixed_image_and_wrapped_text_pages_reuse_measurement_across_output_scales() { x: 20, y: 35, text: "Page 測試 العربية wrapped text".into(), - color: crate::draw::WHITE, + color: draw::WHITE, size: 16.0, - font_descriptor: crate::draw::FontDescriptor::default(), + font_descriptor: FontDescriptor::default(), background_enabled: false, wrap_width: Some(140), }); - let mut page = page( - frame, - CanvasExportBackdropSnapshot::Solid(crate::draw::WHITE), - ); + let mut page = page(frame, CanvasExportBackdropSnapshot::Solid(draw::WHITE)); page.viewport_width = 220; page.viewport_height = 130; let mut image_only = page.clone(); @@ -374,7 +376,7 @@ fn mixed_image_and_wrapped_text_pages_reuse_measurement_across_output_scales() { .shapes .retain(|shape| matches!(shape.shape, Shape::Image { .. })); let paint = |page: &CanvasPageExportSnapshot, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, caches: &mut RenderCaches, scale: i32| { let mut surface = @@ -391,13 +393,13 @@ fn mixed_image_and_wrapped_text_pages_reuse_measurement_across_output_scales() { } surface.data().unwrap().to_vec() }; - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut caches = RenderCaches::default(); for scale in [1, 2, 1] { let actual = paint(&page, &measurer, &mut caches, scale); let fresh = paint( &page, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut RenderCaches::default(), scale, ); diff --git a/src/capture/band_cut.rs b/src/capture/band_cut.rs index cf2112bc8..bc67e4ae8 100644 --- a/src/capture/band_cut.rs +++ b/src/capture/band_cut.rs @@ -4,6 +4,9 @@ //! preceding cut. The engine is backend-neutral: it does not know about Wayland, //! Cairo, source tokens, or capture destinations. +use std::fmt; +use std::fmt::{Display, Formatter}; + use crate::screen_pixels::PackedArgb32; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -28,8 +31,8 @@ pub(crate) enum BandCutError { InvalidPixels, } -impl std::fmt::Display for BandCutError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for BandCutError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { match self { Self::EmptyRange => formatter.write_str("cut range is empty"), Self::OutOfBounds => formatter.write_str("cut range leaves the current output"), @@ -41,7 +44,7 @@ impl std::fmt::Display for BandCutError { } impl CutBand { - pub(crate) fn new(axis: CutAxis, start: u32, end: u32) -> Result { + pub(crate) fn try_new(axis: CutAxis, start: u32, end: u32) -> Result { if start >= end { return Err(BandCutError::EmptyRange); } @@ -54,7 +57,7 @@ impl CutBand { first: u32, second: u32, ) -> Result { - Self::new(axis, first.min(second), first.max(second)) + Self::try_new(axis, first.min(second), first.max(second)) } pub(crate) const fn axis(self) -> CutAxis { @@ -299,7 +302,7 @@ mod tests { x as u8, y as u8, (y.wrapping_mul(width).wrapping_add(x)) as u8, - 0x80 | ((x + y) as u8 & 0x3F), + 0x80 | ((x + y) as u8 & 0x3f), ]); } } @@ -326,7 +329,7 @@ mod tests { } fn cut(axis: CutAxis, start: u32, end: u32) -> CutBand { - CutBand::new(axis, start, end).expect("valid cut") + CutBand::try_new(axis, start, end).expect("valid cut") } fn reference_one(pixels: &PackedArgb32, cut: CutBand) -> Result { @@ -528,7 +531,7 @@ mod tests { #[test] fn empty_half_open_range_is_rejected() { assert_eq!( - CutBand::new(CutAxis::Columns, 3, 3), + CutBand::try_new(CutAxis::Columns, 3, 3), Err(BandCutError::EmptyRange) ); assert_eq!( @@ -637,7 +640,7 @@ mod tests { }; for start in 0..extent { for end in (start + 1)..=extent { - let Ok(band) = CutBand::new(axis, start, end) else { + let Ok(band) = CutBand::try_new(axis, start, end) else { continue; }; let optimized = apply_band_cuts(&source, &[band]); diff --git a/src/capture/clipboard.rs b/src/capture/clipboard.rs index 24ef18a09..56c2ef3d0 100644 --- a/src/capture/clipboard.rs +++ b/src/capture/clipboard.rs @@ -1,6 +1,12 @@ //! Clipboard integration for copying screenshots. -use super::{dependencies::CaptureClipboard, types::CaptureError}; +use std::fmt::Display; + +use crate::process_broker; +use crate::process_broker::HelperKind; + +use super::dependencies::CaptureClipboard; +use super::types::CaptureError; use crate::process_broker::max_publish_bytes; use std::ffi::OsStr; use std::time::Duration; @@ -117,10 +123,10 @@ where /// Copy to the clipboard through the brokered `wl-copy` helper. fn copy_via_command(image_data: &[u8]) -> Result<(), String> { - let output = crate::process_broker::current() + let output = process_broker::current() .and_then(|broker| { broker.publish( - crate::process_broker::HelperKind::WlCopy, + HelperKind::WlCopy, OsStr::new("wl-copy"), [OsStr::new("--type"), OsStr::new("image/png")], image_data.to_vec(), @@ -145,10 +151,10 @@ fn copy_via_command(image_data: &[u8]) -> Result<(), String> { /// Read the published image back through brokered `wl-paste`. fn read_back_via_command(output_cap: usize) -> Result, String> { - let output = crate::process_broker::current() + let output = process_broker::current() .and_then(|broker| { broker.run( - crate::process_broker::HelperKind::WlPaste, + HelperKind::WlPaste, OsStr::new("wl-paste"), clipboard_image_read_args(), Vec::new(), @@ -179,7 +185,7 @@ fn clipboard_image_read_args() -> [&'static OsStr; 3] { ] } -fn persistence_error(error: impl std::fmt::Display) -> CaptureError { +fn persistence_error(error: impl Display) -> CaptureError { CaptureError::ClipboardError(format!("Could not persist clipboard: {error}")) } diff --git a/src/capture/dependencies.rs b/src/capture/dependencies.rs index 5502c5e66..d05e1f9e8 100644 --- a/src/capture/dependencies.rs +++ b/src/capture/dependencies.rs @@ -1,11 +1,11 @@ -use std::{future::Future, path::PathBuf, pin::Pin, sync::Arc}; - -use crate::capture::{ - clipboard, - file::{self, FileSaveConfig}, - sources, - types::{CaptureError, CaptureType}, -}; +use std::future::Future; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::Arc; + +use crate::capture::file::{self, FileSaveConfig}; +use crate::capture::types::{CaptureError, CaptureType}; +use crate::capture::{clipboard, sources}; /// Abstraction over how image data is captured for the different capture types. pub trait CaptureSource: Send + Sync { diff --git a/src/capture/desktop_backdrop.rs b/src/capture/desktop_backdrop.rs index 3ea9f6495..74cbaa298 100644 --- a/src/capture/desktop_backdrop.rs +++ b/src/capture/desktop_backdrop.rs @@ -2,12 +2,10 @@ use std::sync::Arc; use tokio::task; -use crate::capture::{ - dependencies::CaptureDependencies, - types::{ - CaptureError, CaptureType, DesktopBackdropCaptureRequest, DesktopBackdropCaptureResult, - DesktopBackdropGeometry, - }, +use crate::capture::dependencies::CaptureDependencies; +use crate::capture::types::{ + CaptureError, CaptureType, DesktopBackdropCaptureRequest, DesktopBackdropCaptureResult, + DesktopBackdropGeometry, }; use crate::image_decode::{DecodeLimits, decode_rgba, format_from_mime_or_bytes}; @@ -25,24 +23,23 @@ pub(crate) async fn capture_desktop_backdrop( image_data.len() ); - task::spawn_blocking(move || decode_desktop_backdrop(image_data, request)) + task::spawn_blocking(move || decode_desktop_backdrop(&image_data, &request)) .await .map_err(|err| CaptureError::ImageError(format!("Backdrop decode task failed: {err}")))? } fn decode_desktop_backdrop( - image_data: Vec, - request: DesktopBackdropCaptureRequest, + image_data: &[u8], + request: &DesktopBackdropCaptureRequest, ) -> Result { - let format = format_from_mime_or_bytes("", &image_data).ok_or_else(|| { + let format = format_from_mime_or_bytes("", image_data).ok_or_else(|| { CaptureError::ImageError("Desktop backdrop capture returned an unsupported image".into()) })?; - let decoded = - decode_rgba(format, &image_data, DecodeLimits::SCREEN_CAPTURE).map_err(|err| { - CaptureError::ImageError(format!("Failed to decode desktop backdrop: {err}")) - })?; + let decoded = decode_rgba(format, image_data, DecodeLimits::SCREEN_CAPTURE).map_err(|err| { + CaptureError::ImageError(format!("Failed to decode desktop backdrop: {err}")) + })?; let argb = rgba_to_cairo_argb(&decoded.rgba)?; - desktop_backdrop_from_argb(argb, decoded.width, decoded.height, &request) + desktop_backdrop_from_argb(argb, decoded.width, decoded.height, request) } fn rgba_to_cairo_argb(rgba: &[u8]) -> Result, CaptureError> { diff --git a/src/capture/file.rs b/src/capture/file.rs index b6573b950..cef77159a 100644 --- a/src/capture/file.rs +++ b/src/capture/file.rs @@ -1,5 +1,10 @@ //! File saving functionality for screenshots. +use std::io::ErrorKind; +use std::{io, iter}; + +use crate::{durable_io, paths}; + use super::types::CaptureError; use crate::durable_io::{ AtomicWriteOptions, DurableIoError, OverwriteMode, PermissionPolicy, SymlinkPolicy, @@ -41,6 +46,7 @@ impl Default for FileSaveConfig { /// /// # Returns /// Generated filename with extension +#[must_use] pub fn generate_filename(template: &str, format: &str) -> String { let now = now_local(); let filename = format_with_template(now, template); @@ -55,16 +61,16 @@ fn sanitize_save_extension(format: &str) -> Option { fn save_file_name(template: &str, format: &str) -> Result { let stem = format_with_template(now_local(), template); let extension = sanitize_save_extension(format).ok_or_else(|| { - CaptureError::SaveError(std::io::Error::other("unsupported screenshot file format")) + CaptureError::SaveError(io::Error::other("unsupported screenshot file format")) })?; - if !crate::paths::is_single_path_component(&stem) { - return Err(CaptureError::SaveError(std::io::Error::other( + if !paths::is_single_path_component(&stem) { + return Err(CaptureError::SaveError(io::Error::other( "filename template must expand to a single file name", ))); } let filename = format!("{stem}.{extension}"); - if !crate::paths::is_single_path_component(&filename) { - return Err(CaptureError::SaveError(std::io::Error::other( + if !paths::is_single_path_component(&filename) { + return Err(CaptureError::SaveError(io::Error::other( "filename template must expand to a single file name", ))); } @@ -91,7 +97,7 @@ fn save_to_free_path( .file_stem() .and_then(|stem| stem.to_str()) .ok_or_else(|| { - CaptureError::SaveError(std::io::Error::other( + CaptureError::SaveError(io::Error::other( "filename template must expand to a single file name", )) })?; @@ -99,15 +105,15 @@ fn save_to_free_path( .extension() .and_then(|ext| ext.to_str()) .ok_or_else(|| { - CaptureError::SaveError(std::io::Error::other("unsupported screenshot file format")) + CaptureError::SaveError(io::Error::other("unsupported screenshot file format")) })?; if directory.join(&filename).parent() != Some(directory) { - return Err(CaptureError::SaveError(std::io::Error::other( + return Err(CaptureError::SaveError(io::Error::other( "filename template must expand to a single file name", ))); } - let candidates = std::iter::once(filename.clone()) + let candidates = iter::once(filename.clone()) .chain((1..=UNIQUE_NAME_ATTEMPTS).map(|suffix| format!("{base}-{suffix}.{extension}"))); for candidate in candidates { let path = directory.join(candidate); @@ -123,12 +129,12 @@ fn save_to_free_path( path.display() ); } - Err(err) => return Err(CaptureError::SaveError(std::io::Error::other(err))), + Err(err) => return Err(CaptureError::SaveError(io::Error::other(err))), } } - Err(CaptureError::SaveError(std::io::Error::new( - std::io::ErrorKind::AlreadyExists, + Err(CaptureError::SaveError(io::Error::new( + ErrorKind::AlreadyExists, format!( "{filename} and its {UNIQUE_NAME_ATTEMPTS} numbered alternatives already exist in {}", directory.display() @@ -138,8 +144,8 @@ fn save_to_free_path( /// The error for a save that has no directory to write into. pub(crate) fn no_save_directory_error() -> CaptureError { - CaptureError::SaveError(std::io::Error::new( - std::io::ErrorKind::InvalidInput, + CaptureError::SaveError(io::Error::new( + ErrorKind::InvalidInput, "no save directory configured", )) } @@ -197,7 +203,7 @@ pub fn save_screenshot( path.display(), image_data.len() ); - crate::durable_io::write_atomic( + durable_io::write_atomic( path, image_data, AtomicWriteOptions { @@ -221,12 +227,15 @@ pub fn save_screenshot( } /// Expand tilde (~) in path strings. +#[must_use] pub fn expand_tilde(path: &str) -> PathBuf { expand_tilde_global(path) } #[cfg(test)] mod tests { + use crate::test_temp; + use super::*; #[test] @@ -261,7 +270,7 @@ mod tests { #[test] fn ensure_directory_exists_creates_missing_path() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let target = temp.path().join("nested").join("shots"); let resolved = ensure_directory_exists(&target).expect("ensure_directory_exists"); @@ -285,7 +294,7 @@ mod tests { #[test] fn save_to_free_path_stays_inside_the_save_directory() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let directory = temp.path(); let path = save_to_free_path(directory, "shot", "png", |_| Ok(())).expect("safe name"); assert_eq!(path.parent(), Some(directory)); @@ -298,7 +307,7 @@ mod tests { #[test] fn save_to_free_path_moves_on_when_a_name_is_taken_during_the_write() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let directory = temp.path(); let mut attempts = Vec::new(); @@ -337,14 +346,14 @@ mod tests { let error = save_to(Path::new(""), b"image").expect_err("no directory"); assert!( - matches!(&error, CaptureError::SaveError(io) if io.kind() == std::io::ErrorKind::InvalidInput), + matches!(&error, CaptureError::SaveError(io) if io.kind() == ErrorKind::InvalidInput), "unexpected error: {error}" ); } #[test] fn save_screenshot_takes_the_next_free_suffix() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let directory = temp.path().canonicalize().unwrap(); fs::write(directory.join("shot.png"), b"first").unwrap(); @@ -357,7 +366,7 @@ mod tests { #[test] fn save_screenshot_fails_instead_of_overwriting_when_every_name_is_taken() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let directory = temp.path().canonicalize().unwrap(); fs::write(directory.join("shot.png"), b"kept").unwrap(); for suffix in 1..=UNIQUE_NAME_ATTEMPTS { @@ -367,7 +376,7 @@ mod tests { let error = save_to(&directory, b"new").expect_err("no free name"); assert!( - matches!(&error, CaptureError::SaveError(io) if io.kind() == std::io::ErrorKind::AlreadyExists), + matches!(&error, CaptureError::SaveError(io) if io.kind() == ErrorKind::AlreadyExists), "unexpected error: {error}" ); assert_eq!(fs::read(directory.join("shot.png")).unwrap(), b"kept"); diff --git a/src/capture/manager.rs b/src/capture/manager.rs index fb23ccb13..30c0cb0d7 100644 --- a/src/capture/manager.rs +++ b/src/capture/manager.rs @@ -1,25 +1,25 @@ -use std::fmt; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, TrySendError}; +use std::{error, fmt, sync}; +use tokio::runtime::Handle; +#[cfg(test)] +use tokio::runtime::Runtime; use tokio::sync::mpsc; - -use crate::capture::{ - dependencies::CaptureDependencies, - desktop_backdrop::capture_desktop_backdrop, - file::FileSaveConfig, - pipeline::{ - CaptureManagerRequest, CaptureManagerResult, CaptureRequest, deliver_document, - deliver_image, perform_capture, render_and_deliver_document, render_and_deliver_image, - render_image, - }, - types::{ - CaptureDestination, CaptureError, CaptureOutcome, CaptureType, - DesktopBackdropCaptureRequest, DocumentDeliveryRequest, ImageDeliveryRequest, - ImageOperationKind, RenderImageRequest, RenderedDocumentDeliveryRequest, - RenderedImageDeliveryRequest, - }, +use tokio::task::JoinHandle; + +use crate::capture::dependencies::CaptureDependencies; +use crate::capture::desktop_backdrop::capture_desktop_backdrop; +use crate::capture::file::FileSaveConfig; +use crate::capture::pipeline::{ + CaptureManagerRequest, CaptureManagerResult, CaptureRequest, deliver_document, deliver_image, + perform_capture, render_and_deliver_document, render_and_deliver_image, render_image, +}; +use crate::capture::types::{ + CaptureDestination, CaptureError, CaptureOutcome, CaptureType, DesktopBackdropCaptureRequest, + DocumentDeliveryRequest, ImageDeliveryRequest, ImageOperationKind, RenderImageRequest, + RenderedDocumentDeliveryRequest, RenderedImageDeliveryRequest, }; type CompletionNotifier = Arc; @@ -65,7 +65,7 @@ impl fmt::Display for CaptureSubmitError { } } -impl std::error::Error for CaptureSubmitError {} +impl error::Error for CaptureSubmitError {} /// Non-blocking observation of the unique capture completion owner. #[derive(Debug)] @@ -116,12 +116,13 @@ pub struct CaptureManager { healthy: bool, terminal_reported: bool, shutdown_requested: Arc, - worker: Option>, + worker: Option>, } impl CaptureManager { /// Creates a manager whose owner polls completions directly. - pub fn new(runtime_handle: &tokio::runtime::Handle) -> Self { + #[must_use] + pub fn new(runtime_handle: &Handle) -> Self { Self::with_dependencies_and_notifier( runtime_handle, CaptureDependencies::default(), @@ -132,14 +133,14 @@ impl CaptureManager { /// Creates a manager with custom dependencies for deterministic consumers. #[cfg(test)] pub(crate) fn with_dependencies( - runtime_handle: &tokio::runtime::Handle, + runtime_handle: &Handle, dependencies: CaptureDependencies, ) -> Self { Self::with_dependencies_and_notifier(runtime_handle, dependencies, Arc::new(|| {})) } pub(crate) fn with_completion_notifier( - runtime_handle: &tokio::runtime::Handle, + runtime_handle: &Handle, notifier: impl Fn() + Send + Sync + 'static, ) -> Self { Self::with_dependencies_and_notifier( @@ -151,7 +152,7 @@ impl CaptureManager { #[cfg(test)] pub(crate) fn with_dependencies_and_test_notifier( - runtime_handle: &tokio::runtime::Handle, + runtime_handle: &Handle, dependencies: CaptureDependencies, notifier: impl Fn() + Send + Sync + 'static, ) -> Self { @@ -159,12 +160,12 @@ impl CaptureManager { } fn with_dependencies_and_notifier( - runtime_handle: &tokio::runtime::Handle, + runtime_handle: &Handle, dependencies: CaptureDependencies, notifier: CompletionNotifier, ) -> Self { let (request_tx, request_rx) = mpsc::channel::(1); - let (completion_tx, completion_rx) = std::sync::mpsc::sync_channel(1); + let (completion_tx, completion_rx) = sync::mpsc::sync_channel(1); let shutdown_requested = Arc::new(AtomicBool::new(false)); let worker = runtime_handle.spawn(run_capture_worker( request_rx, @@ -523,7 +524,7 @@ impl Drop for CaptureWorkerExitGuard { #[cfg(test)] impl CaptureManager { pub(crate) fn with_closed_channel_for_test() -> Self { - let runtime = tokio::runtime::Runtime::new().expect("test runtime"); + let runtime = Runtime::new().expect("test runtime"); let mut manager = Self::new(runtime.handle()); manager.request_tx.take(); if let Some(worker) = manager.worker.take() { @@ -537,6 +538,8 @@ impl CaptureManager { #[cfg(test)] mod transport_tests { + use std::sync::atomic::AtomicUsize; + use super::*; fn request() -> CaptureManagerRequest { @@ -560,7 +563,7 @@ mod transport_tests { SyncSender, ) { let (request_tx, request_rx) = mpsc::channel(1); - let (completion_tx, completion_rx) = std::sync::mpsc::sync_channel(1); + let (completion_tx, completion_rx) = sync::mpsc::sync_channel(1); let manager = CaptureManager { request_tx: Some(request_tx), completion_rx, @@ -755,8 +758,8 @@ mod transport_tests { #[test] fn completion_channel_is_capacity_one_and_notifier_follows_publication() { - let (completion_tx, completion_rx) = std::sync::mpsc::sync_channel(1); - let notifications = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let (completion_tx, completion_rx) = sync::mpsc::sync_channel(1); + let notifications = Arc::new(AtomicUsize::new(0)); let notified = Arc::clone(¬ifications); let shutdown = Arc::new(AtomicBool::new(true)); let guard = CaptureWorkerExitGuard::new( diff --git a/src/capture/pipeline.rs b/src/capture/pipeline.rs index 5608fb392..8804a43db 100644 --- a/src/capture/pipeline.rs +++ b/src/capture/pipeline.rs @@ -1,15 +1,16 @@ -use std::{fmt, path::PathBuf, sync::Arc}; - -use crate::capture::{ - dependencies::{CaptureClipboard, CaptureDependencies, CaptureFileSaver}, - file::{FileSaveConfig, no_save_directory_error}, - types::{ - CaptureDestination, CaptureError, CaptureResult, CaptureType, - DesktopBackdropCaptureRequest, DesktopBackdropCaptureResult, DocumentDeliveryRequest, - ImageDeliveryRequest, ImageOperationKind, RenderImageRequest, - RenderedDocumentDeliveryRequest, RenderedImage, RenderedImageDeliveryRequest, - }, +use std::fmt; +use std::path::PathBuf; +use std::sync::Arc; + +use crate::capture::dependencies::{CaptureClipboard, CaptureDependencies, CaptureFileSaver}; +use crate::capture::file::{FileSaveConfig, no_save_directory_error}; +use crate::capture::types::{ + CaptureDestination, CaptureError, CaptureResult, CaptureType, DesktopBackdropCaptureRequest, + DesktopBackdropCaptureResult, DocumentDeliveryRequest, ImageDeliveryRequest, + ImageOperationKind, RenderImageRequest, RenderedDocumentDeliveryRequest, RenderedImage, + RenderedImageDeliveryRequest, }; +use crate::process_broker; use tokio::task; #[derive(Clone)] @@ -417,7 +418,7 @@ async fn copy_to_clipboard(clipboard: Arc, image_data: Vec super::clipboard::copy_to_clipboard_with( &image_data, clipboard.as_ref(), - crate::process_broker::max_publish_bytes(), + process_broker::max_publish_bytes(), ) }) .await diff --git a/src/capture/png.rs b/src/capture/png.rs index 055499083..14272ec99 100644 --- a/src/capture/png.rs +++ b/src/capture/png.rs @@ -48,7 +48,7 @@ mod tests { #[test] fn packed_argb32_png_preserves_alpha_and_premultiplied_channels() { let source_data = native_argb(&[ - 0xFF00_00FF, // opaque blue + 0xff00_00ff, // opaque blue 0x0000_0000, // fully transparent 0x8040_4040, // half-alpha premultiplied grey ]); diff --git a/src/capture/portal.rs b/src/capture/portal.rs index 302391934..08603844d 100644 --- a/src/capture/portal.rs +++ b/src/capture/portal.rs @@ -1,5 +1,12 @@ //! xdg-desktop-portal integration for screenshot capture. +use tokio::time; +use tokio::time::Instant; +use zbus::fdo; +use zbus::zvariant::Value; + +use crate::zbus_stream; + use super::types::{CaptureError, CaptureType}; use std::collections::HashMap; use std::future::Future; @@ -44,8 +51,8 @@ trait Screenshot { async fn screenshot( &self, parent_window: &str, - options: HashMap>, - ) -> zbus::Result; + options: HashMap>, + ) -> zbus::Result; } /// D-Bus proxy for org.freedesktop.portal.Request interface. @@ -69,7 +76,7 @@ trait Request { struct PortalAttempt { label: &'static str, - options: HashMap>, + options: HashMap>, } const PORTAL_RESULT_URI_KEY: &str = "uri"; @@ -145,12 +152,12 @@ fn portal_attempts(capture_type: CaptureType) -> Vec { async fn capture_once( connection: &Connection, proxy: &ScreenshotProxy<'_>, - mut options: HashMap>, + mut options: HashMap>, ) -> Result { // The overlay stays hidden until this returns, so a portal that accepts // the request and never answers must not keep it hidden for good. let response_timeout = response_timeout(&options); - let deadline = tokio::time::Instant::now() + response_timeout; + let deadline = Instant::now() + response_timeout; let handle_token = next_handle_token()?; let request_path = portal_request_path(connection, &handle_token)?; @@ -197,7 +204,7 @@ async fn capture_once( within_deadline( deadline, response_timeout, - crate::zbus_stream::next(&mut response_stream), + zbus_stream::next(&mut response_stream), close, ) .await? @@ -220,7 +227,7 @@ async fn capture_once( within_deadline( deadline, response_timeout, - crate::zbus_stream::next(&mut returned_response_stream), + zbus_stream::next(&mut returned_response_stream), close, ) .await? @@ -241,7 +248,7 @@ async fn capture_once( } /// The time a request may wait for the portal's answer. -fn response_timeout(options: &HashMap>) -> Duration { +fn response_timeout(options: &HashMap>) -> Duration { let interactive = options.get(PORTAL_OPTION_INTERACTIVE_KEY) == Some(&zbus::zvariant::Value::from(true)); if interactive { @@ -256,12 +263,12 @@ fn response_timeout(options: &HashMap>) - /// On expiry the request is closed, so its dialog does not outlive the /// capture, and the capture fails instead of waiting forever. async fn within_deadline( - deadline: tokio::time::Instant, + deadline: Instant, limit: Duration, step: impl Future, close: impl Future, ) -> Result { - match tokio::time::timeout_at(deadline, step).await { + match time::timeout_at(deadline, step).await { Ok(value) => Ok(value), Err(_) => { log::warn!( @@ -286,7 +293,7 @@ async fn close_request(connection: &Connection, path: OwnedObjectPath) { .await }; - match tokio::time::timeout(PORTAL_CLOSE_TIMEOUT, close).await { + match time::timeout(PORTAL_CLOSE_TIMEOUT, close).await { Ok(Ok(())) => log::debug!("Closed the unanswered screenshot portal request"), Ok(Err(error)) => log::warn!("Could not close the screenshot portal request: {error}"), Err(_) => log::warn!("Closing the screenshot portal request timed out"), @@ -382,7 +389,7 @@ fn parse_response( fn map_portal_call_error(err: zbus::Error) -> CaptureError { let error_name = match &err { zbus::Error::MethodError(name, _, _) => Some(name.as_str()), - zbus::Error::FDO(fdo_error) if matches!(**fdo_error, zbus::fdo::Error::AccessDenied(_)) => { + zbus::Error::FDO(fdo_error) if matches!(**fdo_error, fdo::Error::AccessDenied(_)) => { Some(DBUS_ERROR_ACCESS_DENIED) } _ => None, @@ -405,9 +412,7 @@ fn map_portal_call_error(err: zbus::Error) -> CaptureError { } /// Build portal options based on capture type. -fn build_portal_options( - capture_type: CaptureType, -) -> HashMap> { +fn build_portal_options(capture_type: CaptureType) -> HashMap> { let mut options = HashMap::new(); match capture_type { @@ -427,7 +432,7 @@ fn build_portal_options( } /// Active-window capture options (user picks window interactively). -fn build_active_window_interactive_options() -> HashMap> { +fn build_active_window_interactive_options() -> HashMap> { let mut options = HashMap::new(); options.insert(PORTAL_OPTION_INTERACTIVE_KEY.to_string(), true.into()); options @@ -448,6 +453,11 @@ pub async fn is_portal_available() -> bool { #[cfg(test)] mod tests { + use std::future; + use std::sync::atomic::{AtomicBool, Ordering}; + + use crate::capture::CaptureFailureKind; + use super::*; #[test] @@ -575,40 +585,34 @@ mod tests { #[tokio::test] async fn an_unanswered_portal_request_is_closed_and_fails() { let limit = Duration::from_millis(20); - let closed = std::sync::atomic::AtomicBool::new(false); + let closed = AtomicBool::new(false); let result = within_deadline( - tokio::time::Instant::now() + limit, + Instant::now() + limit, limit, - std::future::pending::<()>(), - async { closed.store(true, std::sync::atomic::Ordering::SeqCst) }, + future::pending::<()>(), + async { closed.store(true, Ordering::SeqCst) }, ) .await; let error = result.expect_err("a silent portal must not be awaited forever"); assert!(matches!(error, CaptureError::PortalTimeout(_)), "{error}"); - assert_eq!( - error.failure_kind(), - crate::capture::CaptureFailureKind::PortalError - ); - assert!(closed.load(std::sync::atomic::Ordering::SeqCst)); + assert_eq!(error.failure_kind(), CaptureFailureKind::PortalError); + assert!(closed.load(Ordering::SeqCst)); } #[tokio::test] async fn an_answered_portal_request_is_left_open() { let limit = Duration::from_secs(5); - let closed = std::sync::atomic::AtomicBool::new(false); + let closed = AtomicBool::new(false); - let result = within_deadline( - tokio::time::Instant::now() + limit, - limit, - std::future::ready(7), - async { closed.store(true, std::sync::atomic::Ordering::SeqCst) }, - ) + let result = within_deadline(Instant::now() + limit, limit, future::ready(7), async { + closed.store(true, Ordering::SeqCst) + }) .await; assert_eq!(result.expect("answered in time"), 7); - assert!(!closed.load(std::sync::atomic::Ordering::SeqCst)); + assert!(!closed.load(Ordering::SeqCst)); } #[test] @@ -616,10 +620,7 @@ mod tests { let error = parse_response(2, &HashMap::new()).expect_err("response 2 must fail"); assert!(matches!(error, CaptureError::PortalResponse(2))); - assert_eq!( - error.failure_kind(), - crate::capture::CaptureFailureKind::PortalError - ); + assert_eq!(error.failure_kind(), CaptureFailureKind::PortalError); } fn method_error(name: &str, detail: &str) -> zbus::Error { diff --git a/src/capture/portal/tests/transport.rs b/src/capture/portal/tests/transport.rs index 719aabb0f..bda8ab678 100644 --- a/src/capture/portal/tests/transport.rs +++ b/src/capture/portal/tests/transport.rs @@ -1,12 +1,14 @@ //! Real subscription transactions on an owned private session bus. +use tokio::sync::mpsc; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tokio::task; + use super::*; use std::io::{BufRead, BufReader}; use std::process::{Child, Command, Stdio}; -use std::sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, -}; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; use zbus::message::Header; struct PrivateBus { @@ -55,7 +57,7 @@ enum ResponseMode { struct ScreenshotFixture { mode: ResponseMode, - requested: tokio::sync::mpsc::UnboundedSender, + requested: UnboundedSender, closed: Arc, } @@ -78,7 +80,7 @@ impl ScreenshotFixture { mut options: HashMap, #[zbus(header)] header: Header<'_>, #[zbus(connection)] connection: &Connection, - ) -> zbus::fdo::Result { + ) -> fdo::Result { let token = String::try_from(options.remove(PORTAL_OPTION_HANDLE_TOKEN_KEY).unwrap()).unwrap(); let sender = header @@ -102,7 +104,7 @@ impl ScreenshotFixture { }, ) .await - .map_err(|error| zbus::fdo::Error::Failed(error.to_string()))?; + .map_err(|error| fdo::Error::Failed(error.to_string()))?; if matches!(self.mode, ResponseMode::Immediate) { emit_response(connection, &path, 0).await; } @@ -138,10 +140,10 @@ async fn fixture( ) -> ( Connection, Connection, - tokio::sync::mpsc::UnboundedReceiver, + UnboundedReceiver, Arc, ) { - let (requested, receiver) = tokio::sync::mpsc::unbounded_channel(); + let (requested, receiver) = mpsc::unbounded_channel(); let closed = Arc::new(AtomicUsize::new(0)); let service = zbus::connection::Builder::address(bus.address.as_str()) .unwrap() @@ -184,7 +186,7 @@ async fn wait_for_response_subscription( client: &Connection, path: &OwnedObjectPath, ) { - let deadline = tokio::time::Instant::now() + Duration::from_secs(2); + let deadline = Instant::now() + Duration::from_secs(2); loop { let reply = service .call_method( @@ -208,10 +210,10 @@ async fn wait_for_response_subscription( return; } assert!( - tokio::time::Instant::now() < deadline, + Instant::now() < deadline, "client did not subscribe at {path}" ); - tokio::task::yield_now().await; + task::yield_now().await; } } @@ -219,7 +221,7 @@ async fn wait_for_response_subscription( async fn portal_transport_immediate_response_before_method_reply_is_not_lost() { let bus = PrivateBus::start(); let (_service, client, _requests, closed) = fixture(&bus, ResponseMode::Immediate).await; - let result = tokio::time::timeout(Duration::from_secs(2), capture(client)) + let result = time::timeout(Duration::from_secs(2), capture(client)) .await .unwrap() .unwrap(); @@ -234,7 +236,7 @@ async fn deferred_case(mode: ResponseMode, code: u32) -> Result Result<(Vec, u32, u32), CaptureError> { - let decoder = png::Decoder::new(std::io::Cursor::new(data)); + let decoder = png::Decoder::new(Cursor::new(data)); let mut reader = decoder .read_info() .map_err(|e| CaptureError::ImageError(format!("Decode error: {}", e)))?; @@ -27,7 +29,7 @@ pub fn decode_image_to_argb(data: &[u8]) -> Result<(Vec, u32, u32), CaptureE png::ColorType::Rgb => Box::new(buf[..info.buffer_size()].as_chunks::<3>().0.iter().map( |c| { let [r, g, b] = *c; - (r, g, b, 0xFF) + (r, g, b, 0xff) }, )), other => { diff --git a/src/capture/sources/hyprland.rs b/src/capture/sources/hyprland.rs index d4feb5b61..6a725b260 100644 --- a/src/capture/sources/hyprland.rs +++ b/src/capture/sources/hyprland.rs @@ -1,7 +1,7 @@ use tokio::task; use crate::capture::types::CaptureError; -use crate::process_broker::{HelperKind, current}; +use crate::process_broker::{BrokerOutput, HelperKind, current}; use std::ffi::OsStr; use std::time::Duration; @@ -19,7 +19,7 @@ fn run_helper( arguments: &[&str], timeout: Duration, output_cap: usize, -) -> Result { +) -> Result { current() .and_then(|broker| { broker.run( @@ -256,6 +256,8 @@ fn active_window_geometry(json: &serde_json::Value) -> Result Result bool { fn allowed_portal_temp_roots() -> Vec { let mut roots = Vec::new(); - if let Some(runtime) = std::env::var_os(XDG_RUNTIME_DIR_ENV) + if let Some(runtime) = env::var_os(XDG_RUNTIME_DIR_ENV) && !runtime.is_empty() { roots.push(PathBuf::from(runtime)); } - if let Some(cache) = crate::paths::cache_dir() { + if let Some(cache) = paths::cache_dir() { roots.push(cache.join("xdg-desktop-portal")); } roots @@ -129,7 +133,7 @@ enum PortalOpenError { fn open_portal_image(path: &Path, max_bytes: u64) -> Result { let file = open_portal_file(path).map_err(|err| { - if err.kind() == std::io::ErrorKind::NotFound { + if err.kind() == ErrorKind::NotFound { PortalOpenError::NotReady(err.to_string()) } else { PortalOpenError::Rejected { @@ -163,7 +167,7 @@ fn open_portal_image(path: &Path, max_bytes: u64) -> Result std::io::Result { +fn open_portal_file(path: &Path) -> io::Result { use std::fs::OpenOptions; use std::os::unix::fs::OpenOptionsExt; @@ -184,6 +188,10 @@ fn decode_file_uri(uri: &str) -> Result { #[cfg(test)] mod tests { + use std::os::unix; + + use crate::test_env; + use super::*; use crate::env_vars::XDG_RUNTIME_DIR_ENV; use crate::test_temp::TempDir; @@ -198,7 +206,7 @@ mod tests { } fn with_runtime_dir(dir: &Path, body: impl FnOnce() -> T) -> T { - crate::test_env::with_env_var(XDG_RUNTIME_DIR_ENV, Some(dir.as_os_str()), body) + test_env::with_env_var(XDG_RUNTIME_DIR_ENV, Some(dir.as_os_str()), body) } #[test] @@ -206,7 +214,7 @@ mod tests { let temp = TempDir::new().unwrap(); with_runtime_dir(temp.path(), || { let file_path = temp.path().join("capture file.png"); - std::fs::write(&file_path, b"portal-bytes").unwrap(); + fs::write(&file_path, b"portal-bytes").unwrap(); let data = read_image_from_uri(&file_uri_for(&file_path)).expect("read succeeds"); assert_eq!(data, b"portal-bytes"); @@ -221,11 +229,11 @@ mod tests { fn reads_and_leaves_files_outside_portal_temp_roots() { let temp = TempDir::new().unwrap(); let outside = temp.path().join("Pictures"); - std::fs::create_dir(&outside).unwrap(); + fs::create_dir(&outside).unwrap(); let file_path = outside.join("shot.png"); - std::fs::write(&file_path, b"portal-bytes").unwrap(); + fs::write(&file_path, b"portal-bytes").unwrap(); let trusted = temp.path().join("runtime"); - std::fs::create_dir(&trusted).unwrap(); + fs::create_dir(&trusted).unwrap(); with_runtime_dir(&trusted, || { let data = read_image_from_uri(&file_uri_for(&file_path)).expect("read succeeds"); @@ -242,7 +250,7 @@ mod tests { let temp = TempDir::new().unwrap(); with_runtime_dir(temp.path(), || { let file_path = temp.path().join("huge.png"); - std::fs::write(&file_path, b"12345").unwrap(); + fs::write(&file_path, b"12345").unwrap(); let err = read_image_from_uri_with_limit(&file_uri_for(&file_path), 4) .expect_err("oversize file"); match err { @@ -262,11 +270,11 @@ mod tests { fn oversized_files_outside_temp_roots_are_not_deleted() { let temp = TempDir::new().unwrap(); let outside = temp.path().join("Pictures"); - std::fs::create_dir(&outside).unwrap(); + fs::create_dir(&outside).unwrap(); let file_path = outside.join("huge.png"); - std::fs::write(&file_path, b"12345").unwrap(); + fs::write(&file_path, b"12345").unwrap(); let trusted = temp.path().join("runtime"); - std::fs::create_dir(&trusted).unwrap(); + fs::create_dir(&trusted).unwrap(); with_runtime_dir(&trusted, || { let err = read_image_from_uri_with_limit(&file_uri_for(&file_path), 4) @@ -290,9 +298,9 @@ mod tests { let temp = TempDir::new().unwrap(); with_runtime_dir(temp.path(), || { let target = temp.path().join("target.png"); - std::fs::write(&target, b"secret").unwrap(); + fs::write(&target, b"secret").unwrap(); let link = temp.path().join("link.png"); - std::os::unix::fs::symlink(&target, &link).unwrap(); + unix::fs::symlink(&target, &link).unwrap(); read_image_from_uri(&file_uri_for(&link)).expect_err("symlink"); assert!(target.exists()); @@ -316,11 +324,11 @@ mod tests { fn deletable_portal_path_requires_runtime_or_cache_root() { let temp = TempDir::new().unwrap(); let file_path = temp.path().join("shot.png"); - std::fs::write(&file_path, b"x").unwrap(); + fs::write(&file_path, b"x").unwrap(); with_runtime_dir(temp.path(), || { assert!(is_deletable_portal_temp_path(&file_path)); }); - crate::test_env::with_env_var(XDG_RUNTIME_DIR_ENV, None, || { + test_env::with_env_var(XDG_RUNTIME_DIR_ENV, None, || { assert!(!is_deletable_portal_temp_path(&file_path)); }); } diff --git a/src/capture/tests/desktop_backdrop.rs b/src/capture/tests/desktop_backdrop.rs index badf9b410..cec94e198 100644 --- a/src/capture/tests/desktop_backdrop.rs +++ b/src/capture/tests/desktop_backdrop.rs @@ -1,6 +1,7 @@ +use crate::capture::desktop_backdrop::desktop_backdrop_from_argb; use crate::capture::{ DesktopBackdropCaptureRequest, DesktopBackdropGeometry, DesktopBackdropOutputGeometry, - ImageOperationKind, desktop_backdrop::desktop_backdrop_from_argb, + ImageOperationKind, }; fn request( diff --git a/src/capture/tests/fixtures.rs b/src/capture/tests/fixtures.rs index 4145cbea4..4002eadbf 100644 --- a/src/capture/tests/fixtures.rs +++ b/src/capture/tests/fixtures.rs @@ -1,15 +1,12 @@ -use std::{ - future::Future, - path::PathBuf, - pin::Pin, - sync::{Arc, Mutex}, -}; +use std::future::Future; +use std::io; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; -use crate::capture::{ - dependencies::{CaptureClipboard, CaptureFileSaver, CaptureSource}, - file::FileSaveConfig, - types::{CaptureError, CaptureType}, -}; +use crate::capture::dependencies::{CaptureClipboard, CaptureFileSaver, CaptureSource}; +use crate::capture::file::FileSaveConfig; +use crate::capture::types::{CaptureError, CaptureType}; #[derive(Clone)] pub(super) struct MockSource { @@ -45,9 +42,7 @@ impl CaptureFileSaver for MockSaver { fn save(&self, _image_data: &[u8], _config: &FileSaveConfig) -> Result { *self.calls.lock().unwrap() += 1; if self.should_fail { - Err(CaptureError::SaveError(std::io::Error::other( - "save failed", - ))) + Err(CaptureError::SaveError(io::Error::other("save failed"))) } else { Ok(self.path.clone()) } diff --git a/src/capture/tests/manager.rs b/src/capture/tests/manager.rs index 50f32a5eb..335e4aa74 100644 --- a/src/capture/tests/manager.rs +++ b/src/capture/tests/manager.rs @@ -1,22 +1,25 @@ -use std::{ - os::fd::AsRawFd, - path::PathBuf, - sync::{ - Arc, Mutex, - atomic::{AtomicUsize, Ordering}, - }, -}; - +use std::os::fd::AsRawFd; +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, mpsc}; +use std::thread; + +use tokio::runtime::Handle; +use tokio::sync::Notify; +use tokio::task; use tokio::time::{Duration, sleep}; +use crate::backend::wayland::RuntimeWakeSource; +use crate::capture::dependencies::{CaptureDependencies, CaptureFuture, CaptureSource}; +use crate::capture::file::FileSaveConfig; +use crate::capture::manager::{CaptureManager, CapturePoll, CaptureSubmitError}; +use crate::capture::types::{ + CaptureDestination, CaptureError, CaptureFailureKind, CaptureOutcome, CaptureType, +}; use crate::capture::{ DesktopBackdropCaptureRequest, DesktopBackdropGeometry, DocumentDeliveryRequest, ImageDeliveryRequest, ImageFormatMetadata, ImageOperationKind, RenderImageRequest, RenderedDocument, RenderedImage, - dependencies::{CaptureDependencies, CaptureFuture, CaptureSource}, - file::FileSaveConfig, - manager::{CaptureManager, CapturePoll, CaptureSubmitError}, - types::{CaptureDestination, CaptureError, CaptureFailureKind, CaptureOutcome, CaptureType}, }; use super::fixtures::{MockClipboard, MockSaver, MockSource, create_placeholder_image}; @@ -24,8 +27,8 @@ use super::fixtures::{MockClipboard, MockSaver, MockSource, create_placeholder_i struct PanicSource; struct GatedSource { - started: Arc, - release: Arc, + started: Arc, + release: Arc, } impl CaptureSource for PanicSource { @@ -105,13 +108,10 @@ async fn completion_is_observable_when_the_notifier_runs() { }; let notifications = Arc::new(AtomicUsize::new(0)); let notified = Arc::clone(¬ifications); - let mut manager = CaptureManager::with_dependencies_and_test_notifier( - &tokio::runtime::Handle::current(), - deps, - move || { + let mut manager = + CaptureManager::with_dependencies_and_test_notifier(&Handle::current(), deps, move || { notified.fetch_add(1, Ordering::Release); - }, - ); + }); let accepted = manager .request_capture( CaptureType::FullScreen, @@ -131,8 +131,8 @@ async fn completion_is_observable_when_the_notifier_runs() { #[tokio::test(flavor = "multi_thread")] async fn completion_wakes_an_already_blocked_runtime_poll_after_publication() { - let started = Arc::new(tokio::sync::Notify::new()); - let release = Arc::new(tokio::sync::Notify::new()); + let started = Arc::new(Notify::new()); + let release = Arc::new(Notify::new()); let deps = CaptureDependencies { source: Arc::new(GatedSource { started: Arc::clone(&started), @@ -148,15 +148,12 @@ async fn completion_wakes_an_already_blocked_runtime_poll_after_publication() { calls: Arc::new(Mutex::new(0)), }), }; - let runtime_wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let runtime_wake = RuntimeWakeSource::try_new().unwrap(); let wake_handle = runtime_wake.handle(); - let mut manager = CaptureManager::with_dependencies_and_test_notifier( - &tokio::runtime::Handle::current(), - deps, - move || { + let mut manager = + CaptureManager::with_dependencies_and_test_notifier(&Handle::current(), deps, move || { wake_handle.wake().unwrap(); - }, - ); + }); let accepted = manager .request_capture( CaptureType::FullScreen, @@ -167,8 +164,8 @@ async fn completion_wakes_an_already_blocked_runtime_poll_after_publication() { started.notified().await; let wake_fd = runtime_wake.poll_fd().as_raw_fd(); - let (polling_tx, polling_rx) = std::sync::mpsc::sync_channel(0); - let poller = std::thread::spawn(move || { + let (polling_tx, polling_rx) = mpsc::sync_channel(0); + let poller = thread::spawn(move || { let mut pollfd = libc::pollfd { fd: wake_fd, events: libc::POLLIN, @@ -207,13 +204,10 @@ async fn worker_panic_wakes_and_reports_the_active_operation_once() { }; let notifications = Arc::new(AtomicUsize::new(0)); let notified = Arc::clone(¬ifications); - let mut manager = CaptureManager::with_dependencies_and_test_notifier( - &tokio::runtime::Handle::current(), - deps, - move || { + let mut manager = + CaptureManager::with_dependencies_and_test_notifier(&Handle::current(), deps, move || { notified.fetch_add(1, Ordering::Release); - }, - ); + }); let accepted = manager .request_capture( CaptureType::FullScreen, @@ -241,7 +235,7 @@ async fn normal_shutdown_does_not_publish_a_failure_notification() { let notifications = Arc::new(AtomicUsize::new(0)); let notified = Arc::clone(¬ifications); let mut manager = CaptureManager::with_dependencies_and_test_notifier( - &tokio::runtime::Handle::current(), + &Handle::current(), CaptureDependencies::default(), move || { notified.fetch_add(1, Ordering::Release); @@ -249,7 +243,7 @@ async fn normal_shutdown_does_not_publish_a_failure_notification() { ); manager.shutdown(); - tokio::task::yield_now().await; + task::yield_now().await; assert_eq!(notifications.load(Ordering::Acquire), 0); assert!(matches!(manager.poll(), CapturePoll::Idle)); @@ -277,8 +271,7 @@ async fn test_capture_manager_with_dependencies() { saver: Arc::new(saver), clipboard: Arc::new(clipboard), }; - let mut manager = - CaptureManager::with_dependencies(&tokio::runtime::Handle::current(), deps.clone()); + let mut manager = CaptureManager::with_dependencies(&Handle::current(), deps.clone()); manager .request_capture( @@ -337,8 +330,7 @@ async fn capture_manager_records_failure_status() { saver: Arc::new(saver), clipboard: Arc::new(clipboard), }; - let mut manager = - CaptureManager::with_dependencies(&tokio::runtime::Handle::current(), deps.clone()); + let mut manager = CaptureManager::with_dependencies(&Handle::current(), deps.clone()); manager .request_capture( @@ -385,7 +377,7 @@ async fn capture_manager_preserves_user_cancellation_as_a_terminal_outcome() { calls: Arc::new(Mutex::new(0)), }), }; - let mut manager = CaptureManager::with_dependencies(&tokio::runtime::Handle::current(), deps); + let mut manager = CaptureManager::with_dependencies(&Handle::current(), deps); manager .request_capture( CaptureType::FullScreen, @@ -422,7 +414,7 @@ async fn desktop_backdrop_completion_releases_the_manager_for_pdf_delivery() { calls: Arc::new(Mutex::new(0)), }), }; - let mut manager = CaptureManager::with_dependencies(&tokio::runtime::Handle::current(), deps); + let mut manager = CaptureManager::with_dependencies(&Handle::current(), deps); let backdrop_id = manager .request_desktop_backdrop_capture(DesktopBackdropCaptureRequest { logical_width: 100, @@ -489,7 +481,7 @@ async fn request_image_delivery_queues_manager_backed_path() { saver: Arc::new(saver), clipboard: Arc::new(clipboard), }; - let mut manager = CaptureManager::with_dependencies(&tokio::runtime::Handle::current(), deps); + let mut manager = CaptureManager::with_dependencies(&Handle::current(), deps); manager .request_image_delivery(ImageDeliveryRequest { @@ -549,7 +541,7 @@ async fn request_document_delivery_reports_board_pdf_success() { saver: Arc::new(saver), clipboard: Arc::new(clipboard), }; - let mut manager = CaptureManager::with_dependencies(&tokio::runtime::Handle::current(), deps); + let mut manager = CaptureManager::with_dependencies(&Handle::current(), deps); manager .request_document_delivery(DocumentDeliveryRequest { @@ -598,7 +590,7 @@ async fn request_image_delivery_records_canvas_save_failure() { saver: Arc::new(saver), clipboard: Arc::new(clipboard), }; - let mut manager = CaptureManager::with_dependencies(&tokio::runtime::Handle::current(), deps); + let mut manager = CaptureManager::with_dependencies(&Handle::current(), deps); manager .request_image_delivery(ImageDeliveryRequest { @@ -655,7 +647,7 @@ async fn request_document_delivery_records_board_pdf_save_failure() { saver: Arc::new(saver), clipboard: Arc::new(clipboard), }; - let mut manager = CaptureManager::with_dependencies(&tokio::runtime::Handle::current(), deps); + let mut manager = CaptureManager::with_dependencies(&Handle::current(), deps); manager .request_document_delivery(DocumentDeliveryRequest { @@ -707,7 +699,7 @@ async fn request_image_delivery_preserves_clipboard_success_when_file_fails() { saver: Arc::new(saver), clipboard: Arc::new(clipboard), }; - let mut manager = CaptureManager::with_dependencies(&tokio::runtime::Handle::current(), deps); + let mut manager = CaptureManager::with_dependencies(&Handle::current(), deps); manager .request_image_delivery(ImageDeliveryRequest { @@ -735,7 +727,7 @@ async fn request_image_delivery_preserves_clipboard_success_when_file_fails() { #[tokio::test] async fn request_render_image_returns_the_rendered_image_without_delivery() { - let mut manager = CaptureManager::new(&tokio::runtime::Handle::current()); + let mut manager = CaptureManager::new(&Handle::current()); manager .request_render_image(RenderImageRequest { @@ -756,7 +748,7 @@ async fn request_render_image_returns_the_rendered_image_without_delivery() { #[tokio::test] async fn request_render_image_reports_render_failure_and_keeps_the_manager_healthy() { - let mut manager = CaptureManager::new(&tokio::runtime::Handle::current()); + let mut manager = CaptureManager::new(&Handle::current()); manager .request_render_image(RenderImageRequest { diff --git a/src/capture/tests/perform_capture.rs b/src/capture/tests/perform_capture.rs index 37f72ea3d..37e3e9be0 100644 --- a/src/capture/tests/perform_capture.rs +++ b/src/capture/tests/perform_capture.rs @@ -1,17 +1,13 @@ -use std::{ - path::PathBuf, - sync::{Arc, Mutex}, -}; - -use crate::capture::{ - dependencies::{CaptureClipboard, CaptureDependencies, CaptureFileSaver}, - file::FileSaveConfig, - pipeline::{CaptureRequest, deliver_document, deliver_image, perform_capture}, - types::{ - CaptureDestination, CaptureError, CaptureResult, CaptureType, DocumentDeliveryRequest, - ImageDeliveryRequest, ImageFormatMetadata, ImageOperationKind, RenderedDocument, - RenderedImage, - }, +use std::io; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +use crate::capture::dependencies::{CaptureClipboard, CaptureDependencies, CaptureFileSaver}; +use crate::capture::file::FileSaveConfig; +use crate::capture::pipeline::{CaptureRequest, deliver_document, deliver_image, perform_capture}; +use crate::capture::types::{ + CaptureDestination, CaptureError, CaptureResult, CaptureType, DocumentDeliveryRequest, + ImageDeliveryRequest, ImageFormatMetadata, ImageOperationKind, RenderedDocument, RenderedImage, }; use super::fixtures::{MockClipboard, MockSaver, MockSource}; @@ -29,9 +25,7 @@ impl CaptureFileSaver for RecordingSaver { *self.calls.lock().unwrap() += 1; self.configs.lock().unwrap().push(config.clone()); if self.should_fail { - Err(CaptureError::SaveError(std::io::Error::other( - "save failed", - ))) + Err(CaptureError::SaveError(io::Error::other("save failed"))) } else { Ok(self.path.clone()) } diff --git a/src/capture/types.rs b/src/capture/types.rs index 43b498800..7a4267fed 100644 --- a/src/capture/types.rs +++ b/src/capture/types.rs @@ -1,8 +1,13 @@ //! Data types for screenshot capture functionality. +use std::time::Duration; +use std::{error, io}; + +use crate::capture::file::FileSaveConfig; use crate::i18n::tr; +use std::fmt; use std::path::PathBuf; -use std::{fmt, sync::Arc}; +use std::sync::Arc; /// User-facing operation kind for image delivery and status labels. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -14,6 +19,7 @@ pub enum ImageOperationKind { } impl ImageOperationKind { + #[must_use] pub fn success_title(self) -> &'static str { match self { Self::Screenshot => tr("Screenshot Captured"), @@ -23,6 +29,7 @@ impl ImageOperationKind { } } + #[must_use] pub fn failure_title(self) -> &'static str { match self { Self::Screenshot => tr("Screenshot Failed"), @@ -32,6 +39,7 @@ impl ImageOperationKind { } } + #[must_use] pub fn save_failure_title(self) -> &'static str { match self { Self::Screenshot => tr("Screenshot File Not Saved"), @@ -41,6 +49,7 @@ impl ImageOperationKind { } } + #[must_use] pub fn clipboard_failure_title(self) -> &'static str { match self { Self::Screenshot => tr("Screenshot Clipboard Failed"), @@ -50,6 +59,7 @@ impl ImageOperationKind { } } + #[must_use] pub fn fallback_toast(self) -> &'static str { match self { Self::Screenshot => tr("Clipboard failed"), @@ -59,6 +69,7 @@ impl ImageOperationKind { } } + #[must_use] pub fn saved_log_label(self) -> &'static str { match self { Self::Screenshot => "Screenshot", @@ -68,6 +79,7 @@ impl ImageOperationKind { } } + #[must_use] pub fn format_error(self, err: &CaptureError) -> String { match self { Self::Screenshot => err.to_string(), @@ -121,6 +133,7 @@ pub struct ImageFormatMetadata { } impl ImageFormatMetadata { + #[must_use] pub fn png() -> Self { Self { extension: "png".to_string(), @@ -141,7 +154,7 @@ pub struct RenderedImage { pub struct ImageDeliveryRequest { pub image: RenderedImage, pub destination: CaptureDestination, - pub save_config: Option, + pub save_config: Option, pub operation: ImageOperationKind, pub fallback_format_override: Option, } @@ -165,7 +178,7 @@ pub type DocumentRenderJob = Box Result, + pub save_config: Option, pub operation: ImageOperationKind, pub fallback_format_override: Option, } @@ -175,7 +188,7 @@ pub struct RenderedImageDeliveryRequest { pub struct RenderedDocumentDeliveryRequest { pub render: DocumentRenderJob, pub destination: CaptureDestination, - pub save_config: Option, + pub save_config: Option, pub operation: ImageOperationKind, } @@ -190,7 +203,7 @@ pub struct RenderedDocument { pub struct DocumentDeliveryRequest { pub document: RenderedDocument, pub destination: CaptureDestination, - pub save_config: Option, + pub save_config: Option, pub operation: ImageOperationKind, } @@ -209,6 +222,7 @@ pub struct DesktopBackdropGeometry { } impl DesktopBackdropGeometry { + #[must_use] pub fn from_outputs( active: DesktopBackdropOutputGeometry, outputs: &[DesktopBackdropOutputGeometry], @@ -235,16 +249,19 @@ impl DesktopBackdropGeometry { }) } + #[must_use] pub fn verified_physical_size(self) -> Option<(u32, u32)> { let width = self.physical_width?; let height = self.physical_height?; (width > 0 && height > 0).then_some((width, height)) } + #[must_use] pub fn physical_origin(self) -> Option<(u32, u32)> { Some((self.crop_x?, self.crop_y?)) } + #[must_use] pub fn screenshot_size(self) -> Option<(u32, u32)> { let width = self.screenshot_width?; let height = self.screenshot_height?; @@ -440,6 +457,7 @@ impl CaptureResult { /// the image either, so the caller has to offer another way to keep it. /// /// A file-only delivery never asked for the clipboard, so it never counts. + #[must_use] pub fn clipboard_copy_failed(&self) -> bool { let clipboard_requested = matches!( self.destination, @@ -507,7 +525,7 @@ pub enum CaptureError { #[cfg(feature = "dbus")] DBusError(zbus::Error), - SaveError(std::io::Error), + SaveError(io::Error), ClipboardError(String), @@ -529,7 +547,7 @@ pub enum CaptureError { /// The screenshot portal did not answer its request in time. #[cfg_attr(not(feature = "portal"), allow(dead_code))] - PortalTimeout(std::time::Duration), + PortalTimeout(Duration), /// The compositor fast path failed, and so did the portal fallback. FallbackFailed { @@ -540,6 +558,7 @@ pub enum CaptureError { impl CaptureError { /// What kind of failure this is, for choosing the user-facing explanation. + #[must_use] pub fn failure_kind(&self) -> CaptureFailureKind { match self { Self::MissingTool { tool, .. } => CaptureFailureKind::MissingTool(tool), @@ -590,8 +609,8 @@ impl fmt::Display for CaptureError { } } -impl std::error::Error for CaptureError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { +impl error::Error for CaptureError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { match self { #[cfg(feature = "dbus")] Self::DBusError(err) => Some(err), @@ -608,8 +627,8 @@ impl From for CaptureError { } } -impl From for CaptureError { - fn from(value: std::io::Error) -> Self { +impl From for CaptureError { + fn from(value: io::Error) -> Self { Self::SaveError(value) } } diff --git a/src/capture/window_geometry/mod.rs b/src/capture/window_geometry/mod.rs index 422417281..1961b02bf 100644 --- a/src/capture/window_geometry/mod.rs +++ b/src/capture/window_geometry/mod.rs @@ -3,8 +3,10 @@ //! Queries are synchronous because the process broker is synchronous. Callers //! must run [`query_window_targets`] from an off-event-loop worker. +use std::env; use std::ffi::OsStr; +use crate::env_vars; use crate::process_broker::ProcessBroker; mod geometry; @@ -59,7 +61,7 @@ pub(crate) fn detect_backend_from_env( /// Detect the current compositor from its session environment marker. pub(crate) fn detect_backend() -> Option { - let hyprland = std::env::var_os(crate::env_vars::HYPRLAND_INSTANCE_SIGNATURE_ENV); - let sway = std::env::var_os(crate::env_vars::SWAYSOCK_ENV); + let hyprland = env::var_os(env_vars::HYPRLAND_INSTANCE_SIGNATURE_ENV); + let sway = env::var_os(env_vars::SWAYSOCK_ENV); detect_backend_from_env(hyprland.as_deref(), sway.as_deref()) } diff --git a/src/capture/window_geometry/types.rs b/src/capture/window_geometry/types.rs index 979960581..15afd7575 100644 --- a/src/capture/window_geometry/types.rs +++ b/src/capture/window_geometry/types.rs @@ -1,3 +1,6 @@ +use std::fmt::{Display, Formatter}; +use std::{error, fmt}; + use crate::util::Rect; use super::WindowGeometryBackend; @@ -44,8 +47,8 @@ pub(crate) enum WindowGeometryError { }, } -impl std::fmt::Display for WindowGeometryError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for WindowGeometryError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { match self { Self::Unsupported => formatter.write_str("window snapping is unavailable"), Self::Broker(message) => write!(formatter, "window geometry broker failed: {message}"), @@ -59,4 +62,4 @@ impl std::fmt::Display for WindowGeometryError { } } -impl std::error::Error for WindowGeometryError {} +impl error::Error for WindowGeometryError {} diff --git a/src/cli.rs b/src/cli.rs index b25d71144..d58b8d8ee 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,8 +1,10 @@ +use std::env; use std::ffi::OsString; use std::path::PathBuf; use crate::domain::Language; use crate::tray_action::TrayAction; +use crate::{build_info, i18n, runtime_capabilities}; #[derive(Debug, Default)] pub struct Cli { @@ -95,7 +97,7 @@ pub(crate) enum CliOutcome { impl Cli { pub(crate) fn parse() -> Result { - Self::try_parse_from(std::env::args_os()) + Self::try_parse_from(env::args_os()) } pub(crate) fn try_parse_from(args: I) -> Result @@ -164,7 +166,7 @@ impl Cli { cli.language = Some(parse_language(&value_after(&args, index, "--language")?)?); } "--list-languages" => cli.list_languages = true, - crate::runtime_capabilities::RUNTIME_CAPABILITIES_FLAG => { + runtime_capabilities::RUNTIME_CAPABILITIES_FLAG => { cli.runtime_capabilities = true; } _ if arg.starts_with("--daemon-action=") => { @@ -518,7 +520,7 @@ impl Cli { /// Parses a `--language` value: any spelling `[ui] language` accepts. fn parse_language(value: &str) -> Result { - crate::i18n::language_from_code(value).ok_or_else(|| { + i18n::language_from_code(value).ok_or_else(|| { format!("unknown language '{value}'; run 'wayscriber --list-languages' to see the choices") }) } @@ -653,7 +655,7 @@ pub(crate) fn print_help() { } pub(crate) fn print_version() { - println!("wayscriber {}", crate::build_info::version()); + println!("wayscriber {}", build_info::version()); } #[cfg(test)] diff --git a/src/clipboard_text.rs b/src/clipboard_text.rs index 28c7a74f0..1fc027f99 100644 --- a/src/clipboard_text.rs +++ b/src/clipboard_text.rs @@ -7,16 +7,19 @@ use std::ffi::OsStr; use std::time::Duration; +use crate::process_broker; +use crate::process_broker::HelperKind; + pub(crate) enum ClipboardTextError { Empty, Other(String), } pub(crate) fn copy_text_via_command(text: &str) -> Result<(), String> { - let output = crate::process_broker::current() + let output = process_broker::current() .and_then(|broker| { broker.publish( - crate::process_broker::HelperKind::WlCopy, + HelperKind::WlCopy, OsStr::new("wl-copy"), [OsStr::new("--type"), OsStr::new("text/plain;charset=utf-8")], text.as_bytes().to_vec(), @@ -39,10 +42,10 @@ pub(crate) fn copy_text_via_command(text: &str) -> Result<(), String> { } pub(crate) fn read_clipboard_text_via_command() -> Result { - let output = crate::process_broker::current() + let output = process_broker::current() .and_then(|broker| { broker.run( - crate::process_broker::HelperKind::WlPaste, + HelperKind::WlPaste, OsStr::new("wl-paste"), clipboard_text_read_args(), Vec::new(), diff --git a/src/config/action_meta/entries/tools.rs b/src/config/action_meta/entries/tools.rs index 2b895fb07..bc080964d 100644 --- a/src/config/action_meta/entries/tools.rs +++ b/src/config/action_meta/entries/tools.rs @@ -1,3 +1,5 @@ +use crate::config::action_meta::ActionIcon; + use super::ActionMeta; pub const ENTRIES: &[ActionMeta] = &[ @@ -10,7 +12,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::Text + icon: ActionIcon::Text ), meta!( EnterStickyNoteMode, @@ -21,7 +23,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::StickyNote + icon: ActionIcon::StickyNote ), meta!( SelectSelectionTool, @@ -32,7 +34,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::Select + icon: ActionIcon::Select ), meta!( SelectPenTool, @@ -43,7 +45,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::Pen + icon: ActionIcon::Pen ), meta!( SelectLiveShapeTool, @@ -64,7 +66,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::Line + icon: ActionIcon::Line ), meta!( SelectRectTool, @@ -75,7 +77,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::Rect + icon: ActionIcon::Rect ), meta!( SelectEllipseTool, @@ -86,7 +88,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::Ellipse + icon: ActionIcon::Ellipse ), meta!( SelectTriangleTool, @@ -127,7 +129,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::FreeformPolygon + icon: ActionIcon::FreeformPolygon ), meta!( SelectFreeformPolygonTool, @@ -148,7 +150,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::Arrow + icon: ActionIcon::Arrow ), meta!( SelectBlurTool, @@ -159,7 +161,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::Blur + icon: ActionIcon::Blur ), meta!( SelectHighlightTool, @@ -208,7 +210,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::Marker + icon: ActionIcon::Marker ), meta!( SelectStepMarkerTool, @@ -219,7 +221,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::StepMarker + icon: ActionIcon::StepMarker ), meta!( SelectEraserTool, @@ -230,7 +232,7 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true, true, - icon: crate::config::action_meta::ActionIcon::Eraser + icon: ActionIcon::Eraser ), meta!( ToggleEraserMode, diff --git a/src/config/action_meta/mod.rs b/src/config/action_meta/mod.rs index 3b79d11f9..5fd770429 100644 --- a/src/config/action_meta/mod.rs +++ b/src/config/action_meta/mod.rs @@ -1,3 +1,5 @@ +use crate::i18n; + use super::Action; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -51,6 +53,7 @@ pub struct ActionMeta { } impl ActionMeta { + #[must_use] pub fn short_label(self) -> &'static str { self.short_label.unwrap_or(self.label) } @@ -66,8 +69,8 @@ macro_rules! meta { $in_palette:expr, $in_help:expr, $in_toolbar:expr, - $aliases:expr, - icon: $icon:expr + $aliases:expr,icon: + $icon:expr ) => { ActionMeta { action: crate::config::Action::$action, @@ -90,8 +93,8 @@ macro_rules! meta { $category:ident, $in_palette:expr, $in_help:expr, - $in_toolbar:expr, - icon: $icon:expr + $in_toolbar:expr,icon: + $icon:expr ) => { ActionMeta { action: crate::config::Action::$action, @@ -177,6 +180,7 @@ pub fn action_meta_iter() -> impl Iterator { .flat_map(|entries| entries.iter()) } +#[must_use] pub fn action_meta(action: Action) -> Option<&'static ActionMeta> { action_meta_iter().find(|meta| meta.action == action) } @@ -189,12 +193,14 @@ fn source_label(action: Action) -> &'static str { .unwrap_or("Action") } +#[must_use] pub fn action_label(action: Action) -> &'static str { - crate::i18n::tr(source_label(action)) + i18n::tr(source_label(action)) } +#[must_use] pub fn action_short_label(action: Action) -> &'static str { - crate::i18n::tr( + i18n::tr( action_meta(action) .map(|meta| meta.short_label()) .unwrap_or("Action"), @@ -205,26 +211,28 @@ pub fn action_short_label(action: Action) -> &'static str { /// /// Derivation happens on the English source label so the suffixes this UI /// abbreviates stay English; only the result is translated. +#[must_use] pub fn action_display_label(action: Action) -> &'static str { if matches!(action, Action::SelectEllipseTool) { return action_short_label(action); } let label = source_label(action); if let Some(stripped) = label.strip_suffix(" Tool") { - return crate::i18n::tr(stripped); + return i18n::tr(stripped); } if let Some(stripped) = label.strip_suffix(" Mode") { - return crate::i18n::tr(stripped); + return i18n::tr(stripped); } if let Some(stripped) = label.strip_prefix("Toggle ") { - return crate::i18n::tr(stripped); + return i18n::tr(stripped); } action_label(action) } #[allow(dead_code)] +#[must_use] pub fn action_description(action: Action) -> &'static str { - crate::i18n::tr( + i18n::tr( action_meta(action) .map(|meta| meta.description) .unwrap_or(""), diff --git a/src/config/action_meta/tests.rs b/src/config/action_meta/tests.rs index e5164cf9a..ca3a142a7 100644 --- a/src/config/action_meta/tests.rs +++ b/src/config/action_meta/tests.rs @@ -1,3 +1,6 @@ +use crate::config::action_meta; +use crate::config::keybindings::KeybindingsConfig; + use super::*; use std::collections::HashSet; @@ -361,12 +364,11 @@ fn action_meta_covers_surface_actions() { /// therefore tells the user their shortcut collided with "Action". #[test] fn every_configurable_action_has_a_name_to_show() { - let unnamed: Vec = - crate::config::keybindings::KeybindingsConfig::configurable_actions() - .iter() - .copied() - .filter(|action| action_meta(*action).is_none()) - .collect(); + let unnamed: Vec = KeybindingsConfig::configurable_actions() + .iter() + .copied() + .filter(|action| action_meta(*action).is_none()) + .collect(); assert!( unnamed.is_empty(), "these configurable actions would be shown to the user as \"Action\": {unnamed:?}" @@ -446,7 +448,7 @@ fn meta_macro_arms_default_aliases_and_icon() { false, false, false, - icon: crate::config::action_meta::ActionIcon::Undo + icon: action_meta::ActionIcon::Undo ); const FULL: ActionMeta = meta!( Undo, @@ -458,7 +460,7 @@ fn meta_macro_arms_default_aliases_and_icon() { false, false, &["back"], - icon: crate::config::action_meta::ActionIcon::Undo + icon: action_meta::ActionIcon::Undo ); assert!(PLAIN.icon.is_none() && PLAIN.search_aliases.is_empty()); diff --git a/src/config/core.rs b/src/config/core.rs index 1c7800d42..e7f763fa8 100644 --- a/src/config/core.rs +++ b/src/config/core.rs @@ -203,6 +203,7 @@ impl Config { } /// Which `[keybindings]` keys the source this configuration came from spelled out. + #[must_use] pub fn keybinding_authorship(&self) -> &KeybindingAuthorship { &self.keybinding_authorship } @@ -219,6 +220,7 @@ impl Config { } } + #[must_use] pub fn resolved_boards(&self) -> BoardsConfig { match &self.boards { Some(boards) if !boards.items.is_empty() => boards.clone(), diff --git a/src/config/document.rs b/src/config/document.rs index e807a5a7c..2d354f374 100644 --- a/src/config/document.rs +++ b/src/config/document.rs @@ -3,6 +3,8 @@ mod lock; mod merge; +use std::error; + pub use lock::ConfigWriteLockTimeout; use super::io::{ @@ -17,10 +19,9 @@ use crate::durable_io::{ use anyhow::{Context, Result, bail}; use std::collections::BTreeSet; use std::ffi::OsString; -use std::fmt; -use std::fs; use std::io::ErrorKind; use std::path::{Component, Path, PathBuf}; +use std::{fmt, fs}; use toml_edit::DocumentMut; use lock::{CONFIG_WRITE_LOCK_TIMEOUT, acquire_config_write_lock}; @@ -48,7 +49,7 @@ impl fmt::Display for ConfigSourceChanged { } } -impl std::error::Error for ConfigSourceChanged {} +impl error::Error for ConfigSourceChanged {} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigDiagnosticKind { @@ -77,10 +78,12 @@ pub struct ConfigDiagnostic { } impl ConfigDiagnostic { + #[must_use] pub fn kind(&self) -> ConfigDiagnosticKind { self.kind } + #[must_use] pub fn path(&self) -> &str { &self.path } @@ -475,7 +478,7 @@ enum RepairScope { impl ConfigDocument { pub fn load() -> Result { - Self::load_from_path(Config::get_config_path()?) + Self::load_from_path(Config::config_path()?) } pub fn load_from_path(path: impl Into) -> Result { @@ -495,7 +498,7 @@ impl ConfigDocument { /// The returned warning describes what could not be used. Saving either /// fallback remains revision-guarded and creates a backup first. pub fn load_for_editing() -> Result<(Self, Option)> { - Self::load_for_editing_from_path(Config::get_config_path()?) + Self::load_for_editing_from_path(Config::config_path()?) } pub fn load_for_editing_from_path(path: impl Into) -> Result<(Self, Option)> { @@ -509,7 +512,7 @@ impl ConfigDocument { Err(error) if revision.bytes().is_some() => { let document = revision .bytes() - .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .and_then(|bytes| str::from_utf8(bytes).ok()) .and_then(|input| input.parse::().ok()) .unwrap_or_default(); Ok(( @@ -546,7 +549,7 @@ impl ConfigDocument { fn read(source_path: PathBuf, revision: SourceRevision) -> Result { match revision.bytes() { Some(bytes) => { - let input = std::str::from_utf8(bytes).with_context(|| { + let input = str::from_utf8(bytes).with_context(|| { format!("Config at {} is not valid UTF-8", source_path.display()) })?; let document = input.parse::().with_context(|| { @@ -597,6 +600,7 @@ impl ConfigDocument { }) } + #[must_use] pub fn config(&self) -> &Config { &self.config } @@ -604,20 +608,24 @@ impl ConfigDocument { /// The parsed configuration before validation, exactly as the source spells /// it out. [`Self::config`] is the same values after the running session's /// clamps and shortcut resolution. + #[must_use] pub fn authored_config(&self) -> &Config { &self.authored_config } /// Which `[keybindings]` keys the source spells out. A migration preview /// needs this to tell a shortcut the user wrote from one serde filled in. + #[must_use] pub fn keybinding_authorship(&self) -> &KeybindingAuthorship { &self.authored_config.keybinding_authorship } + #[must_use] pub fn source(&self) -> &ConfigSource { &self.source } + #[must_use] pub fn source_path(&self) -> &Path { &self.source_path } @@ -632,10 +640,12 @@ impl ConfigDocument { /// `config.toml` a link into one profile among several, the path is the /// same for every file it can name, and retargeting it makes the next load /// a different configuration entirely. + #[must_use] pub fn destination(&self) -> &Path { self.revision.destination() } + #[must_use] pub fn diagnostics(&self) -> &[ConfigDiagnostic] { &self.diagnostics } @@ -643,6 +653,7 @@ impl ConfigDocument { /// Entries of the file that did not map and hold their defaults in /// [`Self::config`]. A save replaces exactly these with the draft's /// values; everything else in the file stays as authored. + #[must_use] pub fn section_errors(&self) -> &[ConfigSectionError] { match &self.repair { RepairScope::Unreadable(errors) => errors, @@ -914,14 +925,17 @@ pub struct ConfigDocumentSaveOutcome { } impl ConfigDocumentSaveOutcome { + #[must_use] pub fn document(&self) -> &ConfigDocument { &self.document } + #[must_use] pub fn backup_path(&self) -> Option<&Path> { self.backup_path.as_deref() } + #[must_use] pub fn into_parts(self) -> (ConfigDocument, Option) { (self.document, self.backup_path) } diff --git a/src/config/document/lock.rs b/src/config/document/lock.rs index 92c5d69c4..0d782e4c0 100644 --- a/src/config/document/lock.rs +++ b/src/config/document/lock.rs @@ -19,11 +19,16 @@ //! one. It is created on the first write and left in place; it carries no //! contents and is never read. +use std::fmt::{Display, Formatter}; +use std::{error, fmt, thread}; + use anyhow::{Context, Result, anyhow}; use std::fs::{File, OpenOptions}; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; +use crate::durable_io; + /// How long a save waits for another writer's window to close. /// /// The guarded window is a byte comparison, a file copy, and a rename — @@ -50,8 +55,8 @@ pub struct ConfigWriteLockTimeout { pub waited: Duration, } -impl std::fmt::Display for ConfigWriteLockTimeout { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for ConfigWriteLockTimeout { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { write!( formatter, "another editor is writing {} (waited {:?} for the config write lock)", @@ -61,7 +66,7 @@ impl std::fmt::Display for ConfigWriteLockTimeout { } } -impl std::error::Error for ConfigWriteLockTimeout {} +impl error::Error for ConfigWriteLockTimeout {} /// The lock file guarding one config file's writes. /// @@ -94,7 +99,7 @@ impl Drop for ConfigWriteLock { // Closing the descriptor would release it anyway; unlocking first // says so at the point where the window ends. A failure here leaves // the close to do it, so there is nothing for the caller to act on. - let _ = crate::durable_io::unlock(file); + let _ = durable_io::unlock(file); } } } @@ -115,7 +120,7 @@ pub(super) fn acquire_at(path: &Path, timeout: Duration) -> Result return Ok(ConfigWriteLock { file: Some(file) }), Err(error) if error.kind() == ErrorKind::WouldBlock => { let waited = started.elapsed(); @@ -125,7 +130,7 @@ pub(super) fn acquire_at(path: &Path, timeout: Duration) -> Result continue, Err(error) => { @@ -163,6 +168,8 @@ fn open_lock_file(path: &Path) -> Result { #[cfg(test)] mod tests { + use crate::test_temp; + use super::*; /// A lock somebody else is holding is waited for, then reported — never @@ -170,7 +177,7 @@ mod tests { #[test] #[cfg(unix)] fn a_held_lock_is_reported_as_a_timeout_rather_than_taken() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let destination = temp.path().join("config.toml"); let path = config_write_lock_path(&destination).expect("a lock path"); diff --git a/src/config/enums.rs b/src/config/enums.rs index 765d00e90..1141a2b25 100644 --- a/src/config/enums.rs +++ b/src/config/enums.rs @@ -1,6 +1,8 @@ //! Configuration enum types. -use crate::domain::{Color, color::*}; +use crate::domain::Color; +use crate::domain::color::PALETTE_RED; +use crate::util; use crate::util::ConfigHexColorError; use log::warn; use serde::{Deserialize, Serialize}; @@ -94,6 +96,7 @@ pub enum ReducedMotion { impl ReducedMotion { /// Whether UI animations should run. + #[must_use] pub fn motion_enabled(self) -> bool { !matches!(self, ReducedMotion::On) } @@ -142,15 +145,17 @@ impl ColorSpec { /// names and invalid hex values default to the tuned palette red with a warning. /// Three-component RGB arrays are converted from 0-255 range to 0.0-1.0 range with /// full opacity; four-component arrays take their alpha from the fourth. + #[must_use] pub fn to_color(&self) -> Color { match self { - ColorSpec::Name(name) => match crate::util::parse_config_hex_color(name) { + ColorSpec::Name(name) => match util::parse_config_hex_color(name) { Ok(color) => color, - Err(ConfigHexColorError::MissingHash) => crate::util::name_to_color(name) - .unwrap_or_else(|| { + Err(ConfigHexColorError::MissingHash) => { + util::name_to_color(name).unwrap_or_else(|| { warn!("Unknown color '{}', using red", name); PALETTE_RED - }), + }) + } Err(err) => { warn!("Invalid hex color '{}': {:?}; using red", name, err); PALETTE_RED diff --git a/src/config/field_metadata.rs b/src/config/field_metadata.rs index 79ab2f743..bd1eaf530 100644 --- a/src/config/field_metadata.rs +++ b/src/config/field_metadata.rs @@ -40,6 +40,7 @@ pub enum ScalarConstraint { } impl ScalarConstraint { + #[must_use] pub const fn accepts_u32(self, value: u32) -> bool { match self { Self::Boolean => false, @@ -57,6 +58,7 @@ impl ScalarConstraint { } } + #[must_use] pub const fn unsigned_range(self) -> Option<(u32, u32)> { match self { Self::Unsigned { min, max } => Some((min, max)), @@ -64,6 +66,7 @@ impl ScalarConstraint { } } + #[must_use] pub const fn unsigned_choices(self) -> Option<&'static [u32]> { match self { Self::UnsignedChoice(values) => Some(values), @@ -143,6 +146,7 @@ pub const PERFORMANCE_FIELD_METADATA: &[PerformanceFieldMetadata] = &[ PERFORMANCE_UI_ANIMATION_FPS, ]; +#[must_use] pub fn performance_field_metadata(id: PerformanceFieldId) -> &'static PerformanceFieldMetadata { match id { PerformanceFieldId::BufferCount => &PERFORMANCE_BUFFER_COUNT, diff --git a/src/config/io.rs b/src/config/io.rs index 91cd3a247..e4b95b1e6 100644 --- a/src/config/io.rs +++ b/src/config/io.rs @@ -1,11 +1,15 @@ -use super::ColorSpec; +use std::fmt::{Display, Formatter}; +use std::{error, fmt}; + +use crate::durable_io; + use super::action_meta::action_label; use super::keybindings::{Action, Shortcut}; use super::paths::primary_config_dir; use super::salvage::deserialize_salvaging; use super::types::{PRESET_SLOTS_MAX, ToolPresetConfig}; use super::validate::ConfigValidationReport; -use super::{Config, ConfigDocument}; +use super::{ColorSpec, Config, ConfigDocument}; use crate::domain::Color; use crate::durable_io::{ AtomicWriteOptions, DestinationExpectation, DurableIoError, FileIdentity, OverwriteMode, @@ -59,6 +63,7 @@ pub struct ConfigSectionError { impl ConfigSectionError { /// Whether this entry is `section` itself or lies inside it, so a check /// for `session` also sees a failed `session.storage`. + #[must_use] pub fn is_within(&self, section: &str) -> bool { self.section .strip_prefix(section) @@ -66,8 +71,8 @@ impl ConfigSectionError { } } -impl std::fmt::Display for ConfigSectionError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for ConfigSectionError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { write!(formatter, "[{}] {}", self.section, self.error) } } @@ -92,6 +97,7 @@ impl LoadedConfig { /// their section is partly unreadable — destructive commands that would /// act on default paths, or policies that fail closed — check this instead /// of trusting the defaulted values. + #[must_use] pub fn section_failed(&self, section: &str) -> bool { self.section_errors .iter() @@ -106,13 +112,13 @@ impl Config { /// /// # Errors /// Returns an error if the config directory cannot be determined (e.g., HOME not set). - pub fn get_config_path() -> Result { + pub fn config_path() -> Result { Ok(primary_config_dir()?.join("config.toml")) } /// Determines the directory containing the active configuration file based on the source. pub fn config_directory_from_source(_source: &ConfigSource) -> Result { - let path = Self::get_config_path()?; + let path = Self::config_path()?; path.parent() .map(PathBuf::from) .ok_or_else(|| anyhow!("Config path {} has no parent directory", path.display())) @@ -148,7 +154,7 @@ impl Config { let primary_path = primary_config_dir()?.join("config.toml"); let (config_path, source) = if primary_path.exists() { - (primary_path.clone(), ConfigSource::Primary) + (primary_path, ConfigSource::Primary) } else { info!("Config file not found, using defaults"); debug!("Expected config at: {}", primary_path.display()); @@ -205,7 +211,7 @@ impl Config { /// runs a caller's closure over the loaded config. #[cfg(test)] pub(crate) fn update_file(update: impl FnOnce(&mut Self)) -> Result<()> { - let config_path = Self::get_config_path()?; + let config_path = Self::config_path()?; let document = super::ConfigDocument::load_from_path(&config_path)?; let mut config = document.config().clone(); update(&mut config); @@ -261,8 +267,8 @@ pub struct ShortcutClaimedOnDisk { pub claimed_by: Action, } -impl std::fmt::Display for ShortcutClaimedOnDisk { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for ShortcutClaimedOnDisk { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { write!( formatter, "{} is already assigned to {} in config.toml", @@ -272,7 +278,7 @@ impl std::fmt::Display for ShortcutClaimedOnDisk { } } -impl std::error::Error for ShortcutClaimedOnDisk {} +impl error::Error for ShortcutClaimedOnDisk {} /// The write landed, but the bytes it wrote do not parse back to the requested /// value. @@ -293,8 +299,8 @@ pub struct ConfigEditNotReadBack { pub path: PathBuf, } -impl std::fmt::Display for ConfigEditNotReadBack { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for ConfigEditNotReadBack { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { write!( formatter, "{} was written to {}, but the file does not read back with the requested value", @@ -304,7 +310,7 @@ impl std::fmt::Display for ConfigEditNotReadBack { } } -impl std::error::Error for ConfigEditNotReadBack {} +impl error::Error for ConfigEditNotReadBack {} /// The palette no longer has the slot a recolor was accepted for. /// @@ -316,8 +322,8 @@ pub struct QuickColorSlotMissing { pub index: usize, } -impl std::fmt::Display for QuickColorSlotMissing { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for QuickColorSlotMissing { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { write!( formatter, "quick color slot {} is no longer in config.toml", @@ -326,7 +332,7 @@ impl std::fmt::Display for QuickColorSlotMissing { } } -impl std::error::Error for QuickColorSlotMissing {} +impl error::Error for QuickColorSlotMissing {} /// Whether retry can recover by reloading the changed source revision. pub(crate) fn is_stale_source_error(error: &anyhow::Error) -> bool { @@ -456,7 +462,7 @@ fn edit_one_config_key_with_retry( /// Write exactly one action's `[keybindings]` entry. pub fn persist_keybinding_edit(action: Action, bindings: &[String]) -> Result { - write_keybinding_edit(&Config::get_config_path()?, action, bindings) + write_keybinding_edit(&Config::config_path()?, action, bindings) } /// Path-taking form, so the suite can drive real files without the process @@ -545,7 +551,7 @@ pub fn persist_preset_slot( slot: usize, preset: Option<&ToolPresetConfig>, ) -> Result { - write_preset_slot(&Config::get_config_path()?, slot, preset) + write_preset_slot(&Config::config_path()?, slot, preset) } /// Path-taking form; test-only for the reason given on @@ -576,7 +582,7 @@ fn write_preset_slot( config.presets.set_slot(slot, preset.cloned()); Ok(()) }, - &|config| config.presets.get_slot(slot) == preset, + &|config| config.presets.slot(slot) == preset, ) } @@ -590,7 +596,7 @@ fn write_preset_slot( /// because there is no other way to express the color; the entries it gains /// carry the values that were already in effect, so no slot changes meaning. pub fn persist_quick_color(index: usize, color: Color) -> Result { - write_quick_color(&Config::get_config_path()?, index, color) + write_quick_color(&Config::config_path()?, index, color) } /// Path-taking form; test-only for the reason given on @@ -764,7 +770,7 @@ pub(super) fn write_config_text_atomic( let mut options = AtomicWriteOptions::user_config_file(); options.overwrite = overwrite; options.symlink = SymlinkPolicy::Reject; - match crate::durable_io::write_atomic_reporting_identity( + match durable_io::write_atomic_reporting_identity( destination, contents.as_bytes(), options, @@ -785,28 +791,33 @@ pub(super) fn write_config_text_atomic( #[cfg(test)] mod tests { + use std::cell::Cell; + use std::io; + + use crate::test_temp; + use super::*; #[test] fn unrelated_failure_with_stale_wording_does_not_reapply_the_edit() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("config.toml"); fs::write(&path, "[ui]\nshow_status_bar = true\n").unwrap(); - let invocations = std::cell::Cell::new(0); + let invocations = Cell::new(0); let error = edit_one_config_key_with_retry( &path, "status", &|_| { invocations.set(invocations.get() + 1); - Err(std::io::Error::other("unrelated file changed on disk")).context("edit failed") + Err(io::Error::other("unrelated file changed on disk")).context("edit failed") }, &|_| false, ) .unwrap_err(); assert_eq!(invocations.get(), 1); - assert!(error.downcast_ref::().is_some()); + assert!(error.downcast_ref::().is_some()); assert_eq!( fs::read_to_string(path).unwrap(), "[ui]\nshow_status_bar = true\n" @@ -816,10 +827,10 @@ mod tests { #[test] fn changed_source_reapplies_once_and_preserves_the_other_edit() { for collide_twice in [false, true] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("config.toml"); fs::write(&path, "[ui]\nshow_status_bar = true\n").unwrap(); - let invocations = std::cell::Cell::new(0); + let invocations = Cell::new(0); let result = edit_one_config_key_with_retry( &path, @@ -872,7 +883,7 @@ mod tests { /// has something to get wrong. #[test] fn document_config_is_a_fixed_point_of_validation() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = temp.path().join("config.toml"); fs::write( &path, @@ -920,7 +931,7 @@ mod tests { /// before the pair started. #[test] fn two_backups_in_the_same_second_keep_distinct_names_and_contents() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); // Each attempt gets its own directory, so the `-1` suffix means "the // stamp collided" and nothing else. Two calls take microseconds; the diff --git a/src/config/keybindings.rs b/src/config/keybindings.rs index cd8e0cd54..d7b7c52e1 100644 --- a/src/config/keybindings.rs +++ b/src/config/keybindings.rs @@ -9,11 +9,13 @@ mod config; mod defaults; mod shortcut; +use std::sync::OnceLock; + pub use crate::domain::Action; pub use authorship::KeybindingAuthorship; pub use binding::{ - KeyBinding, NAMED_KEYS, canonical_key_names, is_deliverable_key_name, key_display_name, - suggest_key_name, + ChordModifiers, KeyBinding, NAMED_KEYS, canonical_key_names, is_deliverable_key_name, + key_display_name, suggest_key_name, }; pub use config::{ConfigurableAction, KeybindingConflict, KeybindingsConfig}; pub use shortcut::{ @@ -27,7 +29,7 @@ pub use shortcut::{ /// hundreds of strings — and callers of the "is this action rebindable" check /// include the command palette's per-frame tooltip path. pub fn default_keybindings() -> &'static KeybindingsConfig { - static DEFAULTS: std::sync::OnceLock = std::sync::OnceLock::new(); + static DEFAULTS: OnceLock = OnceLock::new(); DEFAULTS.get_or_init(KeybindingsConfig::default) } diff --git a/src/config/keybindings/authorship.rs b/src/config/keybindings/authorship.rs index 8d59f3d9d..de75da835 100644 --- a/src/config/keybindings/authorship.rs +++ b/src/config/keybindings/authorship.rs @@ -31,6 +31,7 @@ impl KeybindingAuthorship { /// keys is exactly the set of authored actions, and a key that no action /// owns is harmless here (the document loader reports it as an unknown /// setting separately). + #[must_use] pub fn from_toml_source(input: &str) -> Self { let Ok(root) = input.parse::() else { // Unreachable from the loaders, which both parse this same text @@ -81,6 +82,7 @@ impl KeybindingAuthorship { } /// Whether the source spelled out one `[keybindings]` key. + #[must_use] pub fn is_explicit(&self, config_key: &str) -> bool { match self { Self::AllExplicit => true, diff --git a/src/config/keybindings/binding.rs b/src/config/keybindings/binding.rs index 2812bc744..e07e0c942 100644 --- a/src/config/keybindings/binding.rs +++ b/src/config/keybindings/binding.rs @@ -1,5 +1,5 @@ -use std::fmt; use std::hash::{Hash, Hasher}; +use std::{fmt, iter}; /// A single keybinding: a key character with optional modifiers. #[derive(Debug, Clone, Eq)] @@ -12,6 +12,17 @@ pub struct KeyBinding { pub logo: bool, } +/// The modifier keys a shortcut chord can carry. Keyboard, pointer, and +/// stylus triggers share this set; Tab is a drag-tool modifier only. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +pub struct ChordModifiers { + pub ctrl: bool, + pub shift: bool, + pub alt: bool, + /// Super / Meta / Windows (Wayland logo modifier). Displayed as `Super`. + pub logo: bool, +} + /// Equality and hashing ignore the key name's case, exactly like [`Self::matches`] /// and the config-file contract ("key names are case-insensitive"). Deriving /// them over the authored spelling instead would let `["ctrl+z"]` and @@ -97,6 +108,7 @@ const KEY_DISPLAY_NAMES: &[(&str, &str)] = &[ /// Case-insensitive like every other key-name comparison, so a config that /// spells `arrowleft` still displays `←`. Anything without a display form — /// single characters, `+`, `Space`, the function keys — comes back unchanged. +#[must_use] pub fn key_display_name(key: &str) -> &str { KEY_DISPLAY_NAMES .iter() @@ -113,6 +125,7 @@ pub fn key_display_name(key: &str) -> &str { /// rendered label is what lets pre-formatted label text — joined alternatives, /// compacted ranges, `then` sequences — stay searchable without every surface /// carrying a second string. +#[must_use] pub fn canonical_key_names(label: &str) -> Option { let mut restored: Option = None; for (name, display) in KEY_DISPLAY_NAMES { @@ -167,6 +180,7 @@ fn starts_with_word_char(text: &str) -> bool { /// Single characters come through as themselves, so any one-character key is /// deliverable; everything else has to be one of [`NAMED_KEYS`]. Matching is /// case-insensitive because so is [`KeyBinding::matches`]. +#[must_use] pub fn is_deliverable_key_name(key: &str) -> bool { key.chars().count() == 1 || NAMED_KEYS @@ -179,6 +193,7 @@ pub fn is_deliverable_key_name(key: &str) -> bool { /// Covers the two mistakes that actually happen: a misspelled modifier, which /// the parser folds into the key because it does not recognise it, and a /// near-miss on a named key. +#[must_use] pub fn suggest_key_name(key: &str) -> Option { if let Some((head, rest)) = key.split_once('+') { // The parser only leaves a `+` in the key when a segment was not a @@ -287,23 +302,18 @@ pub(crate) fn parse_chord_parts(s: &str) -> Result { }) } -pub(crate) fn format_modifiers( - ctrl: bool, - shift: bool, - alt: bool, - logo: bool, -) -> Vec<&'static str> { +pub(crate) fn format_modifiers(modifiers: ChordModifiers) -> Vec<&'static str> { let mut parts = Vec::new(); - if ctrl { + if modifiers.ctrl { parts.push("Ctrl"); } - if shift { + if modifiers.shift { parts.push("Shift"); } - if alt { + if modifiers.alt { parts.push("Alt"); } - if logo { + if modifiers.logo { parts.push("Super"); } parts @@ -329,21 +339,30 @@ impl KeyBinding { /// /// [`fmt::Display`] stays the canonical config spelling, because that is /// what gets written back to `config.toml`. + #[must_use] pub fn display_label(&self) -> String { - format_modifiers(self.ctrl, self.shift, self.alt, self.logo) + format_modifiers(self.modifiers()) .into_iter() - .chain(std::iter::once(key_display_name(&self.key))) + .chain(iter::once(key_display_name(&self.key))) .collect::>() .join("+") } + /// The modifier keys this chord requires. + #[must_use] + pub fn modifiers(&self) -> ChordModifiers { + ChordModifiers { + ctrl: self.ctrl, + shift: self.shift, + alt: self.alt, + logo: self.logo, + } + } + /// Check if this keybinding matches the current input state. - pub fn matches(&self, key: &str, ctrl: bool, shift: bool, alt: bool, logo: bool) -> bool { - self.key.eq_ignore_ascii_case(key) - && self.ctrl == ctrl - && self.shift == shift - && self.alt == alt - && self.logo == logo + #[must_use] + pub fn matches(&self, key: &str, modifiers: ChordModifiers) -> bool { + self.key.eq_ignore_ascii_case(key) && self.modifiers() == modifiers } } @@ -352,9 +371,9 @@ impl fmt::Display for KeyBinding { write!( f, "{}", - format_modifiers(self.ctrl, self.shift, self.alt, self.logo) + format_modifiers(self.modifiers()) .into_iter() - .chain(std::iter::once(self.key.as_str())) + .chain(iter::once(self.key.as_str())) .collect::>() .join("+") ) diff --git a/src/config/keybindings/config/map/edit.rs b/src/config/keybindings/config/map/edit.rs index bb4bf5c41..aeeb4d6d8 100644 --- a/src/config/keybindings/config/map/edit.rs +++ b/src/config/keybindings/config/map/edit.rs @@ -304,6 +304,8 @@ define_action_binding_accessors! { #[cfg(test)] mod tests { + use crate::config; + use super::*; #[test] @@ -345,7 +347,7 @@ mod tests { fn configurable_action_uses_canonical_action_labels() { assert_eq!( ConfigurableAction::CaptureRegionInteractive.label(), - crate::config::action_label(Action::CaptureRegionInteractive) + config::action_label(Action::CaptureRegionInteractive) ); } diff --git a/src/config/keybindings/config/map/mod.rs b/src/config/keybindings/config/map/mod.rs index b003b5977..3d2ee3392 100644 --- a/src/config/keybindings/config/map/mod.rs +++ b/src/config/keybindings/config/map/mod.rs @@ -30,11 +30,13 @@ pub struct KeybindingConflict { impl KeybindingConflict { /// The parsed shortcut every listed action claims. + #[must_use] pub fn binding(&self) -> &Shortcut { &self.binding } /// Every action claiming the shortcut, in keymap traversal order. + #[must_use] pub fn actions(&self) -> &[Action] { &self.actions } @@ -70,18 +72,18 @@ impl ConflictLog { } } -struct BindingInserter<'a> { - map: &'a mut HashMap, - ordered: Option<&'a mut HashMap>>, - conflicts: Option<&'a mut ConflictLog>, +struct BindingInserter<'maps> { + map: &'maps mut HashMap, + ordered: Option<&'maps mut HashMap>>, + conflicts: Option<&'maps mut ConflictLog>, /// Whether a bad binding string or a duplicate key is data rather than a /// failure. Set only for the views that answer "which keys are taken", /// which have to stay usable while the config still has a problem in it. tolerant: bool, } -impl<'a> BindingInserter<'a> { - fn new(map: &'a mut HashMap) -> Self { +impl<'maps> BindingInserter<'maps> { + fn new(map: &'maps mut HashMap) -> Self { Self { map, ordered: None, @@ -91,8 +93,8 @@ impl<'a> BindingInserter<'a> { } fn new_with_order( - map: &'a mut HashMap, - ordered: &'a mut HashMap>, + map: &'maps mut HashMap, + ordered: &'maps mut HashMap>, ) -> Self { Self { map, @@ -103,8 +105,8 @@ impl<'a> BindingInserter<'a> { } fn new_collecting( - map: &'a mut HashMap, - conflicts: &'a mut ConflictLog, + map: &'maps mut HashMap, + conflicts: &'maps mut ConflictLog, ) -> Self { Self { map, @@ -114,7 +116,7 @@ impl<'a> BindingInserter<'a> { } } - fn new_tolerant(map: &'a mut HashMap) -> Self { + fn new_tolerant(map: &'maps mut HashMap) -> Self { Self { map, ordered: None, @@ -249,6 +251,7 @@ impl KeybindingsConfig { /// claims nothing at runtime either. That keeps an editor able to accept an /// edit to an unrelated action while a tolerated problem sits elsewhere in /// the file (#293). + #[must_use] pub fn claimed_keys(&self) -> HashMap { let mut map = HashMap::new(); let mut inserter = BindingInserter::new_tolerant(&mut map); diff --git a/src/config/keybindings/config/types/bindings/board.rs b/src/config/keybindings/config/types/bindings/board.rs index 12a0d389d..e52c99ece 100644 --- a/src/config/keybindings/config/types/bindings/board.rs +++ b/src/config/keybindings/config/types/bindings/board.rs @@ -1,6 +1,14 @@ use serde::{Deserialize, Serialize}; -use crate::config::keybindings::defaults::*; +use crate::config::keybindings::defaults::{ + default_board_1, default_board_2, default_board_3, default_board_4, default_board_5, + default_board_6, default_board_7, default_board_8, default_board_9, default_board_delete, + default_board_duplicate, default_board_new, default_board_next, default_board_paper_edit, + default_board_picker, default_board_prev, default_focus_next_output, default_focus_prev_output, + default_page_delete, default_page_duplicate, default_page_new, default_page_next, + default_page_prev, default_return_to_transparent, default_toggle_blackboard, + default_toggle_whiteboard, +}; #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/config/keybindings/config/types/bindings/capture.rs b/src/config/keybindings/config/types/bindings/capture.rs index 972315b55..a08a9b69f 100644 --- a/src/config/keybindings/config/types/bindings/capture.rs +++ b/src/config/keybindings/config/types/bindings/capture.rs @@ -1,6 +1,15 @@ use serde::{Deserialize, Serialize}; -use crate::config::keybindings::defaults::*; +use crate::config::keybindings::defaults::{ + default_capture_active_window, default_capture_clipboard_full, + default_capture_clipboard_region, default_capture_clipboard_selection, + default_capture_file_full, default_capture_file_region, default_capture_file_selection, + default_capture_full_screen, default_capture_region_interactive, default_capture_selection, + default_copy_text_from_screen, default_export_all_boards_pdf_file, + default_export_board_pdf_file, default_export_canvas_clipboard, + default_export_canvas_clipboard_and_file, default_export_canvas_file, default_measure_mode, + default_open_capture_folder, +}; #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/config/keybindings/config/types/bindings/colors.rs b/src/config/keybindings/config/types/bindings/colors.rs index b3bbd8cd1..08f4f1fd0 100644 --- a/src/config/keybindings/config/types/bindings/colors.rs +++ b/src/config/keybindings/config/types/bindings/colors.rs @@ -1,6 +1,10 @@ use serde::{Deserialize, Serialize}; -use crate::config::keybindings::defaults::*; +use crate::config::keybindings::defaults::{ + default_pick_screen_color, default_set_color_black, default_set_color_blue, + default_set_color_green, default_set_color_orange, default_set_color_pink, + default_set_color_red, default_set_color_white, default_set_color_yellow, +}; #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/config/keybindings/config/types/bindings/core.rs b/src/config/keybindings/config/types/bindings/core.rs index d1faa9184..78e0ff36e 100644 --- a/src/config/keybindings/config/types/bindings/core.rs +++ b/src/config/keybindings/config/types/bindings/core.rs @@ -1,6 +1,9 @@ use serde::{Deserialize, Serialize}; -use crate::config::keybindings::defaults::*; +use crate::config::keybindings::defaults::{ + default_clear_canvas, default_enter_sticky_note_mode, default_enter_text_mode, default_exit, + default_redo, default_undo, +}; #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/config/keybindings/config/types/bindings/presets.rs b/src/config/keybindings/config/types/bindings/presets.rs index 7aaf8c167..073b5ea43 100644 --- a/src/config/keybindings/config/types/bindings/presets.rs +++ b/src/config/keybindings/config/types/bindings/presets.rs @@ -1,6 +1,11 @@ use serde::{Deserialize, Serialize}; -use crate::config::keybindings::defaults::*; +use crate::config::keybindings::defaults::{ + default_apply_preset_1, default_apply_preset_2, default_apply_preset_3, default_apply_preset_4, + default_apply_preset_5, default_clear_preset_1, default_clear_preset_2, default_clear_preset_3, + default_clear_preset_4, default_clear_preset_5, default_save_preset_1, default_save_preset_2, + default_save_preset_3, default_save_preset_4, default_save_preset_5, +}; #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/config/keybindings/config/types/bindings/selection.rs b/src/config/keybindings/config/types/bindings/selection.rs index b12418951..d398d4c0c 100644 --- a/src/config/keybindings/config/types/bindings/selection.rs +++ b/src/config/keybindings/config/types/bindings/selection.rs @@ -1,6 +1,14 @@ use serde::{Deserialize, Serialize}; -use crate::config::keybindings::defaults::*; +use crate::config::keybindings::defaults::{ + default_copy_selection, default_delete_selection, default_duplicate_selection, + default_move_selection_to_back, default_move_selection_to_bottom, + default_move_selection_to_end, default_move_selection_to_front, + default_move_selection_to_start, default_move_selection_to_top, default_nudge_selection_down, + default_nudge_selection_down_large, default_nudge_selection_left, + default_nudge_selection_right, default_nudge_selection_up, default_nudge_selection_up_large, + default_paste_selection, default_select_all, +}; #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/config/keybindings/config/types/bindings/tools.rs b/src/config/keybindings/config/types/bindings/tools.rs index 4e9fbb8f8..0decba12c 100644 --- a/src/config/keybindings/config/types/bindings/tools.rs +++ b/src/config/keybindings/config/types/bindings/tools.rs @@ -1,6 +1,21 @@ use serde::{Deserialize, Serialize}; -use crate::config::keybindings::defaults::*; +use crate::config::keybindings::defaults::{ + default_cycle_arrow_style, default_cycle_blur_style, default_cycle_font_family, + default_decrease_font_size, default_decrease_marker_opacity, default_decrease_pen_smoothing, + default_decrease_shape_recognition_sensitivity, default_decrease_thickness, + default_increase_font_size, default_increase_marker_opacity, default_increase_pen_smoothing, + default_increase_shape_recognition_sensitivity, default_increase_thickness, + default_open_font_picker, default_reset_arrow_labels, default_reset_step_markers, + default_select_arrow_tool, default_select_blur_tool, default_select_ellipse_tool, + default_select_eraser_tool, default_select_freeform_polygon_tool, + default_select_highlight_tool, default_select_laser_tool, default_select_line_tool, + default_select_live_shape_tool, default_select_marker_tool, default_select_parallelogram_tool, + default_select_pen_tool, default_select_rect_tool, default_select_regular_polygon_tool, + default_select_rhombus_tool, default_select_selection_tool, default_select_spotlight_tool, + default_select_step_marker_tool, default_select_triangle_tool, default_toggle_eraser_mode, + default_toggle_highlight_tool, +}; #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/config/keybindings/config/types/bindings/ui.rs b/src/config/keybindings/config/types/bindings/ui.rs index 53dbec39b..7f52c82a2 100644 --- a/src/config/keybindings/config/types/bindings/ui.rs +++ b/src/config/keybindings/config/types/bindings/ui.rs @@ -1,6 +1,15 @@ use serde::{Deserialize, Serialize}; -use crate::config::keybindings::defaults::*; +use crate::config::keybindings::defaults::{ + default_cycle_toolbar_display, default_open_about, default_open_configurator, + default_open_context_menu, default_render_profile_next, default_render_profile_off, + default_render_profile_previous, default_toggle_click_highlight, + default_toggle_command_palette, default_toggle_fill, default_toggle_floating_badge, + default_toggle_focus_mode, default_toggle_help, default_toggle_input_hud, + default_toggle_light_mode, default_toggle_light_mode_drawing, default_toggle_presenter_mode, + default_toggle_quick_help, default_toggle_radial_menu, default_toggle_selection_properties, + default_toggle_status_bar, default_toggle_toolbar, default_toggle_zoom_chip, +}; #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/config/keybindings/config/types/bindings/zoom.rs b/src/config/keybindings/config/types/bindings/zoom.rs index d55f82471..d1586bba5 100644 --- a/src/config/keybindings/config/types/bindings/zoom.rs +++ b/src/config/keybindings/config/types/bindings/zoom.rs @@ -1,6 +1,9 @@ use serde::{Deserialize, Serialize}; -use crate::config::keybindings::defaults::*; +use crate::config::keybindings::defaults::{ + default_refresh_zoom_capture, default_reset_zoom, default_toggle_frozen_mode, + default_toggle_zoom_lock, default_zoom_in, default_zoom_out, +}; #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/config/keybindings/shortcut.rs b/src/config/keybindings/shortcut.rs index a7ea64e08..d28d48108 100644 --- a/src/config/keybindings/shortcut.rs +++ b/src/config/keybindings/shortcut.rs @@ -5,11 +5,12 @@ //! names are reserved and never stored as key names. Sequences use `>` between //! keyboard chords (`Ctrl+K > Ctrl+C`). -use std::fmt; +use std::collections::HashMap; use std::hash::{Hash, Hasher}; +use std::{fmt, mem, slice}; use super::binding::{ - ChordParts, KeyBinding, format_modifiers, parse_chord_parts, suggest_key_name, + ChordModifiers, ChordParts, KeyBinding, format_modifiers, parse_chord_parts, suggest_key_name, }; /// Highest `MouseExtraN` index accepted in this release (`MouseExtra1`..=`MouseExtra4`). @@ -57,6 +58,7 @@ impl PointerButton { Err(format!("Unknown mouse button `{name}`.")) } + #[must_use] pub fn name(self) -> String { match self { Self::Back => "MouseBack".to_string(), @@ -89,6 +91,7 @@ impl StylusButton { Err(format!("Unknown stylus button `{name}`.")) } + #[must_use] pub fn name(self) -> &'static str { match self { Self::Primary => "StylusPrimary", @@ -107,6 +110,19 @@ pub struct PointerTrigger { pub logo: bool, } +impl PointerTrigger { + /// The modifier keys this trigger requires. + #[must_use] + pub fn modifiers(&self) -> ChordModifiers { + ChordModifiers { + ctrl: self.ctrl, + shift: self.shift, + alt: self.alt, + logo: self.logo, + } + } +} + impl PartialEq for PointerTrigger { fn eq(&self, other: &Self) -> bool { self.button == other.button @@ -137,6 +153,19 @@ pub struct StylusTrigger { pub logo: bool, } +impl StylusTrigger { + /// The modifier keys this trigger requires. + #[must_use] + pub fn modifiers(&self) -> ChordModifiers { + ChordModifiers { + ctrl: self.ctrl, + shift: self.shift, + alt: self.alt, + logo: self.logo, + } + } +} + impl PartialEq for StylusTrigger { fn eq(&self, other: &Self) -> bool { self.button == other.button @@ -178,7 +207,7 @@ impl PartialEq for ShortcutTrigger { impl Hash for ShortcutTrigger { fn hash(&self, state: &mut H) { - std::mem::discriminant(self).hash(state); + mem::discriminant(self).hash(state); match self { Self::Keyboard(binding) => binding.hash(state), Self::Pointer(trigger) => trigger.hash(state), @@ -218,6 +247,7 @@ impl ShortcutTrigger { Ok(Self::Keyboard(key_binding_from_parts(parts))) } + #[must_use] pub fn as_key_binding(&self) -> Option<&KeyBinding> { match self { Self::Keyboard(binding) => Some(binding), @@ -226,6 +256,7 @@ impl ShortcutTrigger { } /// Whether an input event can currently deliver this trigger. + #[must_use] pub fn is_deliverable(&self) -> bool { match self { Self::Keyboard(binding) => super::binding::is_deliverable_key_name(&binding.key), @@ -237,6 +268,7 @@ impl ShortcutTrigger { } } + #[must_use] pub fn unknown_key_suggestion(&self) -> Option { match self { Self::Keyboard(binding) => suggest_key_name(&binding.key), @@ -244,6 +276,7 @@ impl ShortcutTrigger { } } + #[must_use] pub fn unknown_key_name(&self) -> Option { match self { Self::Keyboard(binding) => Some(binding.key.clone()), @@ -257,22 +290,12 @@ impl fmt::Display for ShortcutTrigger { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Keyboard(binding) => write!(f, "{binding}"), - Self::Pointer(trigger) => write_device_binding( - f, - trigger.ctrl, - trigger.shift, - trigger.alt, - trigger.logo, - &trigger.button.name(), - ), - Self::Stylus(trigger) => write_device_binding( - f, - trigger.ctrl, - trigger.shift, - trigger.alt, - trigger.logo, - trigger.button.name(), - ), + Self::Pointer(trigger) => { + write_device_binding(f, trigger.modifiers(), &trigger.button.name()) + } + Self::Stylus(trigger) => { + write_device_binding(f, trigger.modifiers(), trigger.button.name()) + } } } } @@ -299,7 +322,7 @@ impl PartialEq for Shortcut { impl Hash for Shortcut { fn hash(&self, state: &mut H) { - std::mem::discriminant(self).hash(state); + mem::discriminant(self).hash(state); match self { Self::Single(trigger) => trigger.hash(state), Self::Sequence(steps) => steps.hash(state), @@ -340,6 +363,7 @@ impl Shortcut { parse_keyboard_sequence(&parts) } + #[must_use] pub fn as_trigger(&self) -> Option<&ShortcutTrigger> { match self { Self::Single(trigger) => Some(trigger), @@ -352,9 +376,10 @@ impl Shortcut { } /// Keyboard chords that make up this shortcut, if it is keyboard-only. + #[must_use] pub fn keyboard_steps(&self) -> Option<&[KeyBinding]> { match self { - Self::Single(ShortcutTrigger::Keyboard(binding)) => Some(std::slice::from_ref(binding)), + Self::Single(ShortcutTrigger::Keyboard(binding)) => Some(slice::from_ref(binding)), Self::Sequence(steps) => Some(steps.as_slice()), Self::Single(_) => None, } @@ -364,6 +389,7 @@ impl Shortcut { /// /// A standalone chord that starts a sequence, or a shorter sequence that /// starts a longer one, cannot be dispatched without delaying the prefix. + #[must_use] pub fn prefix_conflicts_with(&self, other: &Self) -> bool { let Some(left) = self.keyboard_steps() else { return false; @@ -391,6 +417,7 @@ impl Shortcut { } } + #[must_use] pub fn is_deliverable(&self) -> bool { match self { Self::Single(trigger) => trigger.is_deliverable(), @@ -400,6 +427,7 @@ impl Shortcut { } } + #[must_use] pub fn unknown_key_suggestion(&self) -> Option { match self { Self::Single(trigger) => trigger.unknown_key_suggestion(), @@ -407,6 +435,7 @@ impl Shortcut { } } + #[must_use] pub fn unknown_key_name(&self) -> Option { match self { Self::Single(trigger) => trigger.unknown_key_name(), @@ -417,10 +446,8 @@ impl Shortcut { } /// Action that already claims this identity or a keyboard prefix/extension. - pub fn claimed_by( - &self, - claimed: &std::collections::HashMap, - ) -> Option { + #[must_use] + pub fn claimed_by(&self, claimed: &HashMap) -> Option { if let Some(owner) = claimed.get(self) { return Some(*owner); } @@ -485,13 +512,10 @@ fn parse_keyboard_sequence(parts: &[&str]) -> Result { fn write_device_binding( f: &mut fmt::Formatter<'_>, - ctrl: bool, - shift: bool, - alt: bool, - logo: bool, + modifiers: ChordModifiers, name: &str, ) -> fmt::Result { - let mut parts = format_modifiers(ctrl, shift, alt, logo); + let mut parts = format_modifiers(modifiers); parts.push(name); write!(f, "{}", parts.join("+")) } @@ -546,6 +570,7 @@ pub mod linux { pub const BTN_STYLUS: u32 = 0x14b; pub const BTN_STYLUS2: u32 = 0x14c; + #[must_use] pub fn pointer_button(code: u32) -> Option { match code { BTN_SIDE | BTN_BACK => Some(PointerButton::Back), @@ -558,6 +583,7 @@ pub mod linux { } } + #[must_use] pub fn stylus_button(code: u32) -> Option { match code { BTN_STYLUS => Some(StylusButton::Primary), @@ -575,6 +601,7 @@ pub mod linux { pub mod gdk { use super::{MAX_POINTER_EXTRA, PointerButton, StylusButton}; + #[must_use] pub fn pointer_button(button: u32) -> Option { match button { 8 | 11 => Some(PointerButton::Back), @@ -587,6 +614,7 @@ pub mod gdk { } /// Barrel buttons on a GDK tablet/pen source. Tip (1) is not bindable here. + #[must_use] pub fn stylus_button(button: u32) -> Option { match button { 2 => Some(StylusButton::Primary), diff --git a/src/config/keybindings/tests.rs b/src/config/keybindings/tests.rs index fde10b2d6..241c83a22 100644 --- a/src/config/keybindings/tests.rs +++ b/src/config/keybindings/tests.rs @@ -1,3 +1,9 @@ +use std::collections::HashMap; +use std::ffi::OsStr; + +use crate::input::state; +use crate::{env_vars, test_env}; + use super::*; #[test] @@ -79,7 +85,7 @@ fn bindings_differing_only_in_key_case_are_the_same_binding() { let upper = KeyBinding::parse("Ctrl+Z").unwrap(); assert_eq!(lower, upper); - let mut map = std::collections::HashMap::new(); + let mut map = HashMap::new(); map.insert(lower, ()); assert!(map.contains_key(&upper)); assert_eq!( @@ -103,9 +109,17 @@ fn super_aliases_parse_display_equal_and_match_as_super() { assert!(canonical.logo); assert!(!canonical.ctrl); assert_eq!(canonical.to_string(), "Super+X"); - assert!(canonical.matches("x", false, false, false, true)); - assert!(!canonical.matches("x", false, false, false, false)); - assert!(!canonical.matches("x", true, false, false, false)); + let super_held = ChordModifiers { + logo: true, + ..ChordModifiers::default() + }; + let ctrl_held = ChordModifiers { + ctrl: true, + ..ChordModifiers::default() + }; + assert!(canonical.matches("x", super_held)); + assert!(!canonical.matches("x", ChordModifiers::default())); + assert!(!canonical.matches("x", ctrl_held)); for alias in ["Meta+X", "Logo+X", "Win+X", "Windows+X", "meta+x", "WIN+X"] { let parsed = KeyBinding::parse(alias).unwrap(); @@ -120,8 +134,8 @@ fn super_aliases_parse_display_equal_and_match_as_super() { assert_eq!(shifted.to_string(), "Shift+Super+F5"); assert_eq!(KeyBinding::parse("Super+Shift+F5").unwrap(), shifted); - let mut map = std::collections::HashMap::new(); - map.insert(canonical.clone(), ()); + let mut map = HashMap::new(); + map.insert(canonical, ()); assert!(map.contains_key(&KeyBinding::parse("Meta+X").unwrap())); assert!(!map.contains_key(&KeyBinding::parse("Ctrl+X").unwrap())); assert!(!map.contains_key(&KeyBinding::parse("X").unwrap())); @@ -136,11 +150,24 @@ fn test_display_normalizes_modifier_order() { #[test] fn test_matches() { let binding = KeyBinding::parse("Ctrl+Shift+W").unwrap(); - assert!(binding.matches("W", true, true, false, false)); - assert!(binding.matches("w", true, true, false, false)); // Case insensitive - assert!(!binding.matches("W", false, true, false, false)); // Missing ctrl - assert!(!binding.matches("W", true, false, false, false)); // Missing shift - assert!(!binding.matches("A", true, true, false, false)); // Wrong key + let ctrl_shift = ChordModifiers { + ctrl: true, + shift: true, + ..ChordModifiers::default() + }; + let shift_only = ChordModifiers { + shift: true, + ..ChordModifiers::default() + }; + let ctrl_only = ChordModifiers { + ctrl: true, + ..ChordModifiers::default() + }; + assert!(binding.matches("W", ctrl_shift)); + assert!(binding.matches("w", ctrl_shift)); // Case insensitive + assert!(!binding.matches("W", shift_only)); // Missing ctrl + assert!(!binding.matches("W", ctrl_only)); // Missing shift + assert!(!binding.matches("A", ctrl_shift)); // Wrong key } #[test] @@ -381,8 +408,12 @@ fn test_parse_trims_surrounding_whitespace() { #[test] fn test_matches_requires_exact_alt_state() { let binding = KeyBinding::parse("Alt+X").unwrap(); - assert!(binding.matches("x", false, false, true, false)); - assert!(!binding.matches("x", false, false, false, false)); + let alt_held = ChordModifiers { + alt: true, + ..ChordModifiers::default() + }; + assert!(binding.matches("x", alt_held)); + assert!(!binding.matches("x", ChordModifiers::default())); } #[test] @@ -880,15 +911,12 @@ const DEFAULT_BINDING_SNAPSHOT: &[(&str, &[&str])] = &[ /// cannot record because it pins the other one. #[test] fn gnome_sessions_get_the_unprefixed_page_navigation_defaults() { - let (prev, next) = crate::test_env::with_env_var( - crate::env_vars::XDG_CURRENT_DESKTOP_ENV, - Some(std::ffi::OsStr::new("GNOME")), + let (prev, next) = test_env::with_env_var( + env_vars::XDG_CURRENT_DESKTOP_ENV, + Some(OsStr::new("GNOME")), || { let defaults = KeybindingsConfig::default(); - ( - defaults.board.page_prev.clone(), - defaults.board.page_next.clone(), - ) + (defaults.board.page_prev.clone(), defaults.board.page_next) }, ); @@ -898,7 +926,7 @@ fn gnome_sessions_get_the_unprefixed_page_navigation_defaults() { #[test] fn default_bindings_match_the_checked_in_snapshot() { - let defaults = crate::test_env::with_scrubbed_desktop_env(KeybindingsConfig::default); + let defaults = test_env::with_scrubbed_desktop_env(KeybindingsConfig::default); let actual = KeybindingsConfig::configurable_actions() .iter() .map(|action| { @@ -985,7 +1013,7 @@ fn every_name_the_input_layer_produces_is_deliverable() { Key::Char('/'), ]; for key in keys { - let name = crate::input::state::key_to_action_label_for_test(key) + let name = state::key_to_action_label_for_test(key) .unwrap_or_else(|| panic!("{key:?} produces no binding name")); assert!( is_deliverable_key_name(&name), diff --git a/src/config/migration.rs b/src/config/migration.rs index 32d410f58..10fb689c9 100644 --- a/src/config/migration.rs +++ b/src/config/migration.rs @@ -20,11 +20,13 @@ pub struct MigrationChange { impl MigrationChange { /// The `[keybindings]` key the change applies to. + #[must_use] pub fn config_key(&self) -> &'static str { self.config_key } /// The human-facing name of the action that key binds. + #[must_use] pub fn action_label(&self) -> &'static str { self.action_label } @@ -32,12 +34,14 @@ impl MigrationChange { /// The shortcuts the source spells out today. An omitted field reads as /// the default this build filled in, which is the value the migration is /// reacting to. + #[must_use] pub fn before(&self) -> &[String] { &self.before } /// The shortcuts the migration proposes. Empty means the action would be /// left unbound. + #[must_use] pub fn after(&self) -> &[String] { &self.after } @@ -68,6 +72,7 @@ impl MigrationPreview { /// — a file whose fields are customized, or one that spells nothing out, /// keeps its own values under every recipe's own gating, and a stamp with /// no accompanying change is not worth asking about. + #[must_use] pub fn for_authored_config(authored: &Config) -> Option { if authored.config_revision >= CURRENT_CONFIG_REVISION { return None; @@ -103,12 +108,14 @@ impl MigrationPreview { } /// The proposed field changes, in keymap traversal order. + #[must_use] pub fn changes(&self) -> &[MigrationChange] { &self.changes } /// The `config_revision` an accepted migration records, so the same /// proposal is not offered again. + #[must_use] pub fn proposed_revision(&self) -> u32 { self.proposed_revision } diff --git a/src/config/mod.rs b/src/config/mod.rs index 339328428..035d9954e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -55,9 +55,9 @@ pub use io::{ persist_preset_slot, persist_quick_color, }; pub use keybindings::{ - Action, ConfigurableAction, KeyBinding, KeybindingAuthorship, KeybindingConflict, - KeybindingsConfig, MAX_SEQUENCE_STEPS, PointerButton, PointerTrigger, Shortcut, - ShortcutTrigger, StylusButton, StylusTrigger, + Action, ChordModifiers, ConfigurableAction, KeyBinding, KeybindingAuthorship, + KeybindingConflict, KeybindingsConfig, MAX_SEQUENCE_STEPS, PointerButton, PointerTrigger, + Shortcut, ShortcutTrigger, StylusButton, StylusTrigger, }; pub use migration::{MigrationChange, MigrationPreview}; #[allow(unused_imports)] diff --git a/src/config/salvage.rs b/src/config/salvage.rs index 81fe43ff6..c29ce199b 100644 --- a/src/config/salvage.rs +++ b/src/config/salvage.rs @@ -8,6 +8,7 @@ //! replace all of it. use anyhow::{Context, Result}; +use toml::de; use super::Config; use super::io::ConfigSectionError; @@ -33,7 +34,7 @@ pub(super) struct SalvagedConfig { /// from — and fails the call. pub(super) fn deserialize_salvaging( input: &str, - mut deserialize: impl FnMut(toml::Table) -> Result, + mut deserialize: impl FnMut(toml::Table) -> Result, ) -> Result { let table = input .parse::() @@ -156,7 +157,7 @@ fn can_split(path: &[String]) -> bool { /// Only a table whose empty form maps is ever split (see [`can_split`]), so /// every table above `path` fills in its other keys with defaults around the /// probe. -fn probe(path: &[String], value: toml::Value) -> Result<(), toml::de::Error> { +fn probe(path: &[String], value: toml::Value) -> Result<(), de::Error> { let mut nested = value; for key in path.iter().rev() { let mut table = toml::Table::new(); diff --git a/src/config/schema.rs b/src/config/schema.rs index 91d675bd0..8f6e6e1e4 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -4,7 +4,13 @@ use serde_json::Value; impl Config { /// Generates a JSON Schema describing the full configuration surface. + /// + /// # Panics + /// + /// Panics if the generated schema cannot be converted to a JSON value. + /// The schema is already JSON, so this only happens on a `schemars` bug. #[allow(dead_code)] + #[must_use] pub fn json_schema() -> Value { serde_json::to_value(schema_for!(Config)) .expect("serializing configuration schema should succeed") diff --git a/src/config/test_helpers.rs b/src/config/test_helpers.rs index ddc1a54cd..2f3757217 100644 --- a/src/config/test_helpers.rs +++ b/src/config/test_helpers.rs @@ -2,11 +2,15 @@ use std::fs; use std::path::{Path, PathBuf}; use std::time::SystemTime; +use std::env; +use std::ffi::OsString; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; +use crate::domain::Language; use crate::env_vars::{LANGUAGE_OVERRIDE_ENV, XDG_CONFIG_HOME_ENV}; use crate::test_temp::TempDir; +use crate::{i18n, test_env}; /// Runs `f` with a fresh config home, and with the UI language under the /// test's control: loading a config at startup selects its `[ui] language`, @@ -16,17 +20,17 @@ pub(crate) fn with_temp_config_home(f: F) -> T where F: FnOnce(&Path) -> T, { - let _guard = crate::test_env::lock(); + let _guard = test_env::lock(); let temp = TempDir::new().expect("tempdir"); let _restore = RestoreConfigHome { - config_home: std::env::var_os(XDG_CONFIG_HOME_ENV), - language_override: std::env::var_os(LANGUAGE_OVERRIDE_ENV), - language: crate::i18n::language(), + config_home: env::var_os(XDG_CONFIG_HOME_ENV), + language_override: env::var_os(LANGUAGE_OVERRIDE_ENV), + language: i18n::language(), }; // SAFETY: tests serialize process environment access and restore the previous value. unsafe { - std::env::set_var(XDG_CONFIG_HOME_ENV, temp.path()); - std::env::remove_var(LANGUAGE_OVERRIDE_ENV); + env::set_var(XDG_CONFIG_HOME_ENV, temp.path()); + env::remove_var(LANGUAGE_OVERRIDE_ENV); } f(temp.path()) @@ -34,9 +38,9 @@ where /// What [`with_temp_config_home`] changed, put back on drop. struct RestoreConfigHome { - config_home: Option, - language_override: Option, - language: crate::domain::Language, + config_home: Option, + language_override: Option, + language: Language, } impl Drop for RestoreConfigHome { @@ -48,13 +52,13 @@ impl Drop for RestoreConfigHome { (LANGUAGE_OVERRIDE_ENV, self.language_override.take()), ] { match value { - Some(value) => std::env::set_var(key, value), - None => std::env::remove_var(key), + Some(value) => env::set_var(key, value), + None => env::remove_var(key), } } } // The override is back, so restore the resolved language directly. - crate::i18n::restore_language(self.language); + i18n::restore_language(self.language); } } diff --git a/src/config/tests/board_grid.rs b/src/config/tests/board_grid.rs index 4eaaedf11..766d82142 100644 --- a/src/config/tests/board_grid.rs +++ b/src/config/tests/board_grid.rs @@ -1,5 +1,6 @@ -use crate::config::{BoardGridConfig, BoardGridKindConfig, Config}; +use crate::config::{BoardGridConfig, BoardGridKindConfig, BoardsConfig, Config}; use crate::domain::{BoardGrid, BoardGridKind}; +use crate::input::BoardManager; #[test] fn board_grid_config_defaults_and_modes_round_trip() { @@ -40,12 +41,12 @@ fn board_grid_config_clamps_spacing_and_disables_transparent_patterns() { #[test] fn board_grid_runtime_mapping_preserves_new_session_templates() { - let mut config = crate::config::BoardsConfig::default(); + let mut config = BoardsConfig::default(); config.items[1].grid = BoardGridConfig { kind: BoardGridKindConfig::IsometricDots, spacing: 20, }; - let mut boards = crate::input::BoardManager::from_config(config); + let mut boards = BoardManager::from_config(config); assert_eq!( boards.board_states()[1].spec.grid, BoardGrid::new(BoardGridKind::IsometricDots, 20) diff --git a/src/config/tests/document.rs b/src/config/tests/document.rs index e0dfd9c25..e25680bf0 100644 --- a/src/config/tests/document.rs +++ b/src/config/tests/document.rs @@ -1,3 +1,11 @@ +use crate::config::{ + BoardGridKindConfig, ConfigSourceChanged, ResolvedToolbarItems, ToolbarItemOrderGroup, io, +}; + +use std::collections::HashSet; +use std::time::SystemTime; +use std::{env, process}; + use super::super::*; use std::fs; use std::path::PathBuf; @@ -23,7 +31,7 @@ fn board_grid_guarded_save_preserves_inline_and_nested_unknown_settings() { .iter_mut() .find(|b| b.id == "paper") .unwrap(); - board.grid.kind = crate::config::BoardGridKindConfig::IsometricDots; + board.grid.kind = BoardGridKindConfig::IsometricDots; board.grid.spacing = 40; document.save_with_backup(config).unwrap(); let saved = fs::read_to_string(&temp.path).unwrap(); @@ -48,9 +56,9 @@ struct TempConfig { impl TempConfig { fn new(name: &str) -> Self { let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed); - let root = std::env::temp_dir().join(format!( + let root = env::temp_dir().join(format!( "wayscriber-config-document-{}-{sequence}-{name}", - std::process::id() + process::id() )); fs::create_dir_all(&root).expect("create temporary config directory"); let path = root.join("config.toml"); @@ -79,7 +87,7 @@ fn diagnostic_paths(document: &ConfigDocument) -> Vec<&str> { #[test] fn load_from_path_rejects_oversized_config_file() { let temp = TempConfig::new("oversized"); - let oversized = vec![b'x'; (crate::config::io::MAX_CONFIG_FILE_BYTES as usize) + 1]; + let oversized = vec![b'x'; (io::MAX_CONFIG_FILE_BYTES as usize) + 1]; fs::write(&temp.path, oversized).expect("write oversized config"); let err = ConfigDocument::load_from_path(&temp.path).expect_err("oversized config"); @@ -1758,11 +1766,7 @@ fn exact_revision_detects_same_timestamp_content_replacement() { let error = document .save_with_backup(document.config().clone()) .expect_err("same-time content replacement must conflict"); - assert!( - error - .downcast_ref::() - .is_some() - ); + assert!(error.downcast_ref::().is_some()); assert!(fs::read_to_string(&temp.path).unwrap().contains("144")); } @@ -1773,16 +1777,14 @@ fn exact_revision_detects_content_replacement_with_rolled_back_timestamp() { let document = ConfigDocument::load_from_path(&temp.path).expect("load document"); temp.write("[performance]\nmax_fps_no_vsync = 165\n"); fs::File::open(&temp.path) - .and_then(|file| { - file.set_times(fs::FileTimes::new().set_modified(std::time::SystemTime::UNIX_EPOCH)) - }) + .and_then(|file| file.set_times(fs::FileTimes::new().set_modified(SystemTime::UNIX_EPOCH))) .expect("roll back replacement timestamp"); assert!( document .save_with_backup(document.config().clone()) .expect_err("older timestamp must not hide changed content") - .downcast_ref::() + .downcast_ref::() .is_some() ); } @@ -1793,9 +1795,7 @@ fn exact_revision_allows_timestamp_only_change() { temp.write("[performance]\nmax_fps_no_vsync = 120\n"); let document = ConfigDocument::load_from_path(&temp.path).expect("load document"); fs::File::open(&temp.path) - .and_then(|file| { - file.set_times(fs::FileTimes::new().set_modified(std::time::SystemTime::UNIX_EPOCH)) - }) + .and_then(|file| file.set_times(fs::FileTimes::new().set_modified(SystemTime::UNIX_EPOCH))) .expect("roll back timestamp without changing content"); document @@ -1814,7 +1814,7 @@ fn exact_revision_detects_deletion_creation_and_unsupported_replacement() { deleted_document .save_with_backup(deleted_document.config().clone()) .expect_err("deletion must conflict") - .downcast_ref::() + .downcast_ref::() .is_some() ); @@ -1826,7 +1826,7 @@ fn exact_revision_detects_deletion_creation_and_unsupported_replacement() { created_document .save_with_backup(created_document.config().clone()) .expect_err("creation must conflict") - .downcast_ref::() + .downcast_ref::() .is_some() ); @@ -1864,7 +1864,7 @@ fn exact_revision_detects_changed_symlink_target_with_identical_content() { document .save_with_backup(document.config().clone()) .expect_err("symlink target replacement must conflict") - .downcast_ref::() + .downcast_ref::() .is_some() ); } @@ -2084,8 +2084,8 @@ fn explicit_legacy_stylus_unbind_rewrites_only_that_action_node() { #[test] fn performance_metadata_is_unique_and_matches_example_and_docs() { - let mut ids = std::collections::HashSet::new(); - let mut paths = std::collections::HashSet::new(); + let mut ids = HashSet::new(); + let mut paths = HashSet::new(); let example = include_str!("../../../config.example.toml"); let example_value: toml::Value = toml::from_str(example).expect("parse config example"); let docs = include_str!("../../../docs/CONFIG.md"); @@ -2365,7 +2365,7 @@ enabled = true ); assert!(document.config().ui.toolbar.top_pinned); - let assert_active_item_contract = |items: &crate::config::ResolvedToolbarItems| { + let assert_active_item_contract = |items: &ResolvedToolbarItems| { use crate::config::toolbar_item_ids as ids; for id in [ @@ -2391,15 +2391,13 @@ enabled = true assert_eq!( items .order - .ordered_ids(crate::config::ToolbarItemOrderGroup::TopTools) + .ordered_ids(ToolbarItemOrderGroup::TopTools) .first(), Some(&ids::TOP_TOOL_PEN), "active top_tools order must remain effective" ); assert_eq!( - &items - .order - .ordered_ids(crate::config::ToolbarItemOrderGroup::TopControls)[..2], + &items.order.ordered_ids(ToolbarItemOrderGroup::TopControls)[..2], &[ids::TOP_UTILITY_CLEAR_CANVAS, ids::TOP_UTILITY_TEXT], "active top_controls order must remain effective" ); diff --git a/src/config/tests/file_io.rs b/src/config/tests/file_io.rs index fe093dba0..0642e2c75 100644 --- a/src/config/tests/file_io.rs +++ b/src/config/tests/file_io.rs @@ -1,3 +1,7 @@ +use crate::config::{QuickColorWrite, ToolbarStrokeControls}; +use crate::domain::Color; +use crate::test_env; + use super::super::*; use super::{save_through_document, save_through_document_with_backup}; use crate::config::test_helpers::with_temp_config_home; @@ -273,7 +277,7 @@ fn an_unknown_stroke_controls_style_falls_back_to_the_default_panel() { assert_eq!(loaded.config.drawing.default_thickness, 7.0); assert_eq!( loaded.config.ui.toolbar.stroke_controls, - crate::config::ToolbarStrokeControls::Panel + ToolbarStrokeControls::Panel ); assert_eq!(loaded.section_errors.len(), 1); assert_eq!( @@ -403,7 +407,7 @@ color = "#00FF00" .unwrap(); Config::update_file(|config| { - let crimson = crate::domain::Color { + let crimson = Color { r: 220.0 / 255.0, g: 20.0 / 255.0, b: 60.0 / 255.0, @@ -411,7 +415,7 @@ color = "#00FF00" }; assert_eq!( config.drawing.quick_colors.set_color_at(1, crimson), - crate::config::QuickColorWrite::Written + QuickColorWrite::Written ); }) .expect("save the recolored quick color"); @@ -423,7 +427,7 @@ color = "#00FF00" let palette = QuickColorPalette::from_config(&reloaded.drawing.quick_colors); assert_eq!( palette.color_for_index(1), - Some(crate::domain::Color { + Some(Color { r: 220.0 / 255.0, g: 20.0 / 255.0, b: 60.0 / 255.0, @@ -438,7 +442,7 @@ color = "#00FF00" ); assert_eq!( palette.color_for_index(0), - Some(crate::domain::Color { + Some(Color { r: 1.0, g: 0.0, b: 0.0, @@ -721,7 +725,7 @@ fn config_example_values_equal_the_compiled_defaults() { // example runs serde's default functions for every key it omits, and some // of those consult the desktop environment, so computing one side under // GNOME and the other without it compares two different machines. - let (example, defaults) = crate::test_env::with_scrubbed_desktop_env(|| { + let (example, defaults) = test_env::with_scrubbed_desktop_env(|| { let example = include_str!("../../../config.example.toml"); let example: Config = toml::from_str(example).expect("config.example.toml should parse"); ( @@ -810,7 +814,7 @@ fn an_empty_table_resolves_to_its_section_default() { // binds to the tool wheel. const EMPTY_MEANS_SOMETHING: &[&str] = &["tablet.stylus_button"]; - let drifts = crate::test_env::with_scrubbed_desktop_env(|| { + let drifts = test_env::with_scrubbed_desktop_env(|| { let mut defaults = Config::default(); defaults.validate_and_clamp(); let defaults = toml::Value::try_from(&defaults).expect("serialize default config"); diff --git a/src/config/tests/finite.rs b/src/config/tests/finite.rs index a85532ebc..a31d9bd19 100644 --- a/src/config/tests/finite.rs +++ b/src/config/tests/finite.rs @@ -1,4 +1,9 @@ //! Load-policy contracts, inventoried independently against the declaration-derived schema. +use std::fs; + +use crate::config::SaveValidationError; +use crate::test_temp; + use super::{Config, ConfigDocument}; use toml::Value; @@ -247,7 +252,7 @@ fn fixture(path: &str) -> Value { #[test] fn document_load_enforces_finite_policy_for_every_float_and_array_component() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let file = temp.path().join("config.toml"); for case in cases() { let mut inputs = vec![f64::NAN, f64::INFINITY, f64::NEG_INFINITY]; @@ -263,7 +268,7 @@ fn document_load_enforces_finite_policy_for_every_float_and_array_component() { let mut raw = fixture(&case.path); set(&mut raw, &case.path, Value::Float(input)); let source = toml::to_string(&raw).unwrap(); - std::fs::write(&file, &source).unwrap(); + fs::write(&file, &source).unwrap(); let document = ConfigDocument::load_from_path(&file).unwrap(); assert!( document.section_errors().is_empty(), @@ -286,7 +291,7 @@ fn document_load_enforces_finite_policy_for_every_float_and_array_component() { }; assert_eq!(actual, expected, "{} input={input}", case.path); assert_eq!( - std::fs::read_to_string(&file).unwrap(), + fs::read_to_string(&file).unwrap(), source, "load must not rewrite authored non-finite values" ); @@ -297,14 +302,14 @@ fn document_load_enforces_finite_policy_for_every_float_and_array_component() { #[cfg(feature = "tablet-input")] #[test] fn tablet_normalizes_nonfinite_values_before_ordering_and_clamping() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let file = temp.path().join("config.toml"); for (min, max, expected) in [ ("nan", "inf", (1.0, 8.0)), ("60.0", "-10.0", (1.0, 50.0)), ("45.0", "nan", (8.0, 45.0)), ] { - std::fs::write( + fs::write( &file, format!("[tablet]\nmin_thickness = {min}\nmax_thickness = {max}\n"), ) @@ -319,9 +324,7 @@ fn tablet_normalizes_nonfinite_values_before_ordering_and_clamping() { fn authored_save_rejects_nonfinite_float_with_a_field_level_correction() { let mut edited = Config::default(); edited.drawing.default_thickness = f64::NAN; - let Err(crate::config::SaveValidationError::CorrectedValues(corrections)) = - edited.validate_for_save() - else { + let Err(SaveValidationError::CorrectedValues(corrections)) = edited.validate_for_save() else { panic!("authored save must report the invalid numeric input"); }; assert_eq!(corrections.len(), 1); diff --git a/src/config/tests/load.rs b/src/config/tests/load.rs index 2fdaeaf2e..158bfe6ac 100644 --- a/src/config/tests/load.rs +++ b/src/config/tests/load.rs @@ -1,3 +1,10 @@ +use std::env; + +use crate::config::{PresenterToolbarMode, io}; +use crate::domain::Color; +use crate::input::{DragBindableTool, DragTool}; +use crate::{env_vars, util}; + use super::super::*; use super::save_through_document; use crate::config::test_helpers::with_temp_config_home; @@ -174,12 +181,12 @@ fn presenter_toolbar_mode_defaults_to_hidden_and_round_trips() { let default_config: Config = toml::from_str("").expect("empty config should use defaults"); assert_eq!( default_config.presenter_mode.toolbar_mode, - crate::config::PresenterToolbarMode::Hidden + PresenterToolbarMode::Hidden ); for (value, expected) in [ - ("hidden", crate::config::PresenterToolbarMode::Hidden), - ("micro", crate::config::PresenterToolbarMode::Micro), + ("hidden", PresenterToolbarMode::Hidden), + ("micro", PresenterToolbarMode::Micro), ] { let config: Config = toml::from_str(&format!("[presenter_mode]\ntoolbar_mode = '{value}'\n")) @@ -813,7 +820,7 @@ fn drawing_quick_colors_parse_ordered_entries_with_hex_and_rgb() { let palette = QuickColorPalette::from_config(&config.drawing.quick_colors); assert!(color_approx_eq( &palette.color_for_index(0).unwrap(), - &crate::domain::Color { + &Color { r: 1.0, g: 179.0 / 255.0, b: 186.0 / 255.0, @@ -822,7 +829,7 @@ fn drawing_quick_colors_parse_ordered_entries_with_hex_and_rgb() { )); assert!(color_approx_eq( &palette.color_for_index(1).unwrap(), - &crate::domain::Color { + &Color { r: 1.0 / 255.0, g: 2.0 / 255.0, b: 3.0 / 255.0, @@ -873,7 +880,7 @@ fn quick_colors_set_color_at_keeps_labels_and_materializes_defaults() { let mut config: Config = toml::from_str("[[drawing.quick_colors]]\nlabel = 'Only'\ncolor = 'blue'\n") .expect("short quick color list should parse"); - let crimson = crate::domain::Color { + let crimson = Color { r: 220.0 / 255.0, g: 20.0 / 255.0, b: 60.0 / 255.0, @@ -917,7 +924,7 @@ fn quick_colors_set_color_at_keeps_labels_and_materializes_defaults() { fn quick_colors_set_color_at_starts_from_defaults_when_unconfigured() { let mut config = Config::default(); assert!(config.drawing.quick_colors.is_implicit_default()); - let teal = crate::domain::Color { + let teal = Color { r: 0.0, g: 128.0 / 255.0, b: 128.0 / 255.0, @@ -969,7 +976,7 @@ fn shipped_quick_color_defaults_cover_only_the_built_in_palette() { fn palette_set_color_for_index_recolors_in_place() { let mut palette = QuickColorPalette::default(); let label = palette.entry(1).map(|entry| entry.label.clone()); - let color = crate::domain::Color { + let color = Color { r: 0.25, g: 0.5, b: 0.75, @@ -993,7 +1000,7 @@ fn palette_set_color_for_index_recolors_in_place() { #[test] fn palette_cache_key_changes_when_only_a_slot_alpha_changes() { let mut palette = QuickColorPalette::default(); - let opaque = crate::domain::Color { + let opaque = Color { r: 0.25, g: 0.5, b: 0.75, @@ -1002,7 +1009,7 @@ fn palette_cache_key_changes_when_only_a_slot_alpha_changes() { assert!(palette.set_color_for_index(1, opaque)); let before = palette.cache_key(); - assert!(palette.set_color_for_index(1, crate::domain::Color { a: 0.4, ..opaque })); + assert!(palette.set_color_for_index(1, Color { a: 0.4, ..opaque })); assert_ne!(palette.cache_key(), before); } @@ -1036,7 +1043,7 @@ fn drawing_quick_colors_default_palette_preserves_extended_toolbar_colors() { ); assert!(color_approx_eq( &palette.color_for_index(8).unwrap(), - &crate::domain::Color { + &Color { r: 0.0, g: 1.0, b: 1.0, @@ -1045,7 +1052,7 @@ fn drawing_quick_colors_default_palette_preserves_extended_toolbar_colors() { )); assert!(color_approx_eq( &palette.color_for_index(9).unwrap(), - &crate::domain::Color { + &Color { r: 153.0 / 255.0, g: 102.0 / 255.0, b: 204.0 / 255.0, @@ -1054,7 +1061,7 @@ fn drawing_quick_colors_default_palette_preserves_extended_toolbar_colors() { )); assert!(color_approx_eq( &palette.color_for_index(10).unwrap(), - &crate::domain::Color { + &Color { r: 102.0 / 255.0, g: 102.0 / 255.0, b: 102.0 / 255.0, @@ -1166,7 +1173,7 @@ fn drawing_quick_color_rendered_entries_are_capped_without_dropping_config() { let entries = (0..QUICK_COLOR_RENDER_LIMIT + 3) .map(|index| QuickColorPaletteEntry { label: format!("Color {index}"), - color: crate::domain::Color { + color: Color { r: 0.0, g: 0.0, b: 0.0, @@ -1245,11 +1252,11 @@ fn pdf_transparent_background_defaults_to_none() { /// Resolves one of the tuned built-in palette hex values exactly like /// `ColorSpec::to_color`, so assertions can compare with `==`. -fn tuned_default(hex: &str) -> crate::domain::Color { - crate::util::parse_config_hex_color(hex).expect("tuned default hex is valid") +fn tuned_default(hex: &str) -> Color { + util::parse_config_hex_color(hex).expect("tuned default hex is valid") } -fn color_approx_eq(a: &crate::domain::Color, b: &crate::domain::Color) -> bool { +fn color_approx_eq(a: &Color, b: &Color) -> bool { (a.r - b.r).abs() < 0.001 && (a.g - b.g).abs() < 0.001 && (a.b - b.b).abs() < 0.001 @@ -1291,9 +1298,9 @@ fn load_parses_mouse_button_drag_tool_bindings() { let loaded = Config::load().expect("load succeeds"); let drag_tools = loaded.config.drawing.drag_tools.expect("drag tools config"); - assert_eq!(drag_tools.left.drag_tool, crate::input::DragTool::Line); - assert_eq!(drag_tools.left.shift_drag_tool, crate::input::DragTool::Pen); - assert_eq!(drag_tools.right.drag_tool, crate::input::DragTool::Pen); + assert_eq!(drag_tools.left.drag_tool, DragTool::Line); + assert_eq!(drag_tools.left.shift_drag_tool, DragTool::Pen); + assert_eq!(drag_tools.right.drag_tool, DragTool::Pen); assert_eq!( drag_tools.right.drag_color, Some(ColorSpec::Name("blue".to_string())) @@ -1307,23 +1314,11 @@ fn legacy_drag_fields_accept_drag_bindable_polygon_tools() { toml::from_str("[drawing]\ndrag_tool = 'regular-polygon'\nshift_drag_tool = 'triangle'\n") .expect("drag-bindable polygon tools should parse"); - assert_eq!( - config.drawing.drag_tool, - crate::input::DragBindableTool::RegularPolygon - ); - assert_eq!( - config.drawing.shift_drag_tool, - crate::input::DragBindableTool::Triangle - ); + assert_eq!(config.drawing.drag_tool, DragBindableTool::RegularPolygon); + assert_eq!(config.drawing.shift_drag_tool, DragBindableTool::Triangle); let drag_tools = config.drawing.effective_drag_tools(); - assert_eq!( - drag_tools.left.drag_tool, - crate::input::DragTool::RegularPolygon - ); - assert_eq!( - drag_tools.left.shift_drag_tool, - crate::input::DragTool::Triangle - ); + assert_eq!(drag_tools.left.drag_tool, DragTool::RegularPolygon); + assert_eq!(drag_tools.left.shift_drag_tool, DragTool::Triangle); } #[test] @@ -1351,12 +1346,9 @@ fn effective_drag_tools_preserve_legacy_left_when_only_right_is_configured() { let loaded = Config::load().expect("load succeeds"); let drag_tools = loaded.config.drawing.effective_drag_tools(); - assert_eq!(drag_tools.left.drag_tool, crate::input::DragTool::Arrow); - assert_eq!( - drag_tools.left.shift_drag_tool, - crate::input::DragTool::Eraser - ); - assert_eq!(drag_tools.right.drag_tool, crate::input::DragTool::Pen); + assert_eq!(drag_tools.left.drag_tool, DragTool::Arrow); + assert_eq!(drag_tools.left.shift_drag_tool, DragTool::Eraser); + assert_eq!(drag_tools.right.drag_tool, DragTool::Pen); }); } @@ -1373,28 +1365,19 @@ fn effective_drag_tools_preserve_explicit_builtin_left_mapping() { let loaded = Config::load().expect("load succeeds"); let drag_tools = loaded.config.drawing.effective_drag_tools(); - assert_eq!(drag_tools.left.drag_tool, crate::input::DragTool::Pen); - assert_eq!( - drag_tools.left.shift_drag_tool, - crate::input::DragTool::Line - ); - assert_eq!(drag_tools.left.ctrl_drag_tool, crate::input::DragTool::Rect); - assert_eq!( - drag_tools.left.ctrl_shift_drag_tool, - crate::input::DragTool::Arrow - ); - assert_eq!( - drag_tools.left.tab_drag_tool, - crate::input::DragTool::Ellipse - ); + assert_eq!(drag_tools.left.drag_tool, DragTool::Pen); + assert_eq!(drag_tools.left.shift_drag_tool, DragTool::Line); + assert_eq!(drag_tools.left.ctrl_drag_tool, DragTool::Rect); + assert_eq!(drag_tools.left.ctrl_shift_drag_tool, DragTool::Arrow); + assert_eq!(drag_tools.left.tab_drag_tool, DragTool::Ellipse); }); } #[test] fn ui_defaults_follow_desktop_for_xdg_focus_loss() { - let desktop_like_gnome = crate::env_vars::DESKTOP_ENV_KEYS + let desktop_like_gnome = env_vars::DESKTOP_ENV_KEYS .iter() - .filter_map(|key| std::env::var(key).ok()) + .filter_map(|key| env::var(key).ok()) .any(|value| { let value = value.to_lowercase(); value.contains("ubuntu") || value.contains("gnome") @@ -1502,7 +1485,7 @@ fn load_rejects_oversized_config_file() { let primary_dir = config_root.join(PRIMARY_CONFIG_DIR); fs::create_dir_all(&primary_dir).unwrap(); let config_path = primary_dir.join("config.toml"); - let oversized = vec![b'x'; (crate::config::io::MAX_CONFIG_FILE_BYTES as usize) + 1]; + let oversized = vec![b'x'; (io::MAX_CONFIG_FILE_BYTES as usize) + 1]; fs::write(&config_path, oversized).unwrap(); let err = Config::load().expect_err("oversized config must not load"); diff --git a/src/config/tests/validate.rs b/src/config/tests/validate.rs index 514cc4aa7..c99b0a181 100644 --- a/src/config/tests/validate.rs +++ b/src/config/tests/validate.rs @@ -1,3 +1,6 @@ +use crate::config; +use crate::input::{DragBindableTool, Tool}; + use super::super::*; use crate::input::state::{MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; @@ -90,14 +93,14 @@ fn shape_recognition_sensitivity_defaults_and_clamps() { let mut config = config_from_toml("[drawing]\n"); assert_eq!( config.drawing.shape_recognition_sensitivity, - crate::config::DEFAULT_SHAPE_RECOGNITION_SENSITIVITY + config::DEFAULT_SHAPE_RECOGNITION_SENSITIVITY ); config.drawing.shape_recognition_sensitivity = u8::MAX; config.validate_and_clamp(); assert_eq!( config.drawing.shape_recognition_sensitivity, - crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY + config::MAX_SHAPE_RECOGNITION_SENSITIVITY ); } @@ -508,7 +511,7 @@ fn validate_clamps_preset_fields() { }; config.presets.slot_1 = Some(ToolPresetConfig { name: None, - tool: crate::input::Tool::Pen, + tool: Tool::Pen, color: ColorSpec::Name("red".to_string()), size: 120.0, tool_settings: Some(PresetToolStatesConfig { @@ -964,26 +967,11 @@ fn validate_does_not_clamp_autosave_interval_to_idle() { fn drawing_drag_tool_defaults_match_legacy_mapping() { let config = Config::default(); - assert_eq!( - config.drawing.drag_tool, - crate::input::DragBindableTool::Pen - ); - assert_eq!( - config.drawing.shift_drag_tool, - crate::input::DragBindableTool::Line - ); - assert_eq!( - config.drawing.ctrl_drag_tool, - crate::input::DragBindableTool::Rect - ); - assert_eq!( - config.drawing.ctrl_shift_drag_tool, - crate::input::DragBindableTool::Arrow - ); - assert_eq!( - config.drawing.tab_drag_tool, - crate::input::DragBindableTool::Ellipse - ); + assert_eq!(config.drawing.drag_tool, DragBindableTool::Pen); + assert_eq!(config.drawing.shift_drag_tool, DragBindableTool::Line); + assert_eq!(config.drawing.ctrl_drag_tool, DragBindableTool::Rect); + assert_eq!(config.drawing.ctrl_shift_drag_tool, DragBindableTool::Arrow); + assert_eq!(config.drawing.tab_drag_tool, DragBindableTool::Ellipse); } /// Adding alpha must not rewrite anyone's config: an opaque color has to keep diff --git a/src/config/tests/write_lock.rs b/src/config/tests/write_lock.rs index 088d404b7..59153e76a 100644 --- a/src/config/tests/write_lock.rs +++ b/src/config/tests/write_lock.rs @@ -11,6 +11,11 @@ //! These exercise the advisory lock that makes the check and the rename one //! step. Removing `acquire_config_write_lock` from `merge_and_write` fails both. +use std::ffi::OsStr; + +use crate::input::Tool; +use crate::{session, test_temp}; + use super::super::io::{persist_preset_slot_at, persist_quick_color_at}; use super::super::{ColorSpec, ToolPresetConfig}; use crate::domain::Color; @@ -37,7 +42,7 @@ const WINNER_KEY: &str = "show_status_bar = false"; fn preset(name: &str) -> ToolPresetConfig { ToolPresetConfig { name: Some(name.to_string()), - tool: crate::input::Tool::Pen, + tool: Tool::Pen, color: ColorSpec::from(Color { r: 0.13, g: 0.27, @@ -82,7 +87,7 @@ fn fixture(directory: &Path) -> PathBuf { fn config_write_lock_path(config_path: &Path) -> PathBuf { let name = config_path .file_name() - .and_then(std::ffi::OsStr::to_str) + .and_then(OsStr::to_str) .expect("the fixture path this test built has a file name"); config_path.with_file_name(format!("{name}.lock")) } @@ -97,7 +102,7 @@ fn hold_config_write_lock(config_path: &Path) -> File { .truncate(false) .open(&path) .expect("the lock file beside a fixture this test created"); - crate::session::try_lock_exclusive(&file) + session::try_lock_exclusive(&file) .expect("nothing else can hold the lock on a directory this test just made"); file } @@ -122,7 +127,7 @@ fn backups_in(directory: &Path) -> Vec { /// under the lock — replaces the file and takes the preset with it. #[test] fn an_edit_that_waits_for_the_lock_reapplies_onto_the_writer_that_won() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = fixture(temp.path()); let held = hold_config_write_lock(&path); @@ -166,7 +171,7 @@ fn an_edit_that_waits_for_the_lock_reapplies_onto_the_writer_that_won() { /// the two windows actually overlap. #[test] fn two_racing_edits_both_land_and_a_backup_still_holds_the_original() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); for attempt in 0..24 { let directory = temp.path().join(format!("attempt-{attempt}")); diff --git a/src/config/tests/write_target.rs b/src/config/tests/write_target.rs index 03bb440c2..1bbf6dd8f 100644 --- a/src/config/tests/write_target.rs +++ b/src/config/tests/write_target.rs @@ -25,6 +25,12 @@ //! checked is carried down to the rename, which refuses to land on any other. //! Both are stale sources, and both are recovered the same way: reload, reapply. +use std::ffi::OsStr; +use std::process; + +use crate::input::Tool; +use crate::{session, test_temp}; + use super::super::io::{is_stale_source_error, persist_preset_slot_at}; use super::super::{ColorSpec, ConfigDocument, ToolPresetConfig}; use crate::domain::Color; @@ -48,7 +54,7 @@ setting_from_a_later_release = 7 fn preset(name: &str) -> ToolPresetConfig { ToolPresetConfig { name: Some(name.to_string()), - tool: crate::input::Tool::Pen, + tool: Tool::Pen, color: ColorSpec::from(Color { r: 0.13, g: 0.27, @@ -120,7 +126,7 @@ fn hold_config_write_lock(destination: &Path) -> File { let destination = &fs::canonicalize(destination).expect("a fixture this test created"); let name = destination .file_name() - .and_then(std::ffi::OsStr::to_str) + .and_then(OsStr::to_str) .expect("the fixture path this test built has a file name"); let path = destination.with_file_name(format!("{name}.lock")); let file = OpenOptions::new() @@ -130,7 +136,7 @@ fn hold_config_write_lock(destination: &Path) -> File { .truncate(false) .open(&path) .expect("the lock file beside a fixture this test created"); - crate::session::try_lock_exclusive(&file) + session::try_lock_exclusive(&file) .expect("nothing else can hold the lock on a directory this test just made"); file } @@ -153,7 +159,7 @@ struct RelativeRoot { impl RelativeRoot { fn new(label: &str) -> Self { - let name = format!("wayscriber-test-{label}-{}", std::process::id()); + let name = format!("wayscriber-test-{label}-{}", process::id()); let absolute = fs::canonicalize(".") .expect("the working directory this suite was started in") .join(&name); @@ -188,7 +194,7 @@ impl Drop for RelativeRoot { /// file's contents. #[test] fn a_retarget_after_the_last_check_still_writes_the_file_the_window_was_about() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); const BYSTANDER: &str = "# A file this save has nothing to do with.\n"; let fixture = linked(temp.path(), BYSTANDER); @@ -245,7 +251,7 @@ fn a_retarget_after_the_last_check_still_writes_the_file_the_window_was_about() /// reapplying onto the file that is there now is exactly the right recovery. #[test] fn a_file_swapped_in_under_the_checked_name_is_refused_rather_than_overwritten() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); const REPLACEMENT: &str = "# Another editor's file, left at the same name.\n"; let directory = temp.path().join("swapped"); fs::create_dir_all(&directory).expect("the fixture directory this test named"); @@ -319,7 +325,7 @@ fn a_file_swapped_in_under_the_checked_name_is_refused_rather_than_overwritten() /// the edit on top of what the other editor wrote. #[test] fn an_in_place_change_after_the_last_check_is_refused_and_reapplied() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); const REPLACEMENT: &str = "\ # Another editor rewrote this same file in place.\n\ [ui]\n\ @@ -375,7 +381,7 @@ show_status_bar = false\n"; /// do; overwriting would spend their file to say this one succeeded. #[test] fn a_file_created_under_a_missing_name_during_the_window_is_refused() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); const FIRST_WRITE: &str = "# Somebody else's first config, written just now.\n"; let path = temp.path().join("fresh/config.toml"); @@ -426,7 +432,7 @@ fn a_file_created_under_a_missing_name_during_the_window_is_refused() { /// editors' reload-and-reapply retry recognises. #[test] fn a_retarget_to_identical_bytes_is_still_a_stale_source() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let fixture = linked(temp.path(), ORIGINAL); let document = ConfigDocument::load_from_path(&fixture.link).expect("load through the link"); @@ -477,7 +483,7 @@ fn a_retarget_to_identical_bytes_is_still_a_stale_source() { /// editors already run must then land the edit on the profile in force. #[test] fn a_retargeted_parent_directory_is_a_stale_source_the_retry_reapplies_through() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let first = temp.path().join("profileA"); let second = temp.path().join("profileB"); fs::create_dir_all(&first).expect("the first profile this test named"); @@ -550,7 +556,7 @@ fn a_retargeted_parent_directory_is_a_stale_source_the_retry_reapplies_through() /// on disk. #[test] fn a_path_with_no_links_on_it_is_written_exactly_where_it_was_named() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let directory = temp.path().join("plain"); fs::create_dir_all(&directory).expect("the fixture directory this test named"); let path = directory.join("config.toml"); @@ -603,7 +609,7 @@ fn a_path_with_no_links_on_it_is_written_exactly_where_it_was_named() { /// as the thing that changed it. #[test] fn a_second_save_through_the_document_the_first_returned_is_not_a_change_on_disk() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = temp.path().join("config.toml"); fs::write(&path, ORIGINAL).expect("the fixture this test named"); @@ -643,7 +649,7 @@ fn a_second_save_through_the_document_the_first_returned_is_not_a_change_on_disk /// disk. The deepest ancestor that does exist is what both derivations agree on. #[test] fn a_first_save_into_a_directory_that_does_not_exist_yet_is_not_a_change_on_disk() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = temp.path().join("not-made-yet/wayscriber/config.toml"); let document = ConfigDocument::load_from_path(&path).expect("a missing config loads defaults"); @@ -676,7 +682,7 @@ fn a_first_save_into_a_directory_that_does_not_exist_yet_is_not_a_change_on_disk /// back — the editor that never reloads. #[test] fn a_dot_dot_below_a_missing_directory_saves_twice_without_reporting_itself() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = temp.path().join("missing/../target/config.toml"); let document = ConfigDocument::load_from_path(&path).expect("a missing config loads defaults"); @@ -711,7 +717,7 @@ fn a_dot_dot_below_a_missing_directory_saves_twice_without_reporting_itself() { /// which is what the kernel does with it too. #[test] fn a_dot_dot_climbing_past_the_existing_ancestor_pins_where_the_save_creates_it() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let inner = temp.path().join("inner"); fs::create_dir_all(&inner).expect("the deepest directory this test lets exist"); let path = inner.join("missing/../../outside/config.toml"); @@ -796,7 +802,7 @@ fn a_relative_path_with_no_existing_ancestor_saves_twice_without_reporting_itsel /// every byte it had. #[test] fn an_edit_that_waits_for_the_lock_reapplies_onto_the_retargeted_file() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); const REPLACEMENT: &str = "# The file the link points at by the time the editor wakes.\n"; let fixture = linked(temp.path(), REPLACEMENT); @@ -840,11 +846,11 @@ fn an_edit_that_waits_for_the_lock_reapplies_onto_the_retargeted_file() { /// through the file, and one without leaves no trace of the field. #[test] fn a_preset_keeps_its_fill_color_through_the_config_file() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let path = temp.path().join("config.toml"); fs::write(&path, ORIGINAL).expect("seed config"); let mut filled = preset("Filled"); - filled.tool = crate::input::Tool::Rect; + filled.tool = Tool::Rect; filled.fill_enabled = Some(true); filled.fill_color = Some(ColorSpec::from(Color { r: 0.0, @@ -859,6 +865,6 @@ fn a_preset_keeps_its_fill_color_through_the_config_file() { let written = fs::read_to_string(&path).expect("readable"); assert_eq!(written.matches("fill_color").count(), 1, "{written}"); let loaded: super::super::Config = toml::from_str(&written).expect("parse"); - assert_eq!(loaded.presets.get_slot(1), Some(&filled)); - assert_eq!(loaded.presets.get_slot(2), Some(&preset("Plain"))); + assert_eq!(loaded.presets.slot(1), Some(&filled)); + assert_eq!(loaded.presets.slot(2), Some(&preset("Plain"))); } diff --git a/src/config/types/boards.rs b/src/config/types/boards.rs index e251662b5..c0489ea13 100644 --- a/src/config/types/boards.rs +++ b/src/config/types/boards.rs @@ -57,10 +57,12 @@ impl Default for BoardsConfig { } impl BoardsConfig { + #[must_use] pub fn default_items() -> Vec { default_board_items() } + #[must_use] pub fn default_overlay_item() -> BoardItemConfig { BoardItemConfig { id: "transparent".to_string(), @@ -74,6 +76,7 @@ impl BoardsConfig { } } + #[must_use] pub fn from_legacy(legacy: &BoardConfig) -> Self { let mut items = vec![BoardsConfig::default_overlay_item()]; @@ -162,6 +165,7 @@ pub enum BoardBackgroundConfig { } impl BoardBackgroundConfig { + #[must_use] pub fn is_transparent(&self) -> bool { matches!(self, BoardBackgroundConfig::Transparent(_)) } @@ -177,6 +181,7 @@ pub enum BoardColorConfig { } impl BoardColorConfig { + #[must_use] pub fn rgb(&self) -> [f64; 3] { match self { BoardColorConfig::Rgb(rgb) => *rgb, diff --git a/src/config/types/capture.rs b/src/config/types/capture.rs index b214c690d..eeb9e27eb 100644 --- a/src/config/types/capture.rs +++ b/src/config/types/capture.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::config::enums::RegionPicker; +use crate::paths; /// Region screenshot picker configuration. #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] @@ -104,6 +105,7 @@ impl Default for CaptureConfig { impl CaptureConfig { /// The configured OCR languages, or the default when the authored value is /// unusable. Recognition never passes an unvalidated string to the engine. + #[must_use] pub fn resolved_ocr_languages(&self) -> String { match validate_ocr_languages(&self.ocr_languages) { Ok(languages) => languages, @@ -159,7 +161,7 @@ pub fn validate_filename_template(template: &str) -> Result<(), String> { if trimmed.is_empty() { return Err("value is empty".to_string()); } - if !crate::paths::is_single_path_component(trimmed) { + if !paths::is_single_path_component(trimmed) { return Err("must be a single file name, not a path".to_string()); } Ok(()) diff --git a/src/config/types/drawing.rs b/src/config/types/drawing.rs index 793fe6b14..12342465e 100644 --- a/src/config/types/drawing.rs +++ b/src/config/types/drawing.rs @@ -235,6 +235,7 @@ impl<'de> Deserialize<'de> for QuickColorsConfig { } impl QuickColorsConfig { + #[must_use] pub fn effective_entries(&self) -> Vec { if self.entries.is_empty() { return default_quick_color_entries(); @@ -248,6 +249,7 @@ impl QuickColorsConfig { entries } + #[must_use] pub fn configured_entry_count(&self) -> Option { self.configured_entry_count } @@ -288,6 +290,7 @@ impl QuickColorsConfig { self.entries = entries; } + #[must_use] pub fn is_implicit_default(&self) -> bool { self.configured_entry_count.is_none() && self.entries == default_quick_color_entries() } @@ -314,6 +317,7 @@ pub struct QuickColorConfig { } impl QuickColorConfig { + #[must_use] pub fn resolved_label(&self, index: usize) -> String { quick_color_label(self, index) } @@ -343,6 +347,7 @@ impl QuickColorSlot { Self::Black, ]; + #[must_use] pub fn label(self) -> &'static str { match self { Self::Red => "Red", @@ -356,10 +361,12 @@ impl QuickColorSlot { } } + #[must_use] pub fn index(self) -> usize { self as usize } + #[must_use] pub fn action(self) -> Action { match self { Self::Red => Action::SetColorRed, @@ -373,6 +380,7 @@ impl QuickColorSlot { } } + #[must_use] pub fn fallback_key(self) -> &'static str { match self { Self::Red => "R", @@ -386,6 +394,7 @@ impl QuickColorSlot { } } + #[must_use] pub fn from_action(action: Action) -> Option { match action { Action::SetColorRed => Some(Self::Red), @@ -400,6 +409,7 @@ impl QuickColorSlot { } } + #[must_use] pub fn from_index(index: usize) -> Option { Self::ALL.get(index).copied() } @@ -419,6 +429,7 @@ pub struct QuickColorPalette { } impl QuickColorPalette { + #[must_use] pub fn from_config(config: &QuickColorsConfig) -> Self { if config.entries.is_empty() { return Self::default(); @@ -441,6 +452,7 @@ impl QuickColorPalette { } #[cfg(test)] + #[must_use] pub fn from_entries(entries: Vec) -> Self { let radial_render_len = entries.len().min(QUICK_COLOR_RENDER_LIMIT); Self { @@ -449,15 +461,18 @@ impl QuickColorPalette { } } + #[must_use] pub fn rendered_entries(&self) -> &[QuickColorPaletteEntry] { let len = self.rendered_len(); &self.entries[..len] } + #[must_use] pub fn len(&self) -> usize { self.entries.len() } + #[must_use] pub fn rendered_len(&self) -> usize { if self.is_empty() { return 0; @@ -465,19 +480,23 @@ impl QuickColorPalette { self.entries.len().min(QUICK_COLOR_RENDER_LIMIT) } + #[must_use] pub fn radial_rendered_entries(&self) -> &[QuickColorPaletteEntry] { let len = self.radial_rendered_len(); &self.entries[..len] } + #[must_use] pub fn radial_rendered_len(&self) -> usize { self.radial_render_len.min(self.rendered_len()) } + #[must_use] pub fn is_empty(&self) -> bool { self.entries.is_empty() } + #[must_use] pub fn entry(&self, index: usize) -> Option<&QuickColorPaletteEntry> { self.entries.get(index) } @@ -496,20 +515,24 @@ impl QuickColorPalette { } } + #[must_use] pub fn color_for_action(&self, action: Action) -> Option { let index = QuickColorSlot::from_action(action)?.index(); self.color_for_index(index) } + #[must_use] pub fn color_for_index(&self, index: usize) -> Option { self.entry(index).map(|entry| entry.color) } #[cfg(test)] + #[must_use] pub fn rendered_color_for_index(&self, index: usize) -> Option { self.rendered_entries().get(index).map(|entry| entry.color) } + #[must_use] pub fn radial_color_for_index(&self, index: usize) -> Option { self.radial_rendered_entries() .get(index) @@ -520,6 +543,7 @@ impl QuickColorPalette { QuickColorSlot::from_index(index).map(QuickColorSlot::action) } + #[must_use] pub fn cache_key(&self) -> String { let entries = self .entries @@ -574,6 +598,7 @@ pub struct MouseDragToolsConfig { } impl MouseDragToolsConfig { + #[must_use] pub fn from_legacy( drag_tool: DragBindableTool, shift_drag_tool: DragBindableTool, @@ -595,6 +620,7 @@ impl MouseDragToolsConfig { } } + #[must_use] pub fn from_buttons( left: DragButtonConfig, right: DragButtonConfig, @@ -608,10 +634,12 @@ impl MouseDragToolsConfig { } } + #[must_use] pub fn left_explicit(&self) -> bool { self.left_explicit } + #[must_use] pub fn resolve_with_left_defaults(mut self, left_defaults: &DragButtonConfig) -> Self { if self.left_explicit() { self.left.apply_defaults_from(left_defaults); @@ -703,6 +731,7 @@ pub struct DragButtonConfig { } impl DragButtonConfig { + #[must_use] pub fn from_legacy( drag_tool: DragBindableTool, shift_drag_tool: DragBindableTool, @@ -724,6 +753,7 @@ impl DragButtonConfig { } } + #[must_use] pub fn button_behavior() -> Self { Self { drag_tool: DragTool::Default, @@ -780,6 +810,7 @@ impl Default for DragButtonConfig { } impl DrawingConfig { + #[must_use] pub fn effective_drag_tools(&self) -> MouseDragToolsConfig { match self.drag_tools.clone() { Some(drag_tools) => { @@ -837,6 +868,7 @@ fn default_shortcut_quick_color_entries() -> Vec { /// The color the built-in palette ships for a quick-color slot. Slots past the /// built-in palette were added by the user, so they have no default to restore. +#[must_use] pub fn default_quick_color_for_index(index: usize) -> Option { default_quick_color_entries() .get(index) diff --git a/src/config/types/input_hud.rs b/src/config/types/input_hud.rs index 5c4f8e69f..f418df7d3 100644 --- a/src/config/types/input_hud.rs +++ b/src/config/types/input_hud.rs @@ -106,21 +106,25 @@ pub enum InputHudPosition { impl InputHudPosition { /// Whether the row is anchored to the top edge of the screen. + #[must_use] pub fn is_top(self) -> bool { matches!(self, Self::TopLeft | Self::TopCenter | Self::TopRight) } /// Whether the row is vertically centered (the middle grid line). + #[must_use] pub fn is_middle(self) -> bool { matches!(self, Self::CenterLeft | Self::Center | Self::CenterRight) } /// Whether the row is horizontally centered. + #[must_use] pub fn is_center(self) -> bool { matches!(self, Self::TopCenter | Self::Center | Self::BottomCenter) } /// Whether the row is anchored to the right edge of the screen. + #[must_use] pub fn is_right(self) -> bool { matches!(self, Self::TopRight | Self::CenterRight | Self::BottomRight) } diff --git a/src/config/types/mod.rs b/src/config/types/mod.rs index 7a09a868b..8309e8754 100644 --- a/src/config/types/mod.rs +++ b/src/config/types/mod.rs @@ -44,10 +44,9 @@ pub use click_highlight::{ }; pub use context_menu::ContextMenuUiConfig; pub(crate) use drawing::DEFAULT_HIT_TEST_TOLERANCE; -pub use drawing::DEFAULT_PEN_SMOOTHING; -pub use drawing::{DEFAULT_SHAPE_RECOGNITION_SENSITIVITY, MAX_SHAPE_RECOGNITION_SENSITIVITY}; pub use drawing::{ - DragButtonConfig, DrawingConfig, MouseDragToolsConfig, QUICK_COLOR_RENDER_LIMIT, + DEFAULT_PEN_SMOOTHING, DEFAULT_SHAPE_RECOGNITION_SENSITIVITY, DragButtonConfig, DrawingConfig, + MAX_SHAPE_RECOGNITION_SENSITIVITY, MouseDragToolsConfig, QUICK_COLOR_RENDER_LIMIT, QuickColorConfig, QuickColorPalette, QuickColorPaletteEntry, QuickColorSlot, QuickColorWrite, QuickColorsConfig, default_quick_color_for_index, }; @@ -79,7 +78,6 @@ pub use spotlight::SpotlightConfig; pub use status_bar::{StatusBarItem, StatusBarStyle}; #[cfg(feature = "tablet-input")] pub use tablet::{StylusButtonBinding, TabletInputConfig}; -pub use toolbar::ids as toolbar_item_ids; #[allow(unused_imports)] pub use toolbar::{ ResolvedToolbarItems, ToolbarBackendKind, ToolbarConfig, ToolbarGroupId, ToolbarItemCategory, @@ -95,7 +93,8 @@ pub(crate) use toolbar::{ }; pub use toolbar::{ ToolbarSectionFlag, ToolbarSectionVisibility, fold_legacy_section_flags, - resolve_section_visibility, section_flag_for_item, set_section_visibility, + ids as toolbar_item_ids, resolve_section_visibility, section_flag_for_item, + set_section_visibility, }; pub use tray::{TrayConfig, TrayIconStyle}; pub use ui::UiConfig; diff --git a/src/config/types/presets.rs b/src/config/types/presets.rs index 6beaa9974..6d8cef533 100644 --- a/src/config/types/presets.rs +++ b/src/config/types/presets.rs @@ -1,4 +1,5 @@ -use crate::config::{MouseDragToolsConfig, enums::ColorSpec}; +use crate::config::MouseDragToolsConfig; +use crate::config::enums::ColorSpec; use crate::domain::{Color, EraserMode, Tool}; use crate::draw::EraserKind; use serde::{Deserialize, Serialize}; @@ -84,6 +85,7 @@ pub struct ToolPresetConfig { } impl ToolPresetConfig { + #[must_use] pub fn preview_color_spec(&self) -> ColorSpec { self.tool_settings .as_ref() @@ -91,10 +93,12 @@ impl ToolPresetConfig { .unwrap_or_else(|| self.color.clone()) } + #[must_use] pub fn preview_color(&self) -> Color { self.preview_color_spec().to_color() } + #[must_use] pub fn preview_size(&self) -> f64 { self.tool_settings .as_ref() @@ -156,7 +160,8 @@ pub struct PresetSlotsConfig { } impl PresetSlotsConfig { - pub fn get_slot(&self, slot: usize) -> Option<&ToolPresetConfig> { + #[must_use] + pub fn slot(&self, slot: usize) -> Option<&ToolPresetConfig> { match slot { 1 => self.slot_1.as_ref(), 2 => self.slot_2.as_ref(), diff --git a/src/config/types/session.rs b/src/config/types/session.rs index f83545c2c..1d197e0c1 100644 --- a/src/config/types/session.rs +++ b/src/config/types/session.rs @@ -104,7 +104,7 @@ impl Default for SessionConfig { /// Session storage location options. #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum SessionStorageMode { Auto, @@ -114,7 +114,7 @@ pub enum SessionStorageMode { /// Session compression preferences. #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum SessionCompression { Auto, diff --git a/src/config/types/spotlight.rs b/src/config/types/spotlight.rs index b51bc0747..cffde8b07 100644 --- a/src/config/types/spotlight.rs +++ b/src/config/types/spotlight.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use crate::draw::DEFAULT_SPOTLIGHT_MAGNIFICATION; + /// Spotlight tool settings. /// /// A spotlight dims the whole overlay except the regions it covers, so attention @@ -28,7 +30,7 @@ impl Default for SpotlightConfig { Self { dim_opacity: default_spotlight_dim(), feather: default_spotlight_feather(), - magnification: crate::draw::DEFAULT_SPOTLIGHT_MAGNIFICATION, + magnification: DEFAULT_SPOTLIGHT_MAGNIFICATION, } } } diff --git a/src/config/types/status_bar.rs b/src/config/types/status_bar.rs index 84479778b..c2d14e078 100644 --- a/src/config/types/status_bar.rs +++ b/src/config/types/status_bar.rs @@ -25,6 +25,7 @@ impl StatusBarItem { /// /// Persisted, so these strings are a compatibility surface: renaming one /// silently drops the user's saved choice for that item. + #[must_use] pub fn config_id(self) -> &'static str { match self { Self::ActiveOutput => "active_output", @@ -42,6 +43,7 @@ impl StatusBarItem { } /// The item a persisted identifier names, if this build still has it. + #[must_use] pub fn from_config_id(id: &str) -> Option { Self::ALL.into_iter().find(|item| item.config_id() == id) } diff --git a/src/config/types/toolbar/display.rs b/src/config/types/toolbar/display.rs index 5cfec0fd4..1a5e790fe 100644 --- a/src/config/types/toolbar/display.rs +++ b/src/config/types/toolbar/display.rs @@ -25,6 +25,7 @@ impl TopDisplayMode { /// The form persisted to config: `Hidden` collapses to `Full` so a /// cycle-hidden strip comes back visible on the next start (visibility /// at startup stays `top_pinned`'s job). + #[must_use] pub fn persisted(self) -> Self { match self { Self::Hidden => Self::Full, diff --git a/src/config/types/toolbar/items.rs b/src/config/types/toolbar/items.rs index 20ac23053..5311463d6 100644 --- a/src/config/types/toolbar/items.rs +++ b/src/config/types/toolbar/items.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use std::fmt; use std::str::FromStr; use std::sync::LazyLock; +use std::{fmt, mem}; use serde::{Deserialize, Serialize}; @@ -115,6 +115,7 @@ impl Default for ToolbarItemsConfig { } impl ToolbarItemsConfig { + #[must_use] pub fn resolved(&self) -> ResolvedToolbarItems { let mut hidden = BTreeSet::new(); let mut unknown_hidden = Vec::new(); @@ -149,8 +150,8 @@ impl ToolbarItemsConfig { } pub fn set_hidden(&mut self, id: ToolbarItemId, hidden: bool) { - self.hidden = drain_without(std::mem::take(&mut self.hidden), id); - self.shown = drain_without(std::mem::take(&mut self.shown), id); + self.hidden = drain_without(mem::take(&mut self.hidden), id); + self.shown = drain_without(mem::take(&mut self.shown), id); if hidden { self.hidden.push(id.as_str().to_string()); } else if super::visibility::section_flag_for_item(id).is_some() @@ -179,8 +180,8 @@ impl ToolbarItemsConfig { } let before_hidden = self.hidden.clone(); let before_shown = self.shown.clone(); - self.hidden = drain_without(std::mem::take(&mut self.hidden), id); - self.shown = drain_without(std::mem::take(&mut self.shown), id); + self.hidden = drain_without(mem::take(&mut self.hidden), id); + self.shown = drain_without(mem::take(&mut self.shown), id); match setting { ToolbarItemVisibilitySetting::Default => {} ToolbarItemVisibilitySetting::Hidden => { @@ -279,6 +280,7 @@ pub struct ResolvedToolbarItems { } impl ResolvedToolbarItems { + #[must_use] pub fn is_hidden(&self, id: ToolbarItemId) -> bool { if self.hidden.contains(&id) { return true; @@ -353,6 +355,7 @@ impl ToolbarItemId { Self(value) } + #[must_use] pub const fn as_str(self) -> &'static str { self.0 } @@ -444,6 +447,7 @@ pub enum ToolbarGroupId { } impl ToolbarGroupId { + #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::StepUndo => "step-undo", diff --git a/src/config/types/toolbar/items/definitions.rs b/src/config/types/toolbar/items/definitions.rs index dc4f58d16..510c8c62c 100644 --- a/src/config/types/toolbar/items/definitions.rs +++ b/src/config/types/toolbar/items/definitions.rs @@ -1,12 +1,17 @@ use std::sync::OnceLock; -use super::*; +use super::super::ids; +use super::{ + ToolbarGroupId, ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemId, ToolbarItemSurface, +}; use crate::domain::Language; +use crate::i18n; /// The toolbar items, labeled in the active UI language. +#[must_use] pub fn toolbar_item_definitions() -> &'static [ToolbarItemDefinition] { - toolbar_item_definitions_in(crate::i18n::language()) + toolbar_item_definitions_in(i18n::language()) } /// One table per supported language, built on first use and kept for the @@ -28,7 +33,7 @@ pub(crate) fn toolbar_item_definitions_in(language: Language) -> &'static [Toolb /// two languages even when the active language changes while it is built. fn build_toolbar_item_definitions(language: Language) -> Vec { // Named `tr` so each label keeps the spelling the catalog guard reads. - let tr = |source: &'static str| crate::i18n::lookup(language, source); + let tr = |source: &'static str| i18n::lookup(language, source); vec![ item( diff --git a/src/config/types/toolbar/items/order.rs b/src/config/types/toolbar/items/order.rs index f4cd673b5..81b57c318 100644 --- a/src/config/types/toolbar/items/order.rs +++ b/src/config/types/toolbar/items/order.rs @@ -1,4 +1,10 @@ -use super::*; +use super::definitions::toolbar_item_definitions; +use super::{ + DEFAULT_TOP_CONTROLS_ORDER, DEFAULT_TOP_TOOLS_ORDER, ToolbarItemCategory, + ToolbarItemDefinition, ToolbarItemId, ToolbarItemSurface, +}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] @@ -10,6 +16,7 @@ pub struct ToolbarItemOrderConfig { } impl ToolbarItemOrderConfig { + #[must_use] pub fn resolved(&self) -> ResolvedToolbarOrder { ResolvedToolbarOrder { top_tools: resolve_order_group(ToolbarItemOrderGroup::TopTools, &self.top_tools), @@ -110,6 +117,7 @@ impl ToolbarItemOrderGroup { pub(crate) const ALL: [Self; 2] = [Self::TopTools, Self::TopControls]; } +#[must_use] pub fn toolbar_item_order_group( definition: &ToolbarItemDefinition, ) -> Option { diff --git a/src/config/types/toolbar/items/tests.rs b/src/config/types/toolbar/items/tests.rs index 3a3ff7404..3880db819 100644 --- a/src/config/types/toolbar/items/tests.rs +++ b/src/config/types/toolbar/items/tests.rs @@ -1,3 +1,7 @@ +use std::ptr; + +use crate::{i18n, test_env}; + use super::*; #[test] @@ -348,7 +352,7 @@ fn toolbar_item_definitions_are_unique_parseable_and_labeled() { #[test] fn each_language_keeps_one_table_with_the_same_items() { - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::each_language_keeps_one_table_with_the_same_items" @@ -367,12 +371,12 @@ fn each_language_keeps_one_table_with_the_same_items() { Language::SimplifiedChinese, Language::English, ] { - crate::i18n::set_language(language); + i18n::set_language(language); let expected = match language { Language::SimplifiedChinese => chinese, _ => english, }; - assert!(std::ptr::eq(toolbar_item_definitions(), expected)); + assert!(ptr::eq(toolbar_item_definitions(), expected)); } assert_eq!(english.len(), chinese.len()); diff --git a/src/config/types/toolbar/mode.rs b/src/config/types/toolbar/mode.rs index fe33f8057..5e4e919cb 100644 --- a/src/config/types/toolbar/mode.rs +++ b/src/config/types/toolbar/mode.rs @@ -18,6 +18,7 @@ impl ToolbarLayoutMode { /// The next preset, simplest to richest; wraps from Advanced back to /// Simple. + #[must_use] pub fn next(self) -> Self { match self { Self::Simple => Self::Regular, @@ -26,6 +27,7 @@ impl ToolbarLayoutMode { } } + #[must_use] pub fn section_defaults(self) -> ToolbarSectionDefaults { match self { Self::Simple => ToolbarSectionDefaults { diff --git a/src/config/types/toolbar/overrides.rs b/src/config/types/toolbar/overrides.rs index 4ab6e5776..bbacc75cb 100644 --- a/src/config/types/toolbar/overrides.rs +++ b/src/config/types/toolbar/overrides.rs @@ -52,6 +52,7 @@ pub struct ToolbarModeOverrides { } impl ToolbarModeOverrides { + #[must_use] pub fn for_mode(&self, mode: ToolbarLayoutMode) -> &ToolbarModeOverride { match mode { ToolbarLayoutMode::Simple => &self.simple, diff --git a/src/config/types/toolbar/rebind.rs b/src/config/types/toolbar/rebind.rs index 77588288e..b9d9fdf97 100644 --- a/src/config/types/toolbar/rebind.rs +++ b/src/config/types/toolbar/rebind.rs @@ -14,6 +14,7 @@ pub enum ToolbarRebindModifier { } impl ToolbarRebindModifier { + #[must_use] pub fn matches(self, ctrl: bool, shift: bool, alt: bool) -> bool { match self { Self::Disabled => false, @@ -28,6 +29,7 @@ impl ToolbarRebindModifier { /// `None` when rebind-by-click is disabled. The single source of truth for /// shortcut-rebind copy across the onboarding surfaces (the first-run /// cards), so no key strings are ever hardcoded. + #[must_use] pub fn click_label(self) -> Option<&'static str> { match self { Self::Disabled => None, diff --git a/src/config/types/toolbar/visibility.rs b/src/config/types/toolbar/visibility.rs index cd235cf56..ca2b6d059 100644 --- a/src/config/types/toolbar/visibility.rs +++ b/src/config/types/toolbar/visibility.rs @@ -41,6 +41,7 @@ impl ToolbarSectionFlag { ]; /// The config item id that carries explicit overrides for this flag. + #[must_use] pub fn item_id(self) -> ToolbarItemId { match self { Self::Actions => ids::SIDE_GROUP_ACTIONS, @@ -56,6 +57,7 @@ impl ToolbarSectionFlag { /// Baseline visibility for the flag under `mode`: the mode's defaults /// adjusted by the per-mode config overrides. + #[must_use] pub fn baseline(self, mode: ToolbarLayoutMode, overrides: &ToolbarModeOverrides) -> bool { let defaults = mode.section_defaults(); let over = overrides.for_mode(mode); @@ -83,6 +85,7 @@ impl ToolbarSectionFlag { } /// The flag carried by a config item id, if any. +#[must_use] pub fn section_flag_for_item(id: ToolbarItemId) -> Option { ToolbarSectionFlag::ALL .into_iter() @@ -133,6 +136,7 @@ impl ToolbarSectionVisibility { } } + #[must_use] pub fn get(&self, flag: ToolbarSectionFlag) -> bool { match flag { ToolbarSectionFlag::Actions => self.show_actions_section, @@ -149,6 +153,7 @@ impl ToolbarSectionVisibility { /// Resolve the effective section visibility: explicit item overrides win, /// then the mode baseline. +#[must_use] pub fn resolve_section_visibility( mode: ToolbarLayoutMode, overrides: &ToolbarModeOverrides, diff --git a/src/config/types/ui.rs b/src/config/types/ui.rs index ca8e842f7..39c4d9209 100644 --- a/src/config/types/ui.rs +++ b/src/config/types/ui.rs @@ -239,6 +239,7 @@ impl Default for UiConfig { } impl UiConfig { + #[must_use] pub fn status_bar_item_visible(&self, item: StatusBarItem) -> bool { match item { StatusBarItem::ActiveOutput => self.active_output_badge, diff --git a/src/config/types/updates.rs b/src/config/types/updates.rs index 7a4358470..caca5c023 100644 --- a/src/config/types/updates.rs +++ b/src/config/types/updates.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use serde::{Deserialize, Serialize}; /// Default gap between background update checks. @@ -42,9 +44,10 @@ impl Default for UpdatesConfig { impl UpdatesConfig { /// Interval clamped to the supported range. - pub fn interval(&self) -> std::time::Duration { + #[must_use] + pub fn interval(&self) -> Duration { let hours = self.interval_hours.max(MIN_UPDATE_CHECK_INTERVAL_HOURS); - std::time::Duration::from_secs(u64::from(hours) * 3600) + Duration::from_secs(u64::from(hours) * 3600) } } @@ -66,7 +69,7 @@ mod tests { assert!(config.check); assert!(config.notify); - assert_eq!(config.interval(), std::time::Duration::from_secs(86_400)); + assert_eq!(config.interval(), Duration::from_secs(86_400)); } #[test] @@ -76,7 +79,7 @@ mod tests { ..UpdatesConfig::default() }; - assert_eq!(config.interval(), std::time::Duration::from_secs(3600)); + assert_eq!(config.interval(), Duration::from_secs(3600)); } #[test] diff --git a/src/config/validate/arrow.rs b/src/config/validate/arrow.rs index 82fbf33d4..9bee813f1 100644 --- a/src/config/validate/arrow.rs +++ b/src/config/validate/arrow.rs @@ -1,9 +1,12 @@ -use super::{Config, float::finite_or_default}; +use crate::config::ArrowConfig; + +use super::Config; +use super::float::finite_or_default; use crate::config::{ARROW_ANGLE_MAX, ARROW_ANGLE_MIN, ARROW_LENGTH_MAX, ARROW_LENGTH_MIN}; impl Config { pub(super) fn validate_arrow(&mut self) { - let defaults = crate::config::ArrowConfig::default(); + let defaults = ArrowConfig::default(); self.arrow.length = finite_or_default(self.arrow.length, defaults.length, "arrow.length"); self.arrow.angle_degrees = finite_or_default( self.arrow.angle_degrees, diff --git a/src/config/validate/board.rs b/src/config/validate/board.rs index 9f2326e9b..adece002d 100644 --- a/src/config/validate/board.rs +++ b/src/config/validate/board.rs @@ -1,4 +1,7 @@ -use super::{Config, float::finite_or_default}; +use crate::config::BoardConfig; + +use super::Config; +use super::float::finite_or_default; impl Config { pub(super) fn validate_board(&mut self) { @@ -14,7 +17,7 @@ impl Config { self.board.default_mode = "transparent".to_string(); } - let defaults = crate::config::BoardConfig::default(); + let defaults = BoardConfig::default(); for (field, color, fallback) in [ ( "board.whiteboard_color", diff --git a/src/config/validate/boards.rs b/src/config/validate/boards.rs index 5e3fca0b0..68cfc66bf 100644 --- a/src/config/validate/boards.rs +++ b/src/config/validate/boards.rs @@ -1,8 +1,10 @@ +use crate::config::BoardItemConfig; use crate::config::types::{BoardBackgroundConfig, BoardColorConfig, BoardsConfig}; -use crate::domain::{BoundaryBoardIdSet, clamp_board_rgb}; +use crate::domain::{BoardGrid, BoardGridKind, BoundaryBoardIdSet, clamp_board_rgb}; use log::warn; -use super::{Config, float::finite_or_default}; +use super::Config; +use super::float::finite_or_default; impl Config { pub(super) fn validate_boards(&mut self) { @@ -85,8 +87,8 @@ impl Config { } } -fn normalize_grid(item: &mut crate::config::BoardItemConfig) { - let mut grid = crate::domain::BoardGrid::from(item.grid); +fn normalize_grid(item: &mut BoardItemConfig) { + let mut grid = BoardGrid::from(item.grid); if i64::from(grid.spacing()) != item.grid.spacing { warn!( "Board '{}' grid spacing {} is outside 8–200; using {}", @@ -95,7 +97,7 @@ fn normalize_grid(item: &mut crate::config::BoardItemConfig) { grid.spacing() ); } - if item.background.is_transparent() && grid.kind != crate::domain::BoardGridKind::None { + if item.background.is_transparent() && grid.kind != BoardGridKind::None { warn!("Board '{}' is transparent; disabling its grid", item.id); grid = grid.disabled(); } diff --git a/src/config/validate/drawing.rs b/src/config/validate/drawing.rs index 9f8204384..5a896d8a3 100644 --- a/src/config/validate/drawing.rs +++ b/src/config/validate/drawing.rs @@ -1,11 +1,16 @@ -use super::{Config, float::finite_or_default}; +use std::collections::BTreeSet; + +use crate::config::DrawingConfig; + +use super::Config; +use super::float::finite_or_default; use crate::config::types::{DEFAULT_HIT_TEST_TOLERANCE, MAX_SHAPE_RECOGNITION_SENSITIVITY}; use crate::domain::{MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; use crate::draw::shape::{MAX_PEN_SMOOTHING, REGULAR_POLYGON_MAX_SIDES, REGULAR_POLYGON_MIN_SIDES}; impl Config { pub(super) fn validate_drawing(&mut self) { - let defaults = crate::config::DrawingConfig::default(); + let defaults = DrawingConfig::default(); for (field, value, fallback) in [ ( "drawing.default_thickness", @@ -40,7 +45,7 @@ impl Config { // Repeats are judged without case, the way fontconfig resolves a family // name. `["Sans", "sans"]` is one font written twice, and keeping both // would leave a step that changes only the spelling. - let mut seen = std::collections::BTreeSet::new(); + let mut seen = BTreeSet::new(); let before = self.drawing.font_cycle.len(); self.drawing.font_cycle.retain(|family| { let trimmed = family.trim(); diff --git a/src/config/validate/keybindings.rs b/src/config/validate/keybindings.rs index bf2cd3882..07c36021c 100644 --- a/src/config/validate/keybindings.rs +++ b/src/config/validate/keybindings.rs @@ -85,26 +85,31 @@ pub enum KeybindingProblem { impl InvalidKeybinding { /// Whether this binding was dropped from the session keymap. + #[must_use] pub fn was_dropped(&self) -> bool { matches!(self.problem, KeybindingProblem::Unparseable { .. }) } /// The problem this binding has. + #[must_use] pub fn problem(&self) -> &KeybindingProblem { &self.problem } /// The rejected string exactly as the file spells it. + #[must_use] pub fn binding(&self) -> &str { &self.binding } /// The `[keybindings]` key the string was removed from. + #[must_use] pub fn config_key(&self) -> Option<&'static str> { KeybindingsConfig::config_key_for_action(self.action) } /// Toast-sized wording; [`fmt::Display`] carries the long form. + #[must_use] pub fn summary(&self) -> String { match &self.problem { KeybindingProblem::Unparseable { .. } => format!( @@ -173,42 +178,50 @@ impl KeybindingConflictResolution { /// /// For an exact conflict this is also the shortcut the winner kept. Use /// [`Self::kept_key`] when a sequence conflicts with one of its prefixes. + #[must_use] pub fn key(&self) -> &str { &self.dropped_key } /// The shortcut retained by the winning action, in normalized form. + #[must_use] pub fn kept_key(&self) -> &str { &self.kept_key } /// The shortcut removed from the losing action, in normalized form. + #[must_use] pub fn dropped_key(&self) -> &str { &self.dropped_key } /// The action that keeps the shortcut for this session. + #[must_use] pub fn kept(&self) -> Action { self.kept } /// The action the shortcut was removed from for this session. + #[must_use] pub fn dropped(&self) -> Action { self.dropped } /// Whether one action listed the same shortcut more than once, rather than /// two actions claiming it. + #[must_use] pub fn is_self_duplicate(&self) -> bool { self.kept == self.dropped && self.kept_key == self.dropped_key } /// The `[keybindings]` key the shortcut was removed from. + #[must_use] pub fn dropped_config_key(&self) -> Option<&'static str> { KeybindingsConfig::config_key_for_action(self.dropped) } /// Toast-sized wording; [`fmt::Display`] carries the long form. + #[must_use] pub fn summary(&self) -> String { if self.is_self_duplicate() { format!( @@ -357,16 +370,19 @@ pub struct DefaultShortcutSkipped { impl DefaultShortcutSkipped { /// The omitted action that would have received the shortcut. + #[must_use] pub fn action(&self) -> Action { self.action } /// The skipped shortcut in its normalized form (`Ctrl+Shift+K`). + #[must_use] pub fn binding(&self) -> &str { &self.binding } /// The action the configuration gives the shortcut to instead. + #[must_use] pub fn claimed_by(&self) -> Action { self.claimed_by } @@ -374,11 +390,13 @@ impl DefaultShortcutSkipped { /// The `[keybindings]` key that would hold the skipped default. It is /// absent from the file, which is the whole reason the default was on /// offer. + #[must_use] pub fn config_key(&self) -> Option<&'static str> { KeybindingsConfig::config_key_for_action(self.action) } /// Toast-sized wording; [`fmt::Display`] carries the long form. + #[must_use] pub fn summary(&self) -> String { format!( "{} stays with {}, so the new default for {} is inactive.", diff --git a/src/config/validate/mod.rs b/src/config/validate/mod.rs index b17ba6e94..8a295dfda 100644 --- a/src/config/validate/mod.rs +++ b/src/config/validate/mod.rs @@ -45,6 +45,7 @@ pub struct ConfigValidationReport { impl ConfigValidationReport { /// Whether validation changed nothing the user needs to know about. + #[must_use] pub fn is_empty(&self) -> bool { self.invalid_keybindings.is_empty() && self.keybinding_conflicts.is_empty() diff --git a/src/config/validate/presets.rs b/src/config/validate/presets.rs index 029a10b92..88574be89 100644 --- a/src/config/validate/presets.rs +++ b/src/config/validate/presets.rs @@ -1,4 +1,5 @@ -use super::{Config, float::finite_or_default}; +use super::Config; +use super::float::finite_or_default; use crate::domain::{MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; use crate::draw::{REGULAR_POLYGON_MAX_SIDES, REGULAR_POLYGON_MIN_SIDES, clamp_regular_sides}; diff --git a/src/config/validate/render_profiles.rs b/src/config/validate/render_profiles.rs index eea7f1b83..a6b1a1a19 100644 --- a/src/config/validate/render_profiles.rs +++ b/src/config/validate/render_profiles.rs @@ -1,5 +1,7 @@ use std::collections::HashSet; +use crate::config::RenderColorMappingConfig; + use super::Config; use crate::config::{RenderProfileConfig, RenderProfileExportMode}; use crate::render_profiles::{format_hex_rgb, normalize_profile_id, parse_hex_rgb}; @@ -143,7 +145,7 @@ fn normalize_profile_mappings(profile: &mut RenderProfileConfig) { ); continue; } - normalized.push(crate::config::RenderColorMappingConfig { + normalized.push(RenderColorMappingConfig { from: format_hex_rgb(from), to: format_hex_rgb(to), }); diff --git a/src/config/validate/save.rs b/src/config/validate/save.rs index 2db03393b..b5c141483 100644 --- a/src/config/validate/save.rs +++ b/src/config/validate/save.rs @@ -1,3 +1,8 @@ +use std::fmt::{Display, Formatter}; +use std::{error, fmt}; + +use toml::ser; + use super::{Config, ConfigValidationReport}; /// Why an authored configuration cannot be saved without losing user input. @@ -6,7 +11,7 @@ pub enum SaveValidationError { /// Validation would change these values on their way to disk. Never /// empty: an unchanged configuration is accepted. CorrectedValues(Vec), - Representation(toml::ser::Error), + Representation(ser::Error), } /// One persisted value that validation would change on save. @@ -26,6 +31,7 @@ pub struct CorrectedValue { impl CorrectedValue { /// The change without the path, for a caller that shows the path itself. + #[must_use] pub fn summary(&self) -> String { match (&self.authored, &self.corrected) { (Some(authored), Some(corrected)) => format!( @@ -44,8 +50,8 @@ impl CorrectedValue { } } -impl std::fmt::Display for CorrectedValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for CorrectedValue { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}: {}", self.path, self.summary()) } } @@ -63,8 +69,8 @@ fn describe_value(value: &toml::Value) -> String { } } -impl std::fmt::Display for SaveValidationError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for SaveValidationError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::CorrectedValues(values) => { f.write_str("Some values would be changed on save. Fix them before saving:")?; @@ -80,16 +86,16 @@ impl std::fmt::Display for SaveValidationError { } } } -impl std::error::Error for SaveValidationError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { +impl error::Error for SaveValidationError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { match self { Self::Representation(error) => Some(error), Self::CorrectedValues(_) => None, } } } -impl From for SaveValidationError { - fn from(error: toml::ser::Error) -> Self { +impl From for SaveValidationError { + fn from(error: ser::Error) -> Self { Self::Representation(error) } } diff --git a/src/config/validate/spotlight.rs b/src/config/validate/spotlight.rs index 800c94252..b78708270 100644 --- a/src/config/validate/spotlight.rs +++ b/src/config/validate/spotlight.rs @@ -1,3 +1,7 @@ +use crate::draw::{ + MAX_SPOTLIGHT_MAGNIFICATION, MIN_SPOTLIGHT_MAGNIFICATION, normalize_spotlight_magnification, +}; + use super::Config; use crate::config::SpotlightConfig; @@ -45,18 +49,17 @@ impl Config { defaults.magnification ); self.spotlight.magnification = defaults.magnification; - } else if !(crate::draw::MIN_SPOTLIGHT_MAGNIFICATION - ..=crate::draw::MAX_SPOTLIGHT_MAGNIFICATION) + } else if !(MIN_SPOTLIGHT_MAGNIFICATION..=MAX_SPOTLIGHT_MAGNIFICATION) .contains(&self.spotlight.magnification) { log::warn!( "Invalid spotlight magnification {:.2}, clamping to {:.1}-{:.1} range", self.spotlight.magnification, - crate::draw::MIN_SPOTLIGHT_MAGNIFICATION, - crate::draw::MAX_SPOTLIGHT_MAGNIFICATION + MIN_SPOTLIGHT_MAGNIFICATION, + MAX_SPOTLIGHT_MAGNIFICATION ); self.spotlight.magnification = - crate::draw::normalize_spotlight_magnification(self.spotlight.magnification); + normalize_spotlight_magnification(self.spotlight.magnification); } } } diff --git a/src/config/validate/tablet.rs b/src/config/validate/tablet.rs index 203c87655..1a83eb3bf 100644 --- a/src/config/validate/tablet.rs +++ b/src/config/validate/tablet.rs @@ -1,9 +1,14 @@ -use super::{Config, float::finite_or_default}; +use std::mem; + +use crate::config::TabletInputConfig; + +use super::Config; +use super::float::finite_or_default; use crate::domain::{MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; impl Config { pub(super) fn validate_tablet(&mut self) { - let defaults = crate::config::TabletInputConfig::default(); + let defaults = TabletInputConfig::default(); for (field, value, fallback) in [ ( "tablet.min_thickness", @@ -30,7 +35,7 @@ impl Config { } if self.tablet.min_thickness > self.tablet.max_thickness { - std::mem::swap( + mem::swap( &mut self.tablet.min_thickness, &mut self.tablet.max_thickness, ); diff --git a/src/config/validate/ui/styles.rs b/src/config/validate/ui/styles.rs index 8a8ef79f1..cc3ae0089 100644 --- a/src/config/validate/ui/styles.rs +++ b/src/config/validate/ui/styles.rs @@ -1,10 +1,13 @@ -use super::super::{Config, float::finite_or_default}; +use crate::config::UiConfig; + +use super::super::Config; +use super::super::float::finite_or_default; impl Config { /// These styling fields have no load-time finite bounds. Preserve authored /// finite values, but keep NaN/infinity out of layout and Cairo consumers. pub(super) fn validate_ui_styles(&mut self) { - let defaults = crate::config::UiConfig::default(); + let defaults = UiConfig::default(); for (field, value, fallback) in [ ( "ui.toolbar.top_offset", diff --git a/src/configurator_destination.rs b/src/configurator_destination.rs index a9f5f05f9..a015e4edc 100644 --- a/src/configurator_destination.rs +++ b/src/configurator_destination.rs @@ -43,16 +43,19 @@ const QUICK_COLORS_SEARCH: &str = "Quick Colors"; const ONBOARDING_HINTS_SEARCH: &str = "Show automatic guidance and tips"; /// The Drawing screen, focused on the quick-color palette. +#[must_use] pub fn quick_colors_destination() -> ConfiguratorDestination { ConfiguratorDestination::with_search(ConfiguratorScreen::Drawing, QUICK_COLORS_SEARCH) } /// The General UI section, filtered to the automatic-guidance preference. +#[must_use] pub fn onboarding_hints_destination() -> ConfiguratorDestination { ConfiguratorDestination::with_search(ConfiguratorScreen::UiToolbar, ONBOARDING_HINTS_SEARCH) } /// General UI, filtered to the interface language setting. +#[must_use] pub fn language_destination() -> ConfiguratorDestination { ConfiguratorDestination::with_search(ConfiguratorScreen::UiToolbar, "language") } @@ -89,6 +92,7 @@ pub enum KeybindingsSection { /// storage layout and is spelled out here instead. /// `every_action_section_matches_the_field_tab_that_holds_it` (in the /// configurator crate, which can see both sides) keeps the conversion aligned. +#[must_use] pub fn keybindings_section_for_action(action: Action) -> Option { let section = match action { Action::Exit | Action::OpenConfigurator | Action::OpenAbout => KeybindingsSection::General, @@ -274,6 +278,7 @@ pub fn keybindings_section_for_action(action: Action) -> Option Option { let section = keybindings_section_for_action(action)?; let key = KeybindingsConfig::config_key_for_action(action)?; @@ -347,6 +352,7 @@ impl ConfiguratorScreen { ]; /// The token that names this screen on the command line. + #[must_use] pub const fn as_arg(self) -> &'static str { match self { Self::UiToolbar => "ui/toolbar", @@ -382,6 +388,7 @@ impl ConfiguratorScreen { } /// The screen a token names, if this build has it. + #[must_use] pub fn parse(token: &str) -> Option { match token { "ui/toolbar" => Some(Self::UiToolbar), @@ -434,6 +441,7 @@ pub struct ConfiguratorDestination { impl ConfiguratorDestination { /// A destination that only names a screen. + #[must_use] pub const fn new(screen: ConfiguratorScreen) -> Self { Self { screen, @@ -454,10 +462,12 @@ impl ConfiguratorDestination { } } + #[must_use] pub const fn screen(&self) -> ConfiguratorScreen { self.screen } + #[must_use] pub fn search(&self) -> Option<&str> { self.search.as_deref() } @@ -468,6 +478,7 @@ impl ConfiguratorDestination { /// token. Nothing needs escaping: the token goes from the broker straight /// into `exec`, never through a shell or a file, so a term may contain /// spaces, `?`, and `=` and still come back unchanged. + #[must_use] pub fn as_arg(&self) -> String { let screen = self.screen.as_arg(); match &self.search { @@ -481,6 +492,7 @@ impl ConfiguratorDestination { /// A screen this build does not have, or a query it does not understand, /// yields `None` rather than a partial landing: the configurator says so /// and opens its usual first screen instead. + #[must_use] pub fn parse(text: &str) -> Option { let (screen_token, term) = match text.split_once(QUERY_SEPARATOR) { None => (text, None), diff --git a/src/daemon/binary_conflict.rs b/src/daemon/binary_conflict.rs index 4e06dd5ae..dcf713c54 100644 --- a/src/daemon/binary_conflict.rs +++ b/src/daemon/binary_conflict.rs @@ -94,6 +94,8 @@ pub(super) fn warn_if_other_wayscriber_binaries() { #[cfg(test)] mod tests { + use crate::{env_vars, test_env, test_temp}; + use super::*; use std::fs; use std::os::unix::fs::symlink; @@ -104,7 +106,7 @@ mod tests { #[test] fn no_conflict_when_path_and_extras_are_the_same_file() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let binary = tmp.path().join("wayscriber"); write_file(&binary); let path_env = tmp.path().display().to_string(); @@ -116,7 +118,7 @@ mod tests { #[test] fn reports_a_second_file_on_path() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let current_dir = tmp.path().join("current"); let other_dir = tmp.path().join("other"); fs::create_dir(¤t_dir).unwrap(); @@ -133,7 +135,7 @@ mod tests { #[test] fn reports_a_well_known_copy_even_when_path_matches_current() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let current_dir = tmp.path().join("local"); let usr_dir = tmp.path().join("usr"); fs::create_dir(¤t_dir).unwrap(); @@ -150,7 +152,7 @@ mod tests { #[test] fn symlink_to_the_same_file_is_not_a_conflict() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let current = tmp.path().join("wayscriber"); let alias = tmp.path().join("alias-wayscriber"); write_file(¤t); @@ -161,7 +163,7 @@ mod tests { #[test] fn missing_current_exe_yields_no_others() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let missing = tmp.path().join("missing"); let other = tmp.path().join("wayscriber"); write_file(&other); @@ -171,16 +173,12 @@ mod tests { #[test] fn well_known_includes_resolved_user_local_bin() { - let tmp = crate::test_temp::tempdir().unwrap(); - crate::test_env::with_env_var( - crate::env_vars::HOME_ENV, - Some(tmp.path().as_os_str()), - || { - let paths = well_known_wayscriber_binaries(); - assert!(paths.contains(&PathBuf::from("/usr/bin/wayscriber"))); - assert!(paths.contains(&PathBuf::from("/usr/local/bin/wayscriber"))); - assert!(paths.contains(&tmp.path().join(".local").join("bin").join("wayscriber"))); - }, - ); + let tmp = test_temp::tempdir().unwrap(); + test_env::with_env_var(env_vars::HOME_ENV, Some(tmp.path().as_os_str()), || { + let paths = well_known_wayscriber_binaries(); + assert!(paths.contains(&PathBuf::from("/usr/bin/wayscriber"))); + assert!(paths.contains(&PathBuf::from("/usr/local/bin/wayscriber"))); + assert!(paths.contains(&tmp.path().join(".local").join("bin").join("wayscriber"))); + }); } } diff --git a/src/daemon/control.rs b/src/daemon/control.rs index 1f18cf73e..1aff11dfa 100644 --- a/src/daemon/control.rs +++ b/src/daemon/control.rs @@ -1,13 +1,21 @@ +use std::io::ErrorKind; +use std::{io, process}; + use anyhow::{Context, Result, anyhow}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use crate::daemon::protocol_v2; +use crate::daemon::protocol_v2::{ + ClassifiedRuntimeRecord, ClientCommand, DaemonRequestV2, TerminalCommandResult, +}; #[cfg(test)] use crate::env_vars::XDG_RUNTIME_DIR_ENV; #[cfg(test)] use crate::paths::daemon_command_dir; use crate::tray_action::TrayAction; +use crate::{paths, session}; mod queue; mod response; @@ -147,7 +155,7 @@ impl DaemonToggleRequest { )); } let normalized = normalize_daemon_session_file(path)?; - crate::session::validate_named_session_file_for_foreground(&normalized)?; + session::validate_named_session_file_for_foreground(&normalized)?; self.session_file = Some(normalized); Ok(()) } @@ -157,7 +165,7 @@ fn normalize_daemon_session_file(path: &Path) -> Result { let raw = path .to_str() .ok_or_else(|| anyhow!("--session-file path must be valid UTF-8"))?; - Ok(crate::session::normalize_named_session_file_arg(raw)) + Ok(session::normalize_named_session_file_arg(raw)) } fn current_unix_millis() -> Result { @@ -175,33 +183,32 @@ pub(crate) fn generate_daemon_instance_token() -> String { .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_nanos(); - format!("{:x}-{:x}", std::process::id(), now) + format!("{:x}-{:x}", process::id(), now) } pub(crate) fn send_daemon_toggle_request(request: &DaemonToggleRequest) -> Result<()> { ensure_daemon_running()?; - let record = - match crate::daemon::protocol_v2::read_runtime_record(&crate::paths::daemon_pid_file()) { - Ok(record) => record, - // The daemon takes its lock before it publishes the record, so a lock - // held with no record means one is still starting up. - Err(error) if is_not_found(&error) => { - return Err(anyhow!( - "wayscriber daemon is starting and not ready yet; try again" - )); - } - Err(error) => return Err(error), - }; + let record = match protocol_v2::read_runtime_record(&paths::daemon_pid_file()) { + Ok(record) => record, + // The daemon takes its lock before it publishes the record, so a lock + // held with no record means one is still starting up. + Err(error) if is_not_found(&error) => { + return Err(anyhow!( + "wayscriber daemon is starting and not ready yet; try again" + )); + } + Err(error) => return Err(error), + }; match record { - crate::daemon::protocol_v2::ClassifiedRuntimeRecord::V2(runtime) => { - let command = crate::daemon::protocol_v2::ClientCommand::publish( - &crate::daemon::protocol_v2::DaemonRequestV2::from(request), + ClassifiedRuntimeRecord::V2(runtime) => { + let command = ClientCommand::publish( + &DaemonRequestV2::from(request), &runtime.v2_instance_token, )?; return finish_v2_command(command.wait()?); } - crate::daemon::protocol_v2::ClassifiedRuntimeRecord::LegacyV1 { .. } => {} + ClassifiedRuntimeRecord::LegacyV1 { .. } => {} } let runtime = read_daemon_runtime_info()?; @@ -231,26 +238,22 @@ pub(crate) fn send_daemon_toggle_request(request: &DaemonToggleRequest) -> Resul fn is_not_found(error: &anyhow::Error) -> bool { error.chain().any(|cause| { cause - .downcast_ref::() - .is_some_and(|error| error.kind() == std::io::ErrorKind::NotFound) + .downcast_ref::() + .is_some_and(|error| error.kind() == ErrorKind::NotFound) }) } -fn finish_v2_command(result: crate::daemon::protocol_v2::TerminalCommandResult) -> Result<()> { +fn finish_v2_command(result: TerminalCommandResult) -> Result<()> { match result { - crate::daemon::protocol_v2::TerminalCommandResult::Succeeded => Ok(()), - crate::daemon::protocol_v2::TerminalCommandResult::Canceled => { + TerminalCommandResult::Succeeded => Ok(()), + TerminalCommandResult::Canceled => { Err(anyhow!("daemon command was canceled before any effect")) } - crate::daemon::protocol_v2::TerminalCommandResult::FailedNoEffect(reason) => { - Err(anyhow!(reason)) - } - crate::daemon::protocol_v2::TerminalCommandResult::AdmittedIndeterminate(reason) => { - Err(anyhow!( - "daemon command was admitted but its outcome is indeterminate; do not retry: {reason}" - )) - } - crate::daemon::protocol_v2::TerminalCommandResult::CommittedIndeterminate(reason) => { + TerminalCommandResult::FailedNoEffect(reason) => Err(anyhow!(reason)), + TerminalCommandResult::AdmittedIndeterminate(reason) => Err(anyhow!( + "daemon command was admitted but its outcome is indeterminate; do not retry: {reason}" + )), + TerminalCommandResult::CommittedIndeterminate(reason) => { Err(anyhow!("daemon command outcome is indeterminate: {reason}")) } } diff --git a/src/daemon/control/queue.rs b/src/daemon/control/queue.rs index a7031b6fe..eee4beed0 100644 --- a/src/daemon/control/queue.rs +++ b/src/daemon/control/queue.rs @@ -1,3 +1,7 @@ +use std::process; + +use crate::durable_io; + use super::{ DaemonToggleCommand, DaemonToggleCommands, DaemonToggleEnvelope, DaemonToggleRequest, MAX_DAEMON_TOGGLE_REQUEST_AGE, current_unix_millis, @@ -12,7 +16,7 @@ use std::io::ErrorKind; use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -pub(super) fn clear_file(path: &std::path::Path) -> Result<()> { +pub(super) fn clear_file(path: &Path) -> Result<()> { match fs::remove_file(path) { Ok(()) => Ok(()), Err(err) if err.kind() == ErrorKind::NotFound => Ok(()), @@ -110,7 +114,7 @@ fn clear_legacy_command_files() -> Result<()> { } } -fn next_daemon_toggle_request_path() -> Result { +fn next_daemon_toggle_request_path() -> Result { let dir = daemon_command_dir(); fs::create_dir_all(&dir) .with_context(|| format!("failed to create runtime directory {}", dir.display()))?; @@ -118,7 +122,7 @@ fn next_daemon_toggle_request_path() -> Result { .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_nanos(); - Ok(dir.join(format!("{:032x}-{:08x}.json", stamp, std::process::id()))) + Ok(dir.join(format!("{:032x}-{:08x}.json", stamp, process::id()))) } fn daemon_toggle_response_path_for_request(path: &Path) -> Result { @@ -131,12 +135,12 @@ fn daemon_toggle_response_path_for_request(path: &Path) -> Result { Ok(parent.join("responses").join(file_name)) } -pub(super) fn write_file_atomic(path: &std::path::Path, payload: &[u8]) -> Result<()> { +pub(super) fn write_file_atomic(path: &Path, payload: &[u8]) -> Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent) .with_context(|| format!("failed to create runtime directory {}", parent.display()))?; } - crate::durable_io::write_atomic(path, payload, AtomicWriteOptions::private_runtime_file()) + durable_io::write_atomic(path, payload, AtomicWriteOptions::private_runtime_file()) .with_context(|| format!("failed to write {}", path.display())) } diff --git a/src/daemon/control/response.rs b/src/daemon/control/response.rs index 30d68674b..135dced94 100644 --- a/src/daemon/control/response.rs +++ b/src/daemon/control/response.rs @@ -14,7 +14,7 @@ use std::thread; use std::time::{Duration, Instant}; pub(crate) fn write_daemon_toggle_command_success(command: &DaemonToggleCommand) -> Result<()> { - write_daemon_toggle_response(&command.response_path, DaemonToggleResponse::ok()) + write_daemon_toggle_response(&command.response_path, &DaemonToggleResponse::ok()) } pub(crate) fn write_daemon_toggle_command_error( @@ -23,13 +23,13 @@ pub(crate) fn write_daemon_toggle_command_error( ) -> Result<()> { write_daemon_toggle_response( &command.response_path, - DaemonToggleResponse::error(message.to_string()), + &DaemonToggleResponse::error(message.to_string()), ) } -fn write_daemon_toggle_response(path: &Path, response: DaemonToggleResponse) -> Result<()> { +fn write_daemon_toggle_response(path: &Path, response: &DaemonToggleResponse) -> Result<()> { let payload = - serde_json::to_vec(&response).context("failed to serialize daemon toggle response")?; + serde_json::to_vec(response).context("failed to serialize daemon toggle response")?; write_file_atomic(path, &payload) } diff --git a/src/daemon/control/runtime.rs b/src/daemon/control/runtime.rs index 3ef4fde65..611799789 100644 --- a/src/daemon/control/runtime.rs +++ b/src/daemon/control/runtime.rs @@ -1,3 +1,5 @@ +use std::io; + use super::DaemonRuntimeInfo; use super::queue::{clear_daemon_toggle_request_file, clear_file, write_file_atomic}; use crate::paths::{daemon_lock_file, daemon_pid_file}; @@ -5,8 +7,7 @@ use crate::session::try_lock_exclusive; use anyhow::{Context, Result, anyhow}; use log::warn; use std::fs; -use std::fs::File; -use std::fs::OpenOptions; +use std::fs::{File, OpenOptions}; use std::io::ErrorKind; pub(crate) fn write_daemon_pid_file(pid: u32, token: &str) -> Result<()> { @@ -144,7 +145,7 @@ pub(super) fn signal_daemon_pid(pid: u32) -> Result<()> { return Err(anyhow!( "failed to signal wayscriber daemon {}: {}", pid, - std::io::Error::last_os_error() + io::Error::last_os_error() )); } Ok(()) diff --git a/src/daemon/control/tests.rs b/src/daemon/control/tests.rs index 4e700742c..f19319914 100644 --- a/src/daemon/control/tests.rs +++ b/src/daemon/control/tests.rs @@ -1,7 +1,9 @@ +use crate::daemon::protocol_v2::{DaemonRuntimeRecordV2, ProtocolToken}; +use crate::{test_env, test_temp}; + use super::*; use crate::paths::daemon_pid_file; -use std::env; -use std::fs; +use std::{env, fs}; #[test] fn empty_toggle_request_reports_empty() { @@ -78,11 +80,9 @@ fn session_file_request_rejects_relative_path() { #[test] fn admitted_indeterminate_v2_result_warns_callers_not_to_retry() { - let error = finish_v2_command( - crate::daemon::protocol_v2::TerminalCommandResult::AdmittedIndeterminate( - "final control publication failed".into(), - ), - ) + let error = finish_v2_command(TerminalCommandResult::AdmittedIndeterminate( + "final control publication failed".into(), + )) .unwrap_err(); assert!( @@ -95,7 +95,7 @@ fn admitted_indeterminate_v2_result_warns_callers_not_to_retry() { #[test] fn daemon_toggle_response_round_trips_error_and_is_removed_after_wait() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let command = DaemonToggleCommand { daemon_token: "daemon-token".into(), request: DaemonToggleRequest::default(), @@ -117,7 +117,7 @@ fn daemon_toggle_response_round_trips_error_and_is_removed_after_wait() { #[test] fn daemon_toggle_response_round_trips_success_and_is_removed_after_wait() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let command = DaemonToggleCommand { daemon_token: "daemon-token".into(), request: DaemonToggleRequest::default(), @@ -142,7 +142,7 @@ fn daemon_toggle_response_wait_covers_request_age_and_overlay_stop_grace() { #[test] fn daemon_toggle_response_wait_error_marks_existing_request_canceled() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let command = DaemonToggleCommand { daemon_token: "daemon-token".into(), request: DaemonToggleRequest::default(), @@ -168,7 +168,7 @@ fn daemon_toggle_response_wait_error_marks_existing_request_canceled() { #[test] fn daemon_toggle_response_wait_error_does_not_create_missing_cancel_request() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let command = DaemonToggleCommand { daemon_token: "daemon-token".into(), request: DaemonToggleRequest::default(), @@ -190,7 +190,7 @@ fn daemon_toggle_response_wait_error_does_not_create_missing_cancel_request() { #[test] fn daemon_toggle_response_parse_error_marks_existing_request_canceled() { - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let command = DaemonToggleCommand { daemon_token: "daemon-token".into(), request: DaemonToggleRequest::default(), @@ -216,9 +216,10 @@ fn daemon_toggle_response_parse_error_marks_existing_request_canceled() { #[test] fn daemon_pid_file_round_trips_runtime_info() { - let _guard = crate::test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let tmp = test_temp::tempdir().unwrap(); let prev = env::var_os(XDG_RUNTIME_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, tmp.path()); } @@ -230,17 +231,21 @@ fn daemon_pid_file_round_trips_runtime_info() { .contains("wayscriber daemon is not running") ); - match prev { - Some(value) => unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, value) }, - None => unsafe { env::remove_var(XDG_RUNTIME_DIR_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev { + Some(value) => env::set_var(XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(XDG_RUNTIME_DIR_ENV), + } } } #[test] fn stale_cleanup_removes_matching_runtime_while_lock_is_free() { - let _guard = crate::test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let tmp = test_temp::tempdir().unwrap(); let prev = env::var_os(XDG_RUNTIME_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, tmp.path()); } @@ -264,17 +269,21 @@ fn stale_cleanup_removes_matching_runtime_while_lock_is_free() { assert!(!daemon_pid_file().exists()); assert!(!daemon_command_dir().exists()); - match prev { - Some(value) => unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, value) }, - None => unsafe { env::remove_var(XDG_RUNTIME_DIR_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev { + Some(value) => env::set_var(XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(XDG_RUNTIME_DIR_ENV), + } } } #[test] fn stale_cleanup_removes_only_legacy_commands_and_preserves_versioned_state() { - let _guard = crate::test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let tmp = test_temp::tempdir().unwrap(); let prev = env::var_os(XDG_RUNTIME_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, tmp.path()); } @@ -318,17 +327,21 @@ fn stale_cleanup_removes_only_legacy_commands_and_preserves_versioned_state() { b"not-a-canonical-v1-request" ); - match prev { - Some(value) => unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, value) }, - None => unsafe { env::remove_var(XDG_RUNTIME_DIR_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev { + Some(value) => env::set_var(XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(XDG_RUNTIME_DIR_ENV), + } } } #[test] fn stale_cleanup_preserves_mismatched_runtime() { - let _guard = crate::test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let tmp = test_temp::tempdir().unwrap(); let prev = env::var_os(XDG_RUNTIME_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, tmp.path()); } @@ -355,17 +368,21 @@ fn stale_cleanup_preserves_mismatched_runtime() { assert_eq!(read_daemon_runtime_file().unwrap(), current); assert!(daemon_command_dir().exists()); - match prev { - Some(value) => unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, value) }, - None => unsafe { env::remove_var(XDG_RUNTIME_DIR_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev { + Some(value) => env::set_var(XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(XDG_RUNTIME_DIR_ENV), + } } } #[test] fn take_daemon_toggle_request_round_trips_payload() { - let _guard = crate::test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let tmp = test_temp::tempdir().unwrap(); let prev = env::var_os(XDG_RUNTIME_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, tmp.path()); } @@ -402,17 +419,21 @@ fn take_daemon_toggle_request_round_trips_payload() { assert!(!batch.saw_command_files); assert!(batch.commands.is_empty()); - match prev { - Some(value) => unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, value) }, - None => unsafe { env::remove_var(XDG_RUNTIME_DIR_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev { + Some(value) => env::set_var(XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(XDG_RUNTIME_DIR_ENV), + } } } #[test] fn write_daemon_toggle_request_queues_multiple_files_without_leaking_temp_files() { - let _guard = crate::test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let tmp = test_temp::tempdir().unwrap(); let prev = env::var_os(XDG_RUNTIME_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, tmp.path()); } @@ -443,17 +464,21 @@ fn write_daemon_toggle_request_queues_multiple_files_without_leaking_temp_files( assert!(entries.iter().all(|name| name.ends_with(".json"))); assert!(!entries.iter().any(|name| name.ends_with(".tmp"))); - match prev { - Some(value) => unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, value) }, - None => unsafe { env::remove_var(XDG_RUNTIME_DIR_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev { + Some(value) => env::set_var(XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(XDG_RUNTIME_DIR_ENV), + } } } #[test] fn take_daemon_toggle_request_drains_multiple_payloads_in_order() { - let _guard = crate::test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let tmp = test_temp::tempdir().unwrap(); let prev = env::var_os(XDG_RUNTIME_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, tmp.path()); } @@ -484,17 +509,21 @@ fn take_daemon_toggle_request_drains_multiple_payloads_in_order() { .unwrap_or(true) ); - match prev { - Some(value) => unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, value) }, - None => unsafe { env::remove_var(XDG_RUNTIME_DIR_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev { + Some(value) => env::set_var(XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(XDG_RUNTIME_DIR_ENV), + } } } #[test] fn take_daemon_toggle_request_ignores_mismatched_token() { - let _guard = crate::test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let tmp = test_temp::tempdir().unwrap(); let prev = env::var_os(XDG_RUNTIME_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, tmp.path()); } @@ -515,17 +544,21 @@ fn take_daemon_toggle_request_ignores_mismatched_token() { .is_empty() ); - match prev { - Some(value) => unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, value) }, - None => unsafe { env::remove_var(XDG_RUNTIME_DIR_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev { + Some(value) => env::set_var(XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(XDG_RUNTIME_DIR_ENV), + } } } #[test] fn take_daemon_toggle_request_ignores_stale_payload() { - let _guard = crate::test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let tmp = test_temp::tempdir().unwrap(); let prev = env::var_os(XDG_RUNTIME_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, tmp.path()); } @@ -547,17 +580,21 @@ fn take_daemon_toggle_request_ignores_stale_payload() { assert!(batch.saw_command_files); assert!(batch.commands.is_empty()); - match prev { - Some(value) => unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, value) }, - None => unsafe { env::remove_var(XDG_RUNTIME_DIR_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev { + Some(value) => env::set_var(XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(XDG_RUNTIME_DIR_ENV), + } } } #[test] fn take_daemon_toggle_request_ignores_canceled_payload_but_marks_typed_signal() { - let _guard = crate::test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let tmp = test_temp::tempdir().unwrap(); let prev = env::var_os(XDG_RUNTIME_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, tmp.path()); } @@ -579,19 +616,22 @@ fn take_daemon_toggle_request_ignores_canceled_payload_but_marks_typed_signal() assert!(batch.saw_command_files); assert!(batch.commands.is_empty()); - match prev { - Some(value) => unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, value) }, - None => unsafe { env::remove_var(XDG_RUNTIME_DIR_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev { + Some(value) => env::set_var(XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(XDG_RUNTIME_DIR_ENV), + } } } fn with_client_runtime_dir(run: impl FnOnce() -> T) -> T { - let tmp = crate::test_temp::tempdir().unwrap(); - crate::test_env::with_env_var(XDG_RUNTIME_DIR_ENV, Some(tmp.path().as_os_str()), run) + let tmp = test_temp::tempdir().unwrap(); + test_env::with_env_var(XDG_RUNTIME_DIR_ENV, Some(tmp.path().as_os_str()), run) } fn hold_daemon_lock() -> fs::File { - let path = crate::paths::daemon_lock_file(); + let path = paths::daemon_lock_file(); fs::create_dir_all(path.parent().unwrap()).unwrap(); let lock = fs::OpenOptions::new() .create(true) @@ -600,7 +640,7 @@ fn hold_daemon_lock() -> fs::File { .truncate(false) .open(&path) .unwrap(); - crate::session::try_lock_exclusive(&lock).unwrap(); + session::try_lock_exclusive(&lock).unwrap(); lock } @@ -616,9 +656,9 @@ fn client_without_a_daemon_reports_that_it_is_not_running() { #[test] fn client_clears_a_v2_record_whose_daemon_released_its_lock() { with_client_runtime_dir(|| { - let token = crate::daemon::protocol_v2::ProtocolToken::generate().unwrap(); - let runtime = crate::daemon::protocol_v2::DaemonRuntimeRecordV2::current(token).unwrap(); - crate::daemon::protocol_v2::write_runtime_record_v2(&daemon_pid_file(), &runtime).unwrap(); + let token = ProtocolToken::generate().unwrap(); + let runtime = DaemonRuntimeRecordV2::current(token).unwrap(); + protocol_v2::write_runtime_record_v2(&daemon_pid_file(), &runtime).unwrap(); let error = send_daemon_toggle_request(&DaemonToggleRequest::default()).unwrap_err(); diff --git a/src/daemon/core.rs b/src/daemon/core.rs index 0773cce1d..4425bbafc 100644 --- a/src/daemon/core.rs +++ b/src/daemon/core.rs @@ -1,3 +1,6 @@ +use std::fs::File; +use std::{io, mem, process, thread}; + use anyhow::{Context, Result}; use log::{info, warn}; use std::fs; @@ -13,10 +16,14 @@ use std::time::{Duration, Instant}; use crate::backend::wayland::RuntimeWakeSource; use crate::env_vars::NO_TRAY_ENV; use crate::paths::daemon_lock_file; +use crate::process_broker::ProcessBroker; use crate::session::try_lock_exclusive; use crate::shortcut_hint::{ShortcutRuntimeBackend, current_shortcut_runtime_backend}; use crate::tray_action::TrayAction; -use crate::{decode_session_override, encode_session_override}; +use crate::unix_signals::{SignalListener, SignalListenerHealth}; +use crate::{ + daemon, decode_session_override, encode_session_override, paths, process_broker, unix_signals, +}; use super::control::DaemonToggleRequest; #[cfg(test)] @@ -27,10 +34,9 @@ use super::global_shortcuts::{GlobalShortcutsListener, start_global_shortcuts_li use super::overlay::launch::OverlayLaunchRequest; use super::overlay::lifecycle::OverlayLifecycle; use super::overlay::{ShowOutcome, overlay_start_backoff_reason}; -use super::protocol_v2::DaemonControlProtocolMode; use super::protocol_v2::{ ActionJournal, BootClock, BootDeadline, BootDeadlineSource, CommandOwner, CommandQueueWatcher, - DaemonRuntimeRecordV2, EffectKind, FinalEffect, ProtocolToken, + DaemonControlProtocolMode, DaemonRuntimeRecordV2, EffectKind, FinalEffect, ProtocolToken, }; use super::tray::{TrayRuntime, start_system_tray}; #[cfg(feature = "tray")] @@ -63,9 +69,7 @@ mod toggles; /// can never start an overlay again. Staying up would keep the tray alive /// while every show silently backs off, so the daemon exits instead and lets /// the service manager's `Restart=on-failure` start a fresh one. -fn ensure_process_broker_usable( - broker: Option<&crate::process_broker::ProcessBroker>, -) -> Result<()> { +fn ensure_process_broker_usable(broker: Option<&ProcessBroker>) -> Result<()> { match broker { Some(broker) if !broker.is_healthy() => Err(anyhow::anyhow!( "process broker is no longer usable; exiting so the daemon can be restarted" @@ -74,7 +78,7 @@ fn ensure_process_broker_usable( } } -fn finish_action_batch(failures: Vec) -> Result<()> { +fn finish_action_batch(failures: &[String]) -> Result<()> { if failures.is_empty() { Ok(()) } else { @@ -101,7 +105,7 @@ pub struct Daemon { pub(super) overlay_action_intents: Arc, pub(super) pending_launch: Option, pub(super) session_resume_override: Arc, - pub(super) lock_file: Option, + pub(super) lock_file: Option, pub(super) last_plain_visibility_toggle_completed_at: Option, protocol_mode: DaemonControlProtocolMode, v2_command_owner: Option, @@ -111,7 +115,7 @@ pub struct Daemon { pending_action_admission_retry: Vec, action_admission_retry_at: Option, #[cfg(unix)] - signal_listener: Option, + signal_listener: Option, #[cfg(feature = "tray")] pub(super) tray_status: Arc, } @@ -134,7 +138,7 @@ impl Daemon { initial_mode, initial_named_session_file, remembered_session_file: None, - instance_token: crate::daemon::generate_daemon_instance_token(), + instance_token: daemon::generate_daemon_instance_token(), freeze_on_show: false, tray_enabled, backend_runner: None, @@ -210,7 +214,7 @@ impl Daemon { if Instant::now() >= deadline { return Err(AlreadyRunningError.into()); } - std::thread::sleep(DAEMON_LOCK_RETRY); + thread::sleep(DAEMON_LOCK_RETRY); } Err(err) => return Err(err).context("failed to lock daemon instance"), } @@ -218,10 +222,10 @@ impl Daemon { } fn clear_stale_runtime_files() { - if let Err(err) = crate::daemon::clear_daemon_pid_file() { + if let Err(err) = daemon::clear_daemon_pid_file() { warn!("Failed to clear stale daemon pid file on startup: {}", err); } - if let Err(err) = crate::daemon::clear_daemon_toggle_request_file() { + if let Err(err) = daemon::clear_daemon_toggle_request_file() { warn!( "Failed to clear stale daemon toggle request on startup: {}", err @@ -276,7 +280,7 @@ impl Daemon { let signal_visibility = visibility.clone(); let signal_quit = quit_event.clone(); self.signal_listener = Some( - crate::unix_signals::spawn_listener( + unix_signals::spawn_listener( &DAEMON_SIGNALS, move |sig| { if signal_quit.is_raised() { @@ -334,8 +338,8 @@ impl Daemon { self.acquire_daemon_lock()?; Self::clear_stale_runtime_files(); - let daemon_wake = - RuntimeWakeSource::new().context("Failed to create daemon control wake descriptor")?; + let daemon_wake = RuntimeWakeSource::try_new() + .context("Failed to create daemon control wake descriptor")?; let visibility = self.visibility_intents.publisher(daemon_wake.handle()); let action = self.overlay_action_intents.publisher(daemon_wake.handle()); let quit_event = DaemonControlEvent::new(self.should_quit.clone(), daemon_wake.handle()); @@ -350,7 +354,7 @@ impl Daemon { DaemonControlProtocolMode::LegacyV1 => { super::protocol_v2::prepare_rollback_compatibility() .context("v2 state is not safe for rollback compatibility")?; - crate::daemon::write_daemon_pid_file(std::process::id(), &self.instance_token) + daemon::write_daemon_pid_file(process::id(), &self.instance_token) } #[cfg(test)] DaemonControlProtocolMode::DarkV2Harness => { @@ -363,9 +367,9 @@ impl Daemon { .context("failed to open daemon v2 command owner")?; super::protocol_v2::recover_stale_child_records() .context("failed to recover daemon v2 child proofs")?; - let watcher = CommandQueueWatcher::new(&owner.queue_path()) + let watcher = CommandQueueWatcher::try_new(&owner.queue_path()) .context("failed to watch daemon v2 command queue")?; - let deadline_source = BootDeadlineSource::new() + let deadline_source = BootDeadlineSource::try_new() .context("failed to create daemon v2 deadline source")?; let action_journal = ActionJournal::open().context("failed to open daemon v2 action journal")?; @@ -376,10 +380,7 @@ impl Daemon { self.v2_command_watcher = Some(watcher); self.v2_deadline_source = Some(deadline_source); self.v2_action_journal = Some(action_journal); - super::protocol_v2::write_runtime_record_v2( - &crate::paths::daemon_pid_file(), - &runtime, - ) + super::protocol_v2::write_runtime_record_v2(&paths::daemon_pid_file(), &runtime) } }; if let Err(err) = publish_result { @@ -439,7 +440,7 @@ impl Daemon { ) -> Result<()> { let result = self.run_control_loop(daemon_wake); if result.is_err() - && let Err(err) = crate::daemon::clear_daemon_pid_file() + && let Err(err) = daemon::clear_daemon_pid_file() { warn!("Failed to invalidate daemon readiness after runtime failure: {err}"); } @@ -498,7 +499,7 @@ impl Daemon { } } - ensure_process_broker_usable(crate::process_broker::current().ok().as_ref())?; + ensure_process_broker_usable(process_broker::current().ok().as_ref())?; self.arm_v2_lifecycle_deadline()?; let readiness = wait_for_daemon_lifecycle( daemon_wake, @@ -564,10 +565,7 @@ impl Daemon { return Ok((Vec::new(), false)); } self.action_admission_retry_at = None; - Ok(( - std::mem::take(&mut self.pending_action_admission_retry), - true, - )) + Ok((mem::take(&mut self.pending_action_admission_retry), true)) } fn process_overlay_action_intents(&mut self, actions: Vec) -> Result<()> { @@ -587,20 +585,20 @@ impl Daemon { } } self.overlay_action_intents.finish_batch(action_count); - return finish_action_batch(failures); + return finish_action_batch(&failures); } let Some(journal) = self.v2_action_journal.clone() else { self.retain_action_admission_retry(actions, 0, &mut failures); failures.push("v2 action journal is not installed".to_string()); - return finish_action_batch(failures); + return finish_action_batch(&failures); }; let mut admitted = Vec::with_capacity(actions.len()); let mut retry = Vec::new(); let mut will_be_visible = self.overlay.state() == OverlayState::Visible; let mut actions = actions.into_iter(); while let Some(action) = actions.next() { - if !will_be_visible && matches!(action, crate::tray_action::TrayAction::LightDrawOff) { + if !will_be_visible && matches!(action, TrayAction::LightDrawOff) { continue; } match journal.publish_anonymous(&self.instance_token, action) { @@ -625,7 +623,7 @@ impl Daemon { // begin, so an early runtime failure cannot silently lose the tail. for (action, prepared) in admitted { if self.overlay.state() == OverlayState::Hidden - && matches!(action, crate::tray_action::TrayAction::LightDrawOff) + && matches!(action, TrayAction::LightDrawOff) { let reason = "overlay remained hidden before LightDrawOff delivery"; if let Err(error) = journal.abandon(&prepared, reason) { @@ -660,7 +658,7 @@ impl Daemon { } let completed = action_count - retry.len(); self.retain_action_admission_retry(retry, completed, &mut failures); - finish_action_batch(failures) + finish_action_batch(&failures) } fn retain_action_admission_retry( @@ -714,7 +712,7 @@ impl Daemon { if let Some(action) = legacy_request.overlay_action { if self.overlay.state() == OverlayState::Hidden - && matches!(action, crate::tray_action::TrayAction::LightDrawOff) + && matches!(action, TrayAction::LightDrawOff) { claimed.commit(EffectKind::NoOp)?; claimed.defer()?; @@ -853,10 +851,10 @@ impl Daemon { Err(err) => warn!("Global shortcuts listener thread panicked: {:?}", err), } } - if let Err(err) = crate::daemon::clear_daemon_toggle_request_file() { + if let Err(err) = daemon::clear_daemon_toggle_request_file() { warn!("Failed to clear daemon toggle request file: {}", err); } - if let Err(err) = crate::daemon::clear_daemon_pid_file() { + if let Err(err) = daemon::clear_daemon_pid_file() { warn!("Failed to clear daemon pid file: {}", err); } self.stop_signal_listener() @@ -870,8 +868,8 @@ impl Daemon { .as_ref() .context("daemon signal listener is not installed")?; match listener.health() { - crate::unix_signals::SignalListenerHealth::Running => Ok(()), - crate::unix_signals::SignalListenerHealth::Failed(failure) => { + SignalListenerHealth::Running => Ok(()), + SignalListenerHealth::Failed(failure) => { Err(anyhow::anyhow!("daemon signal listener failed: {failure}")) } health => Err(anyhow::anyhow!( @@ -890,7 +888,7 @@ impl Daemon { #[cfg(unix)] if let Some(mut listener) = self.signal_listener.take() { let failure = match listener.health() { - crate::unix_signals::SignalListenerHealth::Failed(failure) => Some(failure), + SignalListenerHealth::Failed(failure) => Some(failure), _ => None, }; listener @@ -977,7 +975,7 @@ fn wait_for_daemon_lifecycle( }); } if ready < 0 { - let err = std::io::Error::last_os_error(); + let err = io::Error::last_os_error(); if err.kind() == ErrorKind::Interrupted { continue; } diff --git a/src/daemon/core/tests.rs b/src/daemon/core/tests.rs index d4e868365..2e0f3a0cb 100644 --- a/src/daemon/core/tests.rs +++ b/src/daemon/core/tests.rs @@ -1,3 +1,8 @@ +use std::env; +use std::path::Path; + +use crate::{env_vars, test_env, test_temp, tray_action}; + use super::*; use std::sync::Mutex; use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering}; @@ -24,7 +29,7 @@ fn a_starting_daemon_waits_out_a_brief_lock_probe() { #[test] fn an_unusable_process_broker_ends_the_daemon_loop() { - let guard = crate::process_broker::start_for_runtime().unwrap(); + let guard = process_broker::start_for_runtime().unwrap(); ensure_process_broker_usable(None).unwrap(); ensure_process_broker_usable(Some(guard.broker())).unwrap(); @@ -36,8 +41,8 @@ fn an_unusable_process_broker_ends_the_daemon_loop() { #[test] fn daemon_lifecycle_wait_wakes_for_v2_maintenance_deadline() { - let wake = RuntimeWakeSource::new().unwrap(); - let deadline = BootDeadlineSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); + let deadline = BootDeadlineSource::try_new().unwrap(); deadline .arm( super::super::protocol_v2::BootClock::now() @@ -59,9 +64,9 @@ fn daemon_lifecycle_wait_wakes_for_v2_maintenance_deadline() { #[test] fn action_admission_retry_uses_the_existing_v2_deadline_source() { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut daemon = Daemon::new(None, false, None, None); - daemon.v2_deadline_source = Some(BootDeadlineSource::new().unwrap()); + daemon.v2_deadline_source = Some(BootDeadlineSource::try_new().unwrap()); daemon.action_admission_retry_at = Some(BootDeadline::from_nanos(1)); daemon.arm_v2_lifecycle_deadline().unwrap(); @@ -80,18 +85,18 @@ fn action_admission_retry_uses_the_existing_v2_deadline_source() { #[cfg(unix)] #[test] fn listener_failure_invalidates_v1_readiness_and_runs_existing_cleanup() { - let _signal_guard = crate::unix_signals::test_signal_lock(); - let _env_guard = crate::test_env::lock(); - let temp = crate::test_temp::tempdir().unwrap(); - let previous_runtime_dir = std::env::var_os(crate::env_vars::XDG_RUNTIME_DIR_ENV); + let _signal_guard = unix_signals::test_signal_lock(); + let _env_guard = test_env::lock(); + let temp = test_temp::tempdir().unwrap(); + let previous_runtime_dir = env::var_os(env_vars::XDG_RUNTIME_DIR_ENV); // SAFETY: this test holds the process environment mutex. unsafe { - std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, temp.path()); + env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, temp.path()); } - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let wake_handle = wake.handle(); - let listener = crate::unix_signals::spawn_listener( + let listener = unix_signals::spawn_listener( &[libc::SIGWINCH], |_| {}, move || wake_handle.wake().unwrap(), @@ -103,14 +108,14 @@ fn listener_failure_invalidates_v1_readiness_and_runs_existing_cleanup() { let mut daemon = Daemon::new(None, false, None, None); daemon.protocol_mode = DaemonControlProtocolMode::rollback_compatibility(); daemon.signal_listener = Some(listener); - crate::daemon::write_daemon_pid_file(std::process::id(), &daemon.instance_token).unwrap(); - assert!(crate::paths::daemon_pid_file().exists()); + daemon::write_daemon_pid_file(process::id(), &daemon.instance_token).unwrap(); + assert!(paths::daemon_pid_file().exists()); let err = daemon .run_control_loop_and_invalidate_on_failure(&wake) .unwrap_err(); assert!(err.to_string().contains("daemon signal listener failed")); - assert!(!crate::paths::daemon_pid_file().exists()); + assert!(!paths::daemon_pid_file().exists()); let cleanup_err = daemon.shutdown_after_run().unwrap_err(); assert!(cleanup_err.to_string().contains("failed before teardown")); @@ -120,8 +125,8 @@ fn listener_failure_invalidates_v1_readiness_and_runs_existing_cleanup() { // SAFETY: this test still holds the process environment mutex. unsafe { match previous_runtime_dir { - Some(value) => std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, value), - None => std::env::remove_var(crate::env_vars::XDG_RUNTIME_DIR_ENV), + Some(value) => env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(env_vars::XDG_RUNTIME_DIR_ENV), } } } @@ -177,7 +182,7 @@ fn visible_overlay_rejects_different_named_session_request() { assert_eq!(daemon.test_state(), OverlayState::Visible); assert_eq!( daemon.overlay.active_named_session_file(), - Some(std::path::Path::new("/tmp/current.wayscriber-session")) + Some(Path::new("/tmp/current.wayscriber-session")) ); }, ); @@ -214,7 +219,7 @@ fn visible_overlay_is_in_the_session_it_switched_to() { #[test] fn visible_overlay_rejection_writes_daemon_toggle_error_response() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); super::super::overlay::tests::with_visible_overlay( Some(PathBuf::from("/tmp/current.wayscriber-session")), false, @@ -222,7 +227,7 @@ fn visible_overlay_rejection_writes_daemon_toggle_error_response() { let command = DaemonToggleCommand { daemon_token: "daemon-token".into(), request: DaemonToggleRequest { - session_file: Some(std::path::PathBuf::from("/tmp/other.wayscriber-session")), + session_file: Some(PathBuf::from("/tmp/other.wayscriber-session")), ..Default::default() }, request_path: temp.path().join("request.json"), @@ -230,10 +235,7 @@ fn visible_overlay_rejection_writes_daemon_toggle_error_response() { }; let mut suppress_overlay_action_signal = false; - daemon.process_queued_toggle_command( - command.clone(), - &mut suppress_overlay_action_signal, - ); + daemon.process_queued_toggle_command(&command, &mut suppress_overlay_action_signal); let err = read_daemon_toggle_response(&command.response_path) .expect_err("visible target mismatch should be written to response"); @@ -292,12 +294,12 @@ fn duplicate_plain_toggle_requests_are_debounced() { #[test] fn typed_visibility_toggle_request_is_not_debounced() { - let modes = Arc::new(std::sync::Mutex::new(Vec::new())); + let modes = Arc::new(Mutex::new(Vec::new())); let modes_clone = Arc::clone(&modes); let runner: Arc = Arc::new(move |mode| { modes_clone .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .expect("recorded backend modes") .push(mode); Ok(()) }); @@ -317,9 +319,7 @@ fn typed_visibility_toggle_request_is_not_debounced() { ) .unwrap(); - let modes = modes - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + let modes = modes.lock().expect("recorded backend modes"); assert_eq!(modes.as_slice(), &[None, Some("whiteboard".to_string())]); assert_eq!(daemon.test_state(), OverlayState::Hidden); } @@ -386,12 +386,12 @@ fn plain_toggle_after_debounce_window_is_processed() { #[test] fn published_v2_runtime_drives_a_typed_request_to_terminal_response() { - let _env_guard = crate::test_env::lock(); - let temp = crate::test_temp::tempdir().unwrap(); - let previous_runtime_dir = std::env::var_os(crate::env_vars::XDG_RUNTIME_DIR_ENV); + let _env_guard = test_env::lock(); + let temp = test_temp::tempdir().unwrap(); + let previous_runtime_dir = env::var_os(env_vars::XDG_RUNTIME_DIR_ENV); // SAFETY: this test holds the process environment mutex. unsafe { - std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, temp.path()); + env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, temp.path()); } let token = ProtocolToken::generate().unwrap(); @@ -400,7 +400,7 @@ fn published_v2_runtime_drives_a_typed_request_to_terminal_response() { let queue_path = owner.queue_path(); let action_journal = ActionJournal::open().unwrap(); let runtime = DaemonRuntimeRecordV2::current(token).unwrap(); - super::super::protocol_v2::write_runtime_record_v2(&crate::paths::daemon_pid_file(), &runtime) + super::super::protocol_v2::write_runtime_record_v2(&paths::daemon_pid_file(), &runtime) .unwrap(); let _daemon_lock = hold_daemon_lock(); @@ -409,7 +409,7 @@ fn published_v2_runtime_drives_a_typed_request_to_terminal_response() { let runner: Arc = Arc::new(move |mode| { runner_modes .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .expect("recorded backend modes") .push(mode); Ok(()) }); @@ -419,23 +419,23 @@ fn published_v2_runtime_drives_a_typed_request_to_terminal_response() { daemon.v2_command_owner = Some(owner); daemon.v2_action_journal = Some(action_journal); - std::thread::scope(|scope| { + thread::scope(|scope| { let caller = scope.spawn(|| { - crate::daemon::send_daemon_toggle_request(&DaemonToggleRequest { + daemon::send_daemon_toggle_request(&DaemonToggleRequest { mode: Some("whiteboard".into()), ..Default::default() }) }); let deadline = Instant::now() + Duration::from_secs(2); - while std::fs::read_dir(&queue_path).unwrap().next().is_none() { + while fs::read_dir(&queue_path).unwrap().next().is_none() { assert!(Instant::now() < deadline, "v2 caller did not publish"); - std::thread::sleep(Duration::from_millis(2)); + thread::sleep(Duration::from_millis(2)); } loop { daemon.process_v2_commands().unwrap(); if !observed_modes .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .expect("recorded backend modes") .is_empty() { break; @@ -444,7 +444,7 @@ fn published_v2_runtime_drives_a_typed_request_to_terminal_response() { Instant::now() < deadline, "v2 daemon did not claim the published request" ); - std::thread::sleep(Duration::from_millis(2)); + thread::sleep(Duration::from_millis(2)); } caller.join().unwrap().unwrap(); }); @@ -452,7 +452,7 @@ fn published_v2_runtime_drives_a_typed_request_to_terminal_response() { assert_eq!( observed_modes .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .expect("recorded backend modes") .as_slice(), &[Some("whiteboard".into())] ); @@ -461,8 +461,8 @@ fn published_v2_runtime_drives_a_typed_request_to_terminal_response() { // SAFETY: this test still holds the process environment mutex. unsafe { match previous_runtime_dir { - Some(value) => std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, value), - None => std::env::remove_var(crate::env_vars::XDG_RUNTIME_DIR_ENV), + Some(value) => env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(env_vars::XDG_RUNTIME_DIR_ENV), } } } @@ -475,11 +475,8 @@ fn a_visible_v2_action_retains_only_the_pending_activation_token() { let mut runtime = DaemonRuntimeRecordV2::current(ProtocolToken::generate().unwrap()).unwrap(); runtime.v2_instance_token = daemon.instance_token.clone(); - super::super::protocol_v2::write_runtime_record_v2( - &crate::paths::daemon_pid_file(), - &runtime, - ) - .unwrap(); + super::super::protocol_v2::write_runtime_record_v2(&paths::daemon_pid_file(), &runtime) + .unwrap(); let _daemon_lock = hold_daemon_lock(); daemon.protocol_mode = DaemonControlProtocolMode::dark_harness(); daemon.v2_command_owner = Some(owner); @@ -518,12 +515,12 @@ fn a_visible_v2_action_retains_only_the_pending_activation_token() { #[test] fn failed_anonymous_action_admission_does_not_allow_the_tail_to_overtake() { - let _env_guard = crate::test_env::lock(); - let temp = crate::test_temp::tempdir().unwrap(); - let previous_runtime_dir = std::env::var_os(crate::env_vars::XDG_RUNTIME_DIR_ENV); + let _env_guard = test_env::lock(); + let temp = test_temp::tempdir().unwrap(); + let previous_runtime_dir = env::var_os(env_vars::XDG_RUNTIME_DIR_ENV); // SAFETY: this test holds the process environment mutex. unsafe { - std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, temp.path()); + env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, temp.path()); } let token = ProtocolToken::generate().unwrap().to_string(); @@ -535,7 +532,7 @@ fn failed_anonymous_action_admission_does_not_allow_the_tail_to_overtake() { daemon.protocol_mode = DaemonControlProtocolMode::dark_harness(); daemon.instance_token = token.clone(); daemon.v2_action_journal = Some(journal.clone()); - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let publisher = daemon.overlay_action_intents.publisher(wake.handle()); publisher.publish(TrayAction::ToggleFreeze).unwrap(); publisher.publish(TrayAction::ToggleHelp).unwrap(); @@ -600,19 +597,19 @@ fn failed_anonymous_action_admission_does_not_allow_the_tail_to_overtake() { // SAFETY: this test still holds the process environment mutex. unsafe { match previous_runtime_dir { - Some(value) => std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, value), - None => std::env::remove_var(crate::env_vars::XDG_RUNTIME_DIR_ENV), + Some(value) => env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(env_vars::XDG_RUNTIME_DIR_ENV), } } } fn with_runtime_dir(run: impl FnOnce() -> T) -> T { - let _env_guard = crate::test_env::lock(); - let temp = crate::test_temp::tempdir().unwrap(); - let previous_runtime_dir = std::env::var_os(crate::env_vars::XDG_RUNTIME_DIR_ENV); + let _env_guard = test_env::lock(); + let temp = test_temp::tempdir().unwrap(); + let previous_runtime_dir = env::var_os(env_vars::XDG_RUNTIME_DIR_ENV); // SAFETY: this helper holds the process environment mutex. unsafe { - std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, temp.path()); + env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, temp.path()); } let result = run(); @@ -620,8 +617,8 @@ fn with_runtime_dir(run: impl FnOnce() -> T) -> T { // SAFETY: this helper still holds the process environment mutex. unsafe { match previous_runtime_dir { - Some(value) => std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, value), - None => std::env::remove_var(crate::env_vars::XDG_RUNTIME_DIR_ENV), + Some(value) => env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(env_vars::XDG_RUNTIME_DIR_ENV), } } result @@ -629,9 +626,9 @@ fn with_runtime_dir(run: impl FnOnce() -> T) -> T { /// Holds the single-instance lock as a running daemon does, so clients reach /// the harness instead of reporting that no daemon is running. -fn hold_daemon_lock() -> std::fs::File { +fn hold_daemon_lock() -> File { let path = daemon_lock_file(); - std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::create_dir_all(path.parent().unwrap()).unwrap(); let lock = OpenOptions::new() .create(true) .read(true) @@ -664,11 +661,8 @@ fn typed_v2_requests_during_spawn_backoff_fail_without_an_effect() { let owner = CommandOwner::open(&token_text).unwrap(); let journal = ActionJournal::open().unwrap(); let runtime = DaemonRuntimeRecordV2::current(token).unwrap(); - super::super::protocol_v2::write_runtime_record_v2( - &crate::paths::daemon_pid_file(), - &runtime, - ) - .unwrap(); + super::super::protocol_v2::write_runtime_record_v2(&paths::daemon_pid_file(), &runtime) + .unwrap(); let _daemon_lock = hold_daemon_lock(); let mut daemon = daemon_in_spawn_backoff(); daemon.protocol_mode = DaemonControlProtocolMode::dark_harness(); @@ -686,13 +680,13 @@ fn typed_v2_requests_during_spawn_backoff_fail_without_an_effect() { ..Default::default() }, ] { - let result = std::thread::scope(|scope| { - let caller = scope.spawn(|| crate::daemon::send_daemon_toggle_request(&request)); + let result = thread::scope(|scope| { + let caller = scope.spawn(|| daemon::send_daemon_toggle_request(&request)); let deadline = Instant::now() + Duration::from_secs(5); while !caller.is_finished() { daemon.process_v2_commands().unwrap(); assert!(Instant::now() < deadline, "v2 caller did not finish"); - std::thread::sleep(Duration::from_millis(2)); + thread::sleep(Duration::from_millis(2)); } caller.join().unwrap() }); @@ -722,7 +716,7 @@ fn anonymous_action_during_spawn_backoff_is_abandoned_rather_than_replayed() { daemon.protocol_mode = DaemonControlProtocolMode::dark_harness(); daemon.instance_token = token.clone(); daemon.v2_action_journal = Some(journal.clone()); - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let publisher = daemon.overlay_action_intents.publisher(wake.handle()); publisher.publish(TrayAction::CaptureFull).unwrap(); @@ -759,13 +753,13 @@ fn legacy_action_during_spawn_backoff_fails_without_queueing_it() { assert!(format!("{error:#}").contains("backing off"), "{error:#}"); assert!(daemon.pending_launch.is_none()); - assert!(crate::tray_action::take_pending_actions().is_empty()); + assert!(tray_action::take_pending_actions().is_empty()); }); } #[test] fn a_failed_runner_does_not_replay_launch_options_or_leak_resume_override() { - let _env_guard = crate::test_env::lock(); + let _env_guard = test_env::lock(); let previous_override = crate::runtime_session_override(); crate::set_runtime_session_override(Some(true)); diff --git a/src/daemon/core/toggles.rs b/src/daemon/core/toggles.rs index 13c7f4f73..763efc142 100644 --- a/src/daemon/core/toggles.rs +++ b/src/daemon/core/toggles.rs @@ -1,3 +1,6 @@ +use crate::session::catalog; +use crate::{daemon, tray_action}; + use super::super::control::{ DaemonToggleCommand, DaemonToggleCommands, DaemonToggleRequest, write_daemon_toggle_command_error, write_daemon_toggle_command_success, @@ -110,7 +113,7 @@ impl Daemon { pub(super) fn process_queued_toggle_command( &mut self, - command: DaemonToggleCommand, + command: &DaemonToggleCommand, suppress_overlay_action_signal: &mut bool, ) { let result = self.process_single_toggle( @@ -121,14 +124,14 @@ impl Daemon { match result { Ok(spawned_overlay) => { *suppress_overlay_action_signal |= spawned_overlay; - if let Err(err) = write_daemon_toggle_command_success(&command) { + if let Err(err) = write_daemon_toggle_command_success(command) { warn!("Failed to write daemon toggle response: {}", err); } } Err(err) => { let message = format!("{err:#}"); warn!("Toggle overlay failed: {}", message); - if let Err(response_err) = write_daemon_toggle_command_error(&command, &message) { + if let Err(response_err) = write_daemon_toggle_command_error(command, &message) { warn!( "Failed to write daemon toggle error response: {}", response_err @@ -143,7 +146,7 @@ impl Daemon { action: TrayAction, signal_visible_overlay: bool, ) -> Result<()> { - let action_path = crate::tray_action::queue_action(action)?; + let action_path = tray_action::queue_action(action)?; if signal_visible_overlay && self.overlay.state() == OverlayState::Visible { #[cfg(unix)] @@ -187,7 +190,7 @@ impl Daemon { signal_toggle_requested: bool, ) -> Result<()> { let queued_requests = if signal_toggle_requested { - crate::daemon::take_daemon_toggle_requests(&self.instance_token)? + daemon::take_daemon_toggle_requests(&self.instance_token)? } else { DaemonToggleCommands { commands: Vec::new(), @@ -221,12 +224,12 @@ impl Daemon { let mut suppress_overlay_action_signal = false; for command in queued_requests.commands { - self.process_queued_toggle_command(command, &mut suppress_overlay_action_signal); + self.process_queued_toggle_command(&command, &mut suppress_overlay_action_signal); } Ok(()) } } fn named_session_paths_match(left: &Path, right: &Path) -> bool { - crate::session::catalog::session_paths_match(left, right) + catalog::session_paths_match(left, right) } diff --git a/src/daemon/global_shortcuts.rs b/src/daemon/global_shortcuts.rs index b72575273..9f18be8e5 100644 --- a/src/daemon/global_shortcuts.rs +++ b/src/daemon/global_shortcuts.rs @@ -14,16 +14,32 @@ use std::sync::atomic::Ordering; #[cfg(feature = "portal")] use crate::i18n::tr; #[cfg(feature = "portal")] +use crate::zbus_stream; +#[cfg(feature = "portal")] use anyhow::{Context, Result, anyhow}; #[cfg(feature = "portal")] use log::{debug, info, warn}; #[cfg(feature = "portal")] use std::collections::HashMap; #[cfg(feature = "portal")] +use std::env; +#[cfg(feature = "portal")] use std::future::Future; #[cfg(feature = "portal")] +use std::process; +use std::thread; +#[cfg(feature = "portal")] use std::time::{SystemTime, UNIX_EPOCH}; #[cfg(feature = "portal")] +use tokio::runtime::Runtime; +#[cfg(feature = "portal")] +use tokio::sync::oneshot; +#[cfg(feature = "portal")] +use tokio::sync::oneshot::Receiver; +use tokio::sync::oneshot::Sender; +#[cfg(feature = "portal")] +use zbus::zvariant::ObjectPath; +#[cfg(feature = "portal")] use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value}; #[cfg(feature = "portal")] use zbus::{Connection, proxy}; @@ -63,7 +79,7 @@ const PORTAL_KEY_SESSION_HANDLE_TOKEN: &str = "session_handle_token"; const PORTAL_KEY_STARTUP_ID: &str = "startup_id"; pub(super) struct GlobalShortcutsListener { - shutdown: Option>, + shutdown: Option>, thread: JoinHandle<()>, } @@ -74,7 +90,7 @@ impl GlobalShortcutsListener { } } - pub(super) fn join(mut self) -> std::thread::Result<()> { + pub(super) fn join(mut self) -> thread::Result<()> { self.request_shutdown(); self.thread.join() } @@ -86,19 +102,19 @@ pub(super) fn start_global_shortcuts_listener( ) -> Option { #[cfg(feature = "portal")] { - let listener_quit_flag = quit_flag.clone(); - let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); - let preferred_trigger = std::env::var(PORTAL_SHORTCUT_ENV) + let listener_quit_flag = quit_flag; + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let preferred_trigger = env::var(PORTAL_SHORTCUT_ENV) .ok() .filter(|value| !value.trim().is_empty()) .unwrap_or_else(|| DEFAULT_PREFERRED_TRIGGER.to_string()); - let portal_app_id = std::env::var(PORTAL_APP_ID_ENV) + let portal_app_id = env::var(PORTAL_APP_ID_ENV) .ok() .filter(|value| !value.trim().is_empty()) .unwrap_or_else(|| DEFAULT_PORTAL_APP_ID.to_string()); - let thread = std::thread::spawn(move || { - let runtime = match tokio::runtime::Runtime::new() { + let thread = thread::spawn(move || { + let runtime = match Runtime::new() { Ok(runtime) => runtime, Err(err) => { warn!( @@ -145,21 +161,21 @@ pub(super) fn start_global_shortcuts_listener( trait GlobalShortcuts { async fn create_session( &self, - options: HashMap>, + options: HashMap>, ) -> zbus::Result; async fn bind_shortcuts( &self, - session_handle: zbus::zvariant::ObjectPath<'_>, - shortcuts: Vec<(String, HashMap>)>, + session_handle: ObjectPath<'_>, + shortcuts: Vec<(String, HashMap>)>, parent_window: &str, - options: HashMap>, + options: HashMap>, ) -> zbus::Result; #[zbus(signal)] fn activated( &self, - session_handle: zbus::zvariant::ObjectPath<'_>, + session_handle: ObjectPath<'_>, shortcut_id: &str, timestamp: u64, options: HashMap, @@ -176,11 +192,8 @@ trait GlobalShortcuts { default_path = "/org/freedesktop/portal/desktop" )] trait HostPortalRegistry { - async fn register( - &self, - app_id: &str, - options: HashMap>, - ) -> zbus::Result<()>; + async fn register(&self, app_id: &str, options: HashMap>) + -> zbus::Result<()>; } #[cfg(feature = "portal")] @@ -198,7 +211,7 @@ async fn run_listener( visibility: VisibilityPublisher, preferred_trigger: String, portal_app_id: String, - mut shutdown: tokio::sync::oneshot::Receiver<()>, + mut shutdown: Receiver<()>, ) -> Result<()> { let connection = await_portal_operation( Connection::session(), @@ -254,7 +267,7 @@ async fn run_listener( loop { tokio::select! { - maybe_signal = crate::zbus_stream::next(&mut activated_stream) => { + maybe_signal = zbus_stream::next(&mut activated_stream) => { let Some(signal) = maybe_signal else { return Err(anyhow!("GlobalShortcuts.Activated stream ended unexpectedly")); }; @@ -309,6 +322,11 @@ fn log_global_shortcuts_version(version: zbus::Result) { #[cfg(all(test, feature = "portal"))] mod tests { + use std::future; + use std::sync::mpsc; + + use tokio::time; + use super::*; use std::time::Duration; @@ -325,10 +343,10 @@ mod tests { #[test] fn shutdown_handle_unblocks_listener_without_a_timer() { - let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); - let (observed_tx, observed_rx) = std::sync::mpsc::channel(); - let thread = std::thread::spawn(move || { - let runtime = tokio::runtime::Runtime::new().unwrap(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let (observed_tx, observed_rx) = mpsc::channel(); + let thread = thread::spawn(move || { + let runtime = Runtime::new().unwrap(); runtime.block_on(async move { let _ = shutdown_rx.await; observed_tx.send(()).unwrap(); @@ -346,20 +364,20 @@ mod tests { #[test] fn shutdown_interrupts_a_pending_portal_operation() { - let runtime = tokio::runtime::Runtime::new().unwrap(); + let runtime = Runtime::new().unwrap(); runtime.block_on(async { - let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel(); - let (polled_tx, polled_rx) = tokio::sync::oneshot::channel(); + let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); + let (polled_tx, polled_rx) = oneshot::channel(); let operation = async move { polled_tx.send(()).unwrap(); - std::future::pending::<()>().await + future::pending::<()>().await }; let shutdown_task = tokio::spawn(async move { polled_rx.await.unwrap(); shutdown_tx.send(()).unwrap(); }); - let result = tokio::time::timeout( + let result = time::timeout( Duration::from_secs(1), await_portal_operation(operation, &mut shutdown_rx, "test portal operation"), ) @@ -390,7 +408,7 @@ async fn create_global_shortcuts_session( connection: &Connection, proxy: &GlobalShortcutsProxy<'_>, portal_app_id: &str, - shutdown: &mut tokio::sync::oneshot::Receiver<()>, + shutdown: &mut Receiver<()>, ) -> Result { let mut options: HashMap> = HashMap::new(); options.insert( @@ -435,7 +453,7 @@ async fn bind_toggle_shortcut( proxy: &GlobalShortcutsProxy<'_>, session_handle: &OwnedObjectPath, preferred_trigger: &str, - shutdown: &mut tokio::sync::oneshot::Receiver<()>, + shutdown: &mut Receiver<()>, ) -> Result<()> { let mut shortcut_options: HashMap> = HashMap::new(); shortcut_options.insert( @@ -480,7 +498,7 @@ async fn bind_toggle_shortcut( #[cfg(feature = "portal")] async fn await_portal_operation( operation: impl Future, - shutdown: &mut tokio::sync::oneshot::Receiver<()>, + shutdown: &mut Receiver<()>, description: &str, ) -> Result { tokio::select! { @@ -496,7 +514,7 @@ async fn await_portal_operation( async fn wait_for_request_response( connection: &Connection, request_path: OwnedObjectPath, - shutdown: &mut tokio::sync::oneshot::Receiver<()>, + shutdown: &mut Receiver<()>, ) -> Result<(u32, HashMap)> { let request_proxy = RequestProxy::builder(connection) .path(request_path) @@ -515,7 +533,7 @@ async fn wait_for_request_response( .await? .context("failed to subscribe to Request.Response")?; let response_signal = await_portal_operation( - crate::zbus_stream::next(&mut response_stream), + zbus_stream::next(&mut response_stream), shutdown, "portal Request.Response", ) @@ -524,7 +542,7 @@ async fn wait_for_request_response( let args = response_signal .args() .context("failed to parse Request.Response signal arguments")?; - Ok((args.response, args.results.clone())) + Ok((args.response, args.results)) } #[cfg(feature = "portal")] @@ -572,5 +590,5 @@ fn make_handle_token(prefix: &str) -> String { .duration_since(UNIX_EPOCH) .map(|duration| duration.as_nanos()) .unwrap_or(0); - format!("{}_{}_{}", prefix, std::process::id(), nanos) + format!("{}_{}_{}", prefix, process::id(), nanos) } diff --git a/src/daemon/icons.rs b/src/daemon/icons.rs index c86f6ceff..c4acdfb5b 100644 --- a/src/daemon/icons.rs +++ b/src/daemon/icons.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "tray")] +use std::io::Cursor; + #[cfg(feature = "tray")] use log::warn; #[cfg(feature = "tray")] @@ -24,7 +27,7 @@ pub(crate) fn decode_tray_icon_png() -> Option> { #[cfg(feature = "tray")] fn decode_icon_png(bytes: &[u8]) -> Option { - let mut decoder = Decoder::new(std::io::Cursor::new(bytes)); + let mut decoder = Decoder::new(Cursor::new(bytes)); decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16); let mut reader = decoder.read_info().ok()?; let mut buf = vec![0; reader.output_buffer_size()?]; diff --git a/src/daemon/language.rs b/src/daemon/language.rs index 694c5f8b3..26395108a 100644 --- a/src/daemon/language.rs +++ b/src/daemon/language.rs @@ -8,12 +8,13 @@ use log::warn; use crate::config::Config; use crate::domain::Language; +use crate::i18n; /// Selects the saved `[ui] language`, once, before the tray, shortcut and /// update-notice workers start, whichever of them are enabled. A change takes /// effect when the daemon restarts. pub(super) fn initialize_ui_language() { - crate::i18n::initialize(configured_language()); + i18n::initialize(configured_language()); } /// The saved language, or the default (`auto`) when the config cannot be @@ -31,13 +32,15 @@ fn configured_language() -> Language { #[cfg(test)] mod tests { - use std::fs; + use std::{env, fs}; + + use crate::{env_vars, test_env}; use super::*; use crate::config::test_helpers::with_temp_config_home; fn write_config(contents: &str) { - let path = Config::get_config_path().expect("config path"); + let path = Config::config_path().expect("config path"); fs::create_dir_all(path.parent().expect("config dir")).expect("create config dir"); fs::write(path, contents).expect("write config"); } @@ -70,7 +73,7 @@ mod tests { #[test] fn startup_selects_and_prepares_the_saved_language() { - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::startup_selects_and_prepares_the_saved_language" @@ -81,9 +84,9 @@ mod tests { initialize_ui_language(); - assert_eq!(crate::i18n::language(), Language::SimplifiedChinese); - assert!(crate::i18n::catalog_len() > 0); - assert_eq!(crate::i18n::tr("Boards"), "画板"); + assert_eq!(i18n::language(), Language::SimplifiedChinese); + assert!(i18n::catalog_len() > 0); + assert_eq!(i18n::tr("Boards"), "画板"); }); }, ); @@ -91,7 +94,7 @@ mod tests { #[test] fn the_override_variable_still_beats_the_saved_language() { - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::the_override_variable_still_beats_the_saved_language" @@ -101,12 +104,12 @@ mod tests { write_config("[ui]\nlanguage = \"en\"\n"); // SAFETY: this child process runs only this test. unsafe { - std::env::set_var(crate::env_vars::LANGUAGE_OVERRIDE_ENV, "zh-CN"); + env::set_var(env_vars::LANGUAGE_OVERRIDE_ENV, "zh-CN"); } initialize_ui_language(); - assert_eq!(crate::i18n::language(), Language::SimplifiedChinese); + assert_eq!(i18n::language(), Language::SimplifiedChinese); }); }, ); diff --git a/src/daemon/overlay/launch.rs b/src/daemon/overlay/launch.rs index 9bd78f1a3..3a0d9a54b 100644 --- a/src/daemon/overlay/launch.rs +++ b/src/daemon/overlay/launch.rs @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf}; use crate::daemon::control::DaemonToggleRequest; use crate::domain::Language; +use crate::env_vars; use crate::env_vars::{ DESKTOP_STARTUP_ID_ENV, NO_DETACH_ENV, OVERLAY_HOME_SESSION_ENV, OVERLAY_LANGUAGE_ENV, OVERLAY_PREFERRED_SESSION_ENV, OVERLAY_SESSION_REPORTS_ENV, XDG_ACTIVATION_TOKEN_ENV, @@ -108,7 +109,7 @@ pub(super) fn build_overlay_launch( let mut environment = vec![(OsString::from(NO_DETACH_ENV), Some("1".into()))]; if let Some(generation) = generation { environment.push(( - crate::env_vars::OVERLAY_CHILD_GENERATION_ENV.into(), + env_vars::OVERLAY_CHILD_GENERATION_ENV.into(), Some(generation.into()), )); } @@ -196,7 +197,7 @@ mod tests { let mut expected = vec![(NO_DETACH_ENV.into(), Some("1".into()))]; if let Some(generation) = generation { expected.push(( - crate::env_vars::OVERLAY_CHILD_GENERATION_ENV.into(), + env_vars::OVERLAY_CHILD_GENERATION_ENV.into(), Some(generation.into()), )); } @@ -270,7 +271,7 @@ mod tests { .environment .into_iter() .filter(|(key, _)| { - key == OVERLAY_LANGUAGE_ENV || key == crate::env_vars::LANGUAGE_OVERRIDE_ENV + key == OVERLAY_LANGUAGE_ENV || key == env_vars::LANGUAGE_OVERRIDE_ENV }) .collect::>() }; diff --git a/src/daemon/overlay/lifecycle.rs b/src/daemon/overlay/lifecycle.rs index 77b7be7b4..faee91c42 100644 --- a/src/daemon/overlay/lifecycle.rs +++ b/src/daemon/overlay/lifecycle.rs @@ -7,6 +7,9 @@ use std::time::{Duration, Instant}; use anyhow::{Context, Result}; use log::{debug, info, warn}; +use crate::process_broker::{HelperKind, HelperLifetime}; +use crate::{i18n, process_broker}; + use super::super::protocol_v2::{OverlayChildOwner, ReportedSession}; use super::super::types::{BackendRunner, OverlaySpawnCandidate, OverlayState}; use super::launch::{OverlayLaunchRequest, build_overlay_launch}; @@ -78,7 +81,7 @@ impl OverlayLifecycle { resume_override: &AtomicU8, remembered_session_file: Option<&Path>, daemon_token: &str, - ) -> std::result::Result { + ) -> Result { self.child .reserve() .map_err(OverlayStartFailure::BeforeAttempt)?; @@ -94,14 +97,14 @@ impl OverlayLifecycle { crate::decode_session_override(resume_override.load(Ordering::Acquire)), self.child.generation(), remembered_session_file, - crate::i18n::command_line_language(), + i18n::command_line_language(), ); let attempt = (|| -> Result { let daemon_watchdog = super::super::protocol_v2::open_daemon_watchdog()?; - let child = crate::process_broker::current()?.spawn_with_watchdog( - crate::process_broker::HelperKind::Overlay, - crate::process_broker::HelperLifetime::OwnedChild, + let child = process_broker::current()?.spawn_with_watchdog( + HelperKind::Overlay, + HelperLifetime::OwnedChild, &candidate.program, &launch.arguments, launch.environment, @@ -141,7 +144,11 @@ impl OverlayLifecycle { } pub(super) fn mark_hidden(&mut self) { - debug_assert!(self.child.display_pid().is_none()); + debug_assert!( + self.child.display_pid().is_none(), + "release the overlay child before marking it hidden, got pid {:?}", + self.child.display_pid() + ); self.state = OverlayState::Hidden; self.active_named_session_file = None; self.active.store(false, Ordering::Release); diff --git a/src/daemon/overlay/lifecycle/stop.rs b/src/daemon/overlay/lifecycle/stop.rs index e8d650a66..0cc00c2ec 100644 --- a/src/daemon/overlay/lifecycle/stop.rs +++ b/src/daemon/overlay/lifecycle/stop.rs @@ -1,3 +1,5 @@ +use std::io; + use anyhow::{Context, Result}; use log::{info, warn}; use std::os::fd::AsFd; @@ -26,15 +28,15 @@ impl From for StopFailure { } } -impl From for StopFailure { - fn from(error: std::io::Error) -> Self { +impl From for StopFailure { + fn from(error: io::Error) -> Self { anyhow::Error::from(error).into() } } impl OverlayLifecycle { /// Stops the child, returning the session it last reported. - fn terminate(&mut self) -> std::result::Result, StopFailure> { + fn terminate(&mut self) -> Result, StopFailure> { let mut session = None; if let Some(pid) = self.child.display_pid() { let stop_started = Instant::now(); @@ -123,7 +125,7 @@ impl OverlayLifecycle { /// Stops the overlay, returning the session its child last reported. pub(in crate::daemon::overlay) fn hide( &mut self, - ) -> std::result::Result, StopFailure> { + ) -> Result, StopFailure> { let session = self.terminate()?; self.mark_hidden(); Ok(session) diff --git a/src/daemon/overlay/spawn.rs b/src/daemon/overlay/spawn.rs index 38daa7984..5c8794b6a 100644 --- a/src/daemon/overlay/spawn.rs +++ b/src/daemon/overlay/spawn.rs @@ -1,3 +1,7 @@ +use std::fmt::{Display, Formatter}; +use std::path::Path; +use std::{error, fmt}; + use anyhow::anyhow; use log::{info, warn}; use std::collections::HashSet; @@ -15,6 +19,13 @@ use super::super::types::OverlaySpawnCandidate; use super::super::types::OverlaySpawnErrorInfo; impl Daemon { + #[cfg_attr( + not(feature = "tray"), + expect( + clippy::needless_pass_by_value, + reason = "the tray status keeps the message only when `tray` is enabled" + ) + )] pub(super) fn record_overlay_spawn_failure(&mut self, message: String) { let backoff = self.overlay.record_spawn_failure(); warn!( @@ -54,7 +65,7 @@ impl Daemon { } if let Some(arg0) = env::args_os().next() { - let arg0_path = std::path::Path::new(&arg0); + let arg0_path = Path::new(&arg0); if arg0_path.to_string_lossy().contains('/') { if arg0_path.is_file() { Self::push_spawn_candidate(&mut candidates, &mut seen, arg0, "argv0"); @@ -94,7 +105,7 @@ impl Daemon { &mut self, request: &OverlayLaunchRequest, candidates: &[OverlaySpawnCandidate], - ) -> std::result::Result<(), OverlayStartFailure> { + ) -> Result<(), OverlayStartFailure> { if candidates.is_empty() { return Err(OverlayStartFailure::BeforeAttempt(anyhow!( "No overlay spawn candidates available" @@ -158,8 +169,8 @@ impl SpawnAttemptsFailed { } } -impl std::fmt::Display for SpawnAttemptsFailed { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for SpawnAttemptsFailed { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { write!( formatter, "Unable to launch overlay process (tried current_exe/argv0/{PATH_ENV})" @@ -167,10 +178,14 @@ impl std::fmt::Display for SpawnAttemptsFailed { } } -impl std::error::Error for SpawnAttemptsFailed {} +impl error::Error for SpawnAttemptsFailed {} #[cfg(test)] mod tests { + use std::path::PathBuf; + + use crate::daemon::DaemonToggleRequest; + use super::super::launch::{OverlayLaunch, build_overlay_launch}; use super::*; @@ -208,7 +223,7 @@ mod tests { ] { let mut daemon = Daemon::new(Some("whiteboard".into()), false, None, None); daemon.queue_overlay_launch( - Some(crate::daemon::DaemonToggleRequest { + Some(DaemonToggleRequest { mode: Some("transparent".into()), freeze: true, exit_after_capture, @@ -239,7 +254,7 @@ mod tests { Some("whiteboard".into()), false, None, - Some(std::path::PathBuf::from("/tmp/lecture.wayscriber-session")), + Some(PathBuf::from("/tmp/lecture.wayscriber-session")), ); let launch = build_test_launch(&mut daemon); @@ -262,13 +277,11 @@ mod tests { Some("whiteboard".into()), false, None, - Some(std::path::PathBuf::from("/tmp/default.wayscriber-session")), + Some(PathBuf::from("/tmp/default.wayscriber-session")), ); daemon.queue_overlay_launch( - Some(crate::daemon::DaemonToggleRequest { - session_file: Some(std::path::PathBuf::from( - "/tmp/requested.wayscriber-session", - )), + Some(DaemonToggleRequest { + session_file: Some(PathBuf::from("/tmp/requested.wayscriber-session")), ..Default::default() }), None, diff --git a/src/daemon/overlay/tests.rs b/src/daemon/overlay/tests.rs index 4eda2607f..a0ac98aa4 100644 --- a/src/daemon/overlay/tests.rs +++ b/src/daemon/overlay/tests.rs @@ -1,3 +1,12 @@ +use std::ffi::OsString; +use std::os::unix; +use std::{env, thread}; + +use crate::daemon::protocol_v2; +use crate::daemon::protocol_v2::ProtocolToken; +use crate::domain::Language; +use crate::{env_vars, i18n, paths, process_broker, test_env, test_temp}; + use super::*; use std::ffi::OsStr; use std::fs; @@ -57,23 +66,20 @@ pub(in crate::daemon) fn assert_token_only_pending_launch(daemon: &Daemon, token /// Runs `body` with a broker whose overlay launches of this test binary act /// as [`fake_overlay`] children. Receipts land in the runtime root passed to `body`. fn with_fixture(ignore_term: bool, body: impl FnOnce(&mut Daemon, OverlaySpawnCandidate, &Path)) { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let behavior = if ignore_term { fake_overlay::IGNORES_TERM } else { fake_overlay::STOPS_ON_TERM }; - crate::test_env::with_env_vars( + test_env::with_env_vars( &[ - ( - crate::env_vars::XDG_RUNTIME_DIR_ENV, - Some(temp.path().as_os_str()), - ), + (env_vars::XDG_RUNTIME_DIR_ENV, Some(temp.path().as_os_str())), (fake_overlay::FAKE_OVERLAY_ENV, Some(OsStr::new(behavior))), ], || { - let _broker = crate::process_broker::start_for_runtime().unwrap(); + let _broker = process_broker::start_for_runtime().unwrap(); let mut daemon = Daemon::new( Some("transparent".into()), false, @@ -81,11 +87,9 @@ fn with_fixture(ignore_term: bool, body: impl FnOnce(&mut Daemon, OverlaySpawnCa Some(PathBuf::from("/tmp/home.wayscriber-session")), ); - daemon.instance_token = crate::daemon::protocol_v2::ProtocolToken::generate() - .unwrap() - .to_string(); + daemon.instance_token = ProtocolToken::generate().unwrap().to_string(); let candidate = OverlaySpawnCandidate { - program: std::env::current_exe().unwrap().into_os_string(), + program: env::current_exe().unwrap().into_os_string(), source: "test binary", }; @@ -109,7 +113,7 @@ fn receipt(root: &Path) -> serde_json::Value { Instant::now() < deadline, "owned overlay did not publish its launch receipt" ); - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } } @@ -118,7 +122,7 @@ fn assert_retired(daemon: &Daemon, generation: &str) { assert!(!daemon.overlay.active_flag().load(Ordering::Acquire)); assert!(daemon.overlay.active_named_session_file().is_none()); assert!(daemon.overlay.poll_fd().is_none()); - let proofs = crate::daemon::protocol_v2::command_root().join("children"); + let proofs = protocol_v2::command_root().join("children"); for suffix in ["active", "enabled", "ready", "signals"] { assert!(!proofs.join(format!("{generation}.{suffix}")).exists()); } @@ -126,7 +130,7 @@ fn assert_retired(daemon: &Daemon, generation: &str) { } fn session_report(generation: &str) -> PathBuf { - crate::paths::daemon_command_dir() + paths::daemon_command_dir() .join("overlay-targets") .join(format!("{generation}.target")) } @@ -159,7 +163,7 @@ fn wait_until_retired(daemon: &mut Daemon) { while daemon.overlay.state() == OverlayState::Visible { daemon.update_overlay_process_state().unwrap(); assert!(Instant::now() < deadline, "exited child was not retired"); - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } } @@ -169,13 +173,13 @@ const REMEMBERED: &str = "/tmp/remembered.wayscriber-session"; #[test] fn a_daemon_started_with_a_language_gives_it_to_its_overlays() { // The command-line language is set once per process, so this runs alone. - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::a_daemon_started_with_a_language_gives_it_to_its_overlays" ), || { - crate::i18n::set_command_line_language(crate::domain::Language::SimplifiedChinese); + i18n::set_command_line_language(Language::SimplifiedChinese); with_fixture(false, |daemon, _, root| { let receipt = show(daemon, root); assert_eq!(receipt["language"], "zh-CN"); @@ -379,7 +383,7 @@ fn real_child_hide_restart_and_natural_retirement_clear_target_and_visibility() while daemon.overlay.state() == OverlayState::Visible { daemon.update_overlay_process_state().unwrap(); assert!(Instant::now() < deadline, "exited child was not retired"); - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } assert_retired(daemon, second["generation"].as_str().unwrap()); }); @@ -390,7 +394,7 @@ fn real_spawn_failure_drops_options_and_token_before_retry() { with_fixture(false, |daemon, candidate, root| { // A real child that starts and exits before publishing readiness. let failed_program = root.join(fake_overlay::EXITS_BEFORE_READY); - std::os::unix::fs::symlink(&candidate.program, &failed_program).unwrap(); + unix::fs::symlink(&candidate.program, &failed_program).unwrap(); daemon.queue_overlay_launch( Some(DaemonToggleRequest { mode: Some("whiteboard".into()), @@ -418,7 +422,7 @@ fn real_spawn_failure_drops_options_and_token_before_retry() { format!("{failure:#}"), format!( "Unable to launch overlay process (tried current_exe/argv0/{})", - crate::env_vars::PATH_ENV + env_vars::PATH_ENV ) ); let attempts = failure @@ -450,7 +454,7 @@ fn real_spawn_failure_drops_options_and_token_before_retry() { let deadline = Instant::now() + Duration::from_secs(3); while daemon.overlay_start_backoff().is_some() { assert!(Instant::now() < deadline); - std::thread::sleep(Duration::from_millis(10)); + thread::sleep(Duration::from_millis(10)); } let request = daemon.take_pending_launch(); @@ -482,7 +486,7 @@ fn real_spawn_failure_drops_options_and_token_before_retry() { #[test] fn preparation_error_keeps_only_the_token_and_does_not_replay_options() { with_fixture(false, |daemon, candidate, _| { - let commands = crate::daemon::protocol_v2::command_root(); + let commands = protocol_v2::command_root(); fs::create_dir_all(&commands).unwrap(); fs::write(commands.join("children"), b"not a proof directory").unwrap(); daemon.queue_overlay_launch( @@ -508,10 +512,10 @@ fn preparation_error_keeps_only_the_token_and_does_not_replay_options() { "--session-file", "/tmp/home.wayscriber-session" ] - .map(std::ffi::OsString::from) + .map(OsString::from) ); assert!(actual.environment.contains(&( - crate::env_vars::XDG_ACTIVATION_TOKEN_ENV.into(), + env_vars::XDG_ACTIVATION_TOKEN_ENV.into(), Some("retained-token".into()) ))); assert!( diff --git a/src/daemon/overlay/tests/fake_overlay.rs b/src/daemon/overlay/tests/fake_overlay.rs index 1967d74b4..babc853ee 100644 --- a/src/daemon/overlay/tests/fake_overlay.rs +++ b/src/daemon/overlay/tests/fake_overlay.rs @@ -11,8 +11,10 @@ use std::convert::Infallible; use std::ffi::OsStr; -use std::path::Path; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; use std::time::Duration; +use std::{env, fs, process, thread}; use anyhow::{Context, Result}; @@ -21,6 +23,8 @@ use crate::daemon::protocol_v2::{ publish_ready_from_environment, publish_session_from_environment, publish_signal_ready_from_environment, }; +use crate::durable_io::AtomicWriteOptions; +use crate::{durable_io, env_vars, launch_environment, test_fake_helper}; /// Fixture marker inherited by overlay launches from the test's broker. pub(super) const FAKE_OVERLAY_ENV: &str = "WAYSCRIBER_TEST_FAKE_OVERLAY"; @@ -44,22 +48,23 @@ const EXIT_BEFORE_READY_STATUS: i32 = 7; /// The fake overlay could not serve; the test that launched it then fails. const FAILURE_STATUS: i32 = 1; -// SAFETY: the loader calls each `.init_array` entry once, before `main`, with -// the C calling convention. glibc passes `argc`, `argv`, and `envp`; under the -// C ABI a function that declares no parameters ignores extra arguments, so an -// `extern "C" fn()` is sound to register here. Without both markers the -// function only reads the environment and returns. With them it never returns -// into `main`: it serves or exits, and a panic aborts instead of unwinding -// through the loader because the function is `extern "C"`. +// The `unsafe(link_section)` attribute below is sound: the loader calls each +// `.init_array` entry once, before `main`, with the C calling convention. +// glibc passes `argc`, `argv`, and `envp`; under the C ABI a function that +// declares no parameters ignores extra arguments, so an `extern "C" fn()` is +// sound to register here. Without both markers the function only reads the +// environment and returns. With them it never returns into `main`: it serves +// or exits, and a panic aborts instead of unwinding through the loader because +// the function is `extern "C"`. #[used] #[unsafe(link_section = ".init_array")] static RUN_AS_FAKE_OVERLAY: extern "C" fn() = run_as_fake_overlay; extern "C" fn run_as_fake_overlay() { - let Some(behavior) = std::env::var_os(FAKE_OVERLAY_ENV) else { + let Some(behavior) = env::var_os(FAKE_OVERLAY_ENV) else { return; }; - if std::env::var_os(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV).is_none() { + if env::var_os(env_vars::OVERLAY_CHILD_GENERATION_ENV).is_none() { return; } @@ -67,12 +72,12 @@ extern "C" fn run_as_fake_overlay() { if let Err(error) = record(STARTED_THEN_EXITED, b"") { eprintln!("fake overlay failed: {error:#}"); } - std::process::exit(EXIT_BEFORE_READY_STATUS); + process::exit(EXIT_BEFORE_READY_STATUS); } let Err(error) = serve(behavior == IGNORES_TERM); eprintln!("fake overlay failed: {error:#}"); - std::process::exit(FAILURE_STATUS); + process::exit(FAILURE_STATUS); } fn serve(ignore_term: bool) -> Result { @@ -91,7 +96,7 @@ fn serve(ignore_term: bool) -> Result { active_generation_from_environment()?, ActiveGeneration::Enabled { .. } ) { - std::thread::sleep(Duration::from_millis(2)); + thread::sleep(Duration::from_millis(2)); } // Reported before the receipt, so a test that has the receipt can rely on @@ -99,19 +104,19 @@ fn serve(ignore_term: bool) -> Result { let exit = report_session()?; write_receipt()?; if exit { - std::process::exit(0); + process::exit(0); } loop { - std::thread::sleep(Duration::from_secs(60)); + thread::sleep(Duration::from_secs(60)); } } /// Follows the test's [`SESSION_INSTRUCTION`], returning whether to exit. fn report_session() -> Result { - let instruction = match std::fs::read(runtime_root()?.join(SESSION_INSTRUCTION)) { + let instruction = match fs::read(runtime_root()?.join(SESSION_INSTRUCTION)) { Ok(bytes) => serde_json::from_slice::(&bytes)?, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(false), Err(error) => return Err(error).context("failed to read the session instruction"), }; if let Some(report) = instruction["report"].as_str() { @@ -131,20 +136,20 @@ fn report_session() -> Result { fn write_receipt() -> Result<()> { // As launched: GTK may already have unset the startup-notification variables. let launched_with = |name: &str| { - crate::launch_environment::var_os(name).map(|value| value.to_string_lossy().into_owned()) + launch_environment::var_os(name).map(|value| value.to_string_lossy().into_owned()) }; let receipt = serde_json::json!({ "args": launch_arguments()?, - "token": launched_with(crate::env_vars::XDG_ACTIVATION_TOKEN_ENV), - "startup": launched_with(crate::env_vars::DESKTOP_STARTUP_ID_ENV), + "token": launched_with(env_vars::XDG_ACTIVATION_TOKEN_ENV), + "startup": launched_with(env_vars::DESKTOP_STARTUP_ID_ENV), "resume": launched_with(crate::RESUME_SESSION_ENV), - "detach": launched_with(crate::env_vars::NO_DETACH_ENV), - "reports": launched_with(crate::env_vars::OVERLAY_SESSION_REPORTS_ENV), - "home": launched_with(crate::env_vars::OVERLAY_HOME_SESSION_ENV), - "preferred": launched_with(crate::env_vars::OVERLAY_PREFERRED_SESSION_ENV), - "language": launched_with(crate::env_vars::OVERLAY_LANGUAGE_ENV), - "pid": std::process::id(), - "generation": std::env::var(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV)?, + "detach": launched_with(env_vars::NO_DETACH_ENV), + "reports": launched_with(env_vars::OVERLAY_SESSION_REPORTS_ENV), + "home": launched_with(env_vars::OVERLAY_HOME_SESSION_ENV), + "preferred": launched_with(env_vars::OVERLAY_PREFERRED_SESSION_ENV), + "language": launched_with(env_vars::OVERLAY_LANGUAGE_ENV), + "pid": process::id(), + "generation": env::var(env_vars::OVERLAY_CHILD_GENERATION_ENV)?, }); record("receipt", &serde_json::to_vec(&receipt)?) @@ -152,19 +157,19 @@ fn write_receipt() -> Result<()> { /// Writes `.` into the fixture's runtime directory. fn record(kind: &str, contents: &[u8]) -> Result<()> { - let generation = std::env::var(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV)?; + let generation = env::var(env_vars::OVERLAY_CHILD_GENERATION_ENV)?; - crate::durable_io::write_atomic( + durable_io::write_atomic( &runtime_root()?.join(format!("{generation}.{kind}")), contents, - crate::durable_io::AtomicWriteOptions::private_runtime_file(), + AtomicWriteOptions::private_runtime_file(), )?; Ok(()) } -fn runtime_root() -> Result { - std::env::var_os(crate::env_vars::XDG_RUNTIME_DIR_ENV) +fn runtime_root() -> Result { + env::var_os(env_vars::XDG_RUNTIME_DIR_ENV) .map(Into::into) .context("fake overlay needs the fixture's runtime directory") } @@ -172,7 +177,7 @@ fn runtime_root() -> Result { /// Whether this process was started as `name`: the broker passes the program /// path it was given as `argv[0]`, which for a link is the link's own path. fn launched_as(name: &str) -> bool { - crate::test_fake_helper::launch_arguments() + test_fake_helper::launch_arguments() .ok() .and_then(|arguments| arguments.into_iter().next()) .is_some_and(|program| Path::new(&program).file_name() == Some(OsStr::new(name))) @@ -180,7 +185,7 @@ fn launched_as(name: &str) -> bool { /// The overlay arguments this process was launched with, after `argv[0]`. fn launch_arguments() -> Result> { - crate::test_fake_helper::launch_arguments()? + test_fake_helper::launch_arguments()? .into_iter() .skip(1) .map(|argument| { diff --git a/src/daemon/protocol_v2/action.rs b/src/daemon/protocol_v2/action.rs index 6e9c45204..6f42f7df2 100644 --- a/src/daemon/protocol_v2/action.rs +++ b/src/daemon/protocol_v2/action.rs @@ -1,20 +1,27 @@ -use std::collections::{BTreeMap, BTreeSet}; +#[cfg(test)] +use std::collections::BTreeMap; use std::fs::{self, File, OpenOptions}; use std::io::{self, ErrorKind}; use std::os::fd::AsRawFd; use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; use std::path::{Path, PathBuf}; +#[cfg(test)] +use std::sync::LazyLock; +#[cfg(test)] +use std::sync::Mutex; use anyhow::{Context, Result, anyhow, bail}; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; +use crate::durable_io; +use crate::durable_io::AtomicWriteOptions; + use super::digest::sha256_hex; -use super::linux::create_private_directory; use super::wire::{ - ACTION_ENVELOPE_PROTOCOL_VERSION, MAX_ACTION_ENVELOPE_BYTES, bounded_reason, canonical_json, - fresh_id, parse_canonical_json, validate_digest, validate_id, validate_reason, validate_token, + ACTION_ENVELOPE_PROTOCOL_VERSION, MAX_ACTION_ENVELOPE_BYTES, canonical_json, fresh_id, + parse_canonical_json, validate_digest, validate_id, validate_reason, validate_token, }; -use super::{BootClock, BootIdentity, NamespaceIdentity}; use crate::tray_action::TrayAction; mod abandon; @@ -26,8 +33,8 @@ const MAX_ACTIONS: usize = 2048; const MAX_ACTION_QUARANTINE: usize = 1024; #[cfg(test)] -static ANONYMOUS_PUBLISH_FAILURES: std::sync::LazyLock>> = - std::sync::LazyLock::new(|| std::sync::Mutex::new(BTreeMap::new())); +static ANONYMOUS_PUBLISH_FAILURES: LazyLock>> = + LazyLock::new(|| Mutex::new(BTreeMap::new())); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "owner", deny_unknown_fields)] @@ -66,11 +73,11 @@ struct ActionRecord { } #[derive(Serialize)] -struct ActionDigestPayload<'a> { +struct ActionDigestPayload<'action> { protocol_version: u16, - action_id: &'a str, + action_id: &'action str, action_order: u64, - owner: &'a ActionOwner, + owner: &'action ActionOwner, action: TrayAction, } @@ -125,7 +132,7 @@ fn action_root() -> PathBuf { fn consume_anonymous_publish_failure(root: &Path) -> bool { let mut failures = ANONYMOUS_PUBLISH_FAILURES .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("injected publish failures"); let remove = match failures.get_mut(root) { Some(remaining) => { *remaining -= 1; @@ -217,8 +224,8 @@ fn try_open_journal_lock(root: &Path, nonblocking: bool) -> Result> if !file.metadata()?.is_file() { bail!("action journal lock is not a regular file"); } - // SAFETY: file owns the descriptor; flock retains no pointer. let operation = libc::LOCK_EX | if nonblocking { libc::LOCK_NB } else { 0 }; + // SAFETY: file owns the descriptor; flock retains no pointer. if unsafe { libc::flock(file.as_raw_fd(), operation) } != 0 { let error = io::Error::last_os_error(); if nonblocking && error.kind() == ErrorKind::WouldBlock { @@ -240,15 +247,11 @@ fn unlock(file: &File) -> Result<()> { fn write_record(path: &Path, value: &T) -> Result<()> { let bytes = canonical_json(value, MAX_ACTION_ENVELOPE_BYTES)?; - crate::durable_io::write_atomic( - path, - &bytes, - crate::durable_io::AtomicWriteOptions::private_runtime_file(), - ) - .with_context(|| format!("failed to write action record {}", path.display())) + durable_io::write_atomic(path, &bytes, AtomicWriteOptions::private_runtime_file()) + .with_context(|| format!("failed to write action record {}", path.display())) } -fn read_record(path: &Path) -> Result { +fn read_record(path: &Path) -> Result { let bytes = super::linux::read_bounded_regular_file(path, MAX_ACTION_ENVELOPE_BYTES)?; parse_canonical_json(&bytes, MAX_ACTION_ENVELOPE_BYTES) } diff --git a/src/daemon/protocol_v2/action/abandon.rs b/src/daemon/protocol_v2/action/abandon.rs index 424d6f699..146e32482 100644 --- a/src/daemon/protocol_v2/action/abandon.rs +++ b/src/daemon/protocol_v2/action/abandon.rs @@ -1,4 +1,10 @@ -use super::*; +use super::super::wire::bounded_reason; +use super::{ + ActionJournal, ActionRecord, JournalState, PreparedAction, open_journal_lock, read_record, + unlock, write_record, +}; +use anyhow::{Result, anyhow, bail}; +use std::fs; impl ActionJournal { pub(crate) fn abandon(&self, prepared: &PreparedAction, reason: &str) -> Result<()> { diff --git a/src/daemon/protocol_v2/action/claim.rs b/src/daemon/protocol_v2/action/claim.rs index b5ead0727..2c265d49c 100644 --- a/src/daemon/protocol_v2/action/claim.rs +++ b/src/daemon/protocol_v2/action/claim.rs @@ -1,4 +1,12 @@ -use super::*; +use super::super::wire::{fresh_id, validate_token}; +use super::{ + ActionClaimOutcome, ActionJournal, ActionOwner, ActionRecord, ClaimedAction, JournalState, + MAX_ACTIONS, PreparedAction, inode_identity, parse_action_name, quarantine_action, queue_dir, + read_record, try_open_journal_lock, unlock, validate_record, write_record, +}; +use anyhow::{Result, anyhow, bail}; +use std::collections::{BTreeMap, BTreeSet}; +use std::{fs, io}; impl ActionJournal { pub(super) fn claim_next_inner( diff --git a/src/daemon/protocol_v2/action/claimed.rs b/src/daemon/protocol_v2/action/claimed.rs index c019f4319..143284755 100644 --- a/src/daemon/protocol_v2/action/claimed.rs +++ b/src/daemon/protocol_v2/action/claimed.rs @@ -1,4 +1,13 @@ -use super::*; +use super::super::wire::bounded_reason; +#[cfg(test)] +use super::open_journal_lock; +use super::{ + ActionFinishOutcome, ActionOwner, ActionRecord, ClaimedAction, JournalState, PreparedAction, + read_record, try_open_journal_lock, unlock, write_record, +}; +use crate::tray_action::TrayAction; +use anyhow::{Result, anyhow, bail}; +use std::fs; impl ClaimedAction { pub(crate) fn action(&self) -> TrayAction { diff --git a/src/daemon/protocol_v2/action/open.rs b/src/daemon/protocol_v2/action/open.rs index aa5485a37..21acf948b 100644 --- a/src/daemon/protocol_v2/action/open.rs +++ b/src/daemon/protocol_v2/action/open.rs @@ -1,4 +1,21 @@ -use super::*; +use crate::durable_io; + +use super::super::linux::create_private_directory; +use super::super::wire::{ACTION_ENVELOPE_PROTOCOL_VERSION, fresh_id, validate_id, validate_token}; +use super::super::{BootClock, BootIdentity, NamespaceIdentity}; +#[cfg(test)] +use super::{ANONYMOUS_PUBLISH_FAILURES, ClaimedAction, consume_anonymous_publish_failure}; +use super::{ + ActionClaimOutcome, ActionJournal, ActionOwner, ActionRecord, JournalHighWater, JournalState, + MAX_ACTION_QUARANTINE, MAX_ACTIONS, PreparedAction, action_name, action_root, digest_payload, + open_journal_lock, parse_action_name, quarantine_dir, queue_dir, read_record, unlock, + validate_record, write_record, +}; +use crate::tray_action::TrayAction; +use anyhow::{Context, Result, anyhow, bail}; +use std::collections::BTreeMap; +use std::fs; +use std::io::ErrorKind; impl ActionJournal { pub(crate) fn open() -> Result { @@ -27,7 +44,7 @@ impl ActionJournal { pub(crate) fn fail_next_anonymous_publications(&self, count: usize) { let mut failures = ANONYMOUS_PUBLISH_FAILURES .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("injected publish failures"); if count == 0 { failures.remove(&self.root); } else { @@ -48,7 +65,7 @@ impl ActionJournal { // Every journal write holds this lock, so a temporary for an // action record is what a writer that died before its rename // left behind, not work in progress. - if crate::durable_io::temp_file_target(&name) + if durable_io::temp_file_target(&name) .is_some_and(|target| parse_action_name(target).is_ok()) { match fs::remove_file(entry.path()) { diff --git a/src/daemon/protocol_v2/action/tests.rs b/src/daemon/protocol_v2/action/tests.rs index e51c1a9b9..8ba46acbb 100644 --- a/src/daemon/protocol_v2/action/tests.rs +++ b/src/daemon/protocol_v2/action/tests.rs @@ -1,4 +1,6 @@ -use std::env; +use std::{env, thread}; + +use crate::{test_env, test_temp}; use super::*; use crate::env_vars::XDG_RUNTIME_DIR_ENV; @@ -28,8 +30,8 @@ fn action_digests_match_protocol_v2_golden_values() { } fn with_runtime(run: impl FnOnce() -> T) -> T { - let _guard = crate::test_env::lock(); - let temp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let temp = test_temp::tempdir().unwrap(); let previous = env::var_os(XDG_RUNTIME_DIR_ENV); // SAFETY: serialized by the test environment mutex. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, temp.path()) }; @@ -178,6 +180,8 @@ fn event_loop_claim_and_finish_defer_instead_of_waiting_for_locks() { .open(decision_path) .unwrap(); assert_eq!( + // SAFETY: `held_decision` keeps the descriptor open for the call, + // and flock retains no pointer. unsafe { libc::flock(held_decision.as_raw_fd(), libc::LOCK_EX) }, 0 ); @@ -185,6 +189,8 @@ fn event_loop_claim_and_finish_defer_instead_of_waiting_for_locks() { panic!("contended command finish should defer"); }; assert_eq!( + // SAFETY: `held_decision` keeps the descriptor open for the call, + // and flock retains no pointer. unsafe { libc::flock(held_decision.as_raw_fd(), libc::LOCK_UN) }, 0 ); @@ -218,7 +224,7 @@ fn cancellation_during_action_preparation_leaves_a_collectable_tombstone() { let command = owner.claim_next().unwrap().unwrap(); let held_journal_lock = open_journal_lock(&journal.root).unwrap(); let worker_journal = journal.clone(); - let worker = std::thread::spawn(move || { + let worker = thread::spawn(move || { let mut command = command; let outcome = command.prepare_action(&worker_journal); (outcome, command) diff --git a/src/daemon/protocol_v2/child.rs b/src/daemon/protocol_v2/child.rs index 2da9ce56f..551d581db 100644 --- a/src/daemon/protocol_v2/child.rs +++ b/src/daemon/protocol_v2/child.rs @@ -1,12 +1,18 @@ -use std::fs; use std::io::ErrorKind; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd}; use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use std::thread::Builder; use std::time::Duration; +use std::{env, fs, io, process, thread}; use anyhow::{Context, Result, anyhow, bail}; use serde::{Deserialize, Serialize}; +use crate::durable_io::{AtomicWriteOptions, OverwriteMode, PermissionPolicy, SymlinkPolicy}; +use crate::process_broker::BrokerChild; +use crate::{durable_io, env_vars}; + use super::session_target::{ ReportedSession, discard_session_report, read_trusted_session_report, take_final_session_report, }; @@ -27,7 +33,7 @@ pub(crate) enum OverlayChildPhase { struct OwnedOverlayChild { display_pid: u32, pidfd: OwnedFd, - child: crate::process_broker::BrokerChild, + child: BrokerChild, /// Captured when its readiness proved its identity, so its reports can be /// checked after it exits. process_start_ticks: Option, @@ -118,7 +124,7 @@ impl OverlayChildOwner { Ok(self.generation().expect("reserved generation exists")) } - pub(crate) fn start(&mut self, child: crate::process_broker::BrokerChild) -> Result<()> { + pub(crate) fn start(&mut self, child: BrokerChild) -> Result<()> { if self.phase != OverlayChildPhase::Reserved { let _ = child.kill_wait(); bail!("overlay child owner is not reserved"); @@ -202,7 +208,7 @@ impl OverlayChildOwner { "overlay signal authority did not become ready before deadline" ); } - std::thread::sleep(Duration::from_millis(10)); + thread::sleep(Duration::from_millis(10)); continue; } Err(error) => { @@ -227,13 +233,13 @@ impl OverlayChildOwner { daemon_token: daemon_token.to_owned(), }; let enabled_bytes = super::wire::canonical_json(&enabled, 1024)?; - crate::durable_io::write_atomic( + durable_io::write_atomic( &enabled_path(&generation), &enabled_bytes, - crate::durable_io::AtomicWriteOptions { - overwrite: crate::durable_io::OverwriteMode::CreateNew, - permissions: crate::durable_io::PermissionPolicy::FixedMode(0o600), - symlink: crate::durable_io::SymlinkPolicy::Reject, + AtomicWriteOptions { + overwrite: OverwriteMode::CreateNew, + permissions: PermissionPolicy::FixedMode(0o600), + symlink: SymlinkPolicy::Reject, sync_file: true, sync_parent: true, }, @@ -254,7 +260,7 @@ impl OverlayChildOwner { let _ = self.force_kill_and_wait(); bail!("overlay child did not become ready before deadline"); } - std::thread::sleep(Duration::from_millis(10)); + thread::sleep(Duration::from_millis(10)); } } @@ -377,23 +383,23 @@ struct OverlayEnabledRecord { daemon_token: String, } -fn ready_dir() -> std::path::PathBuf { +fn ready_dir() -> PathBuf { super::command_root().join("children") } -fn ready_path(generation: &str) -> std::path::PathBuf { +fn ready_path(generation: &str) -> PathBuf { ready_dir().join(format!("{generation}.ready")) } -fn active_path(generation: &str) -> std::path::PathBuf { +fn active_path(generation: &str) -> PathBuf { ready_dir().join(format!("{generation}.active")) } -fn enabled_path(generation: &str) -> std::path::PathBuf { +fn enabled_path(generation: &str) -> PathBuf { ready_dir().join(format!("{generation}.enabled")) } -fn signals_path(generation: &str) -> std::path::PathBuf { +fn signals_path(generation: &str) -> PathBuf { ready_dir().join(format!("{generation}.signals")) } @@ -407,7 +413,7 @@ fn clear_generation_records(generation: &str) { pub(crate) fn recover_stale_child_records() -> Result<()> { let directory = ready_dir(); let entries = match fs::read_dir(&directory) { - Ok(entries) => entries.take(129).collect::>>()?, + Ok(entries) => entries.take(129).collect::>>()?, Err(error) if error.kind() == ErrorKind::NotFound => return Ok(()), Err(error) => return Err(error).context("failed to enumerate overlay child proofs"), }; @@ -419,7 +425,7 @@ pub(crate) fn recover_stale_child_records() -> Result<()> { .file_name() .into_string() .map_err(|_| anyhow!("overlay child proof name is not UTF-8"))?; - if let Some(target) = crate::durable_io::temp_file_target(&name) { + if let Some(target) = durable_io::temp_file_target(&name) { collect_leftover_proof_temp(&entry.path(), target)?; continue; } @@ -491,7 +497,7 @@ pub(crate) fn recover_stale_child_records() -> Result<()> { /// still rename the temporary belongs to a daemon that is gone, and that /// daemon's watchdog is already stopping it. A temporary for anything but a /// proof record is not this directory's own and still fails recovery. -fn collect_leftover_proof_temp(path: &std::path::Path, target: &str) -> Result<()> { +fn collect_leftover_proof_temp(path: &Path, target: &str) -> Result<()> { let (generation, kind) = target .rsplit_once('.') .ok_or_else(|| anyhow!("invalid overlay child proof temporary"))?; @@ -508,12 +514,12 @@ fn collect_leftover_proof_temp(path: &std::path::Path, target: &str) -> Result<( } pub(crate) fn open_daemon_watchdog() -> Result { - super::linux::open_pidfd(std::process::id()) + super::linux::open_pidfd(process::id()) .context("failed to open daemon pidfd for overlay watchdog") } pub(crate) fn start_daemon_watchdog_from_environment() -> Result<()> { - let Some(raw) = std::env::var_os(crate::env_vars::DAEMON_WATCHDOG_FD_ENV) else { + let Some(raw) = env::var_os(env_vars::DAEMON_WATCHDOG_FD_ENV) else { return Ok(()); }; let raw = raw @@ -530,7 +536,7 @@ pub(crate) fn start_daemon_watchdog_from_environment() -> Result<()> { let descriptor = unsafe { OwnedFd::from_raw_fd(raw) }; // SAFETY: descriptor is owned and F_SETFD changes only exec inheritance. if unsafe { libc::fcntl(descriptor.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) } != 0 { - return Err(std::io::Error::last_os_error()) + return Err(io::Error::last_os_error()) .context("failed to protect daemon watchdog descriptor"); } super::linux::validate_pidfd(descriptor.as_fd()) @@ -538,9 +544,9 @@ pub(crate) fn start_daemon_watchdog_from_environment() -> Result<()> { // SAFETY: application startup has not launched runtime worker threads yet; // removing this private bootstrap marker prevents accidental inheritance. unsafe { - std::env::remove_var(crate::env_vars::DAEMON_WATCHDOG_FD_ENV); + env::remove_var(env_vars::DAEMON_WATCHDOG_FD_ENV); } - std::thread::Builder::new() + Builder::new() .name("wayscriber-daemon-watchdog".into()) .spawn(move || { let mut pollfd = libc::pollfd { @@ -557,9 +563,7 @@ pub(crate) fn start_daemon_watchdog_from_environment() -> Result<()> { // fail-stop condition for its internal overlay child. unsafe { super::linux::fail_stop(70) } } - if result < 0 - && std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted - { + if result < 0 && io::Error::last_os_error().kind() == ErrorKind::Interrupted { continue; } // SAFETY: an unusable parent-death channel cannot safely @@ -572,7 +576,7 @@ pub(crate) fn start_daemon_watchdog_from_environment() -> Result<()> { } pub(crate) fn publish_ready_from_environment() -> Result<()> { - let Some(generation) = std::env::var_os(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV) else { + let Some(generation) = env::var_os(env_vars::OVERLAY_CHILD_GENERATION_ENV) else { return Ok(()); }; let generation = generation @@ -586,17 +590,17 @@ pub(crate) fn publish_ready_from_environment() -> Result<()> { let active = OverlayActiveRecord { protocol_version: super::wire::DAEMON_CHILD_PROTOCOL_VERSION, generation: generation.to_owned(), - pid: std::process::id(), + pid: process::id(), process_start_ticks, }; let active_bytes = super::wire::canonical_json(&active, 1024)?; - crate::durable_io::write_atomic( + durable_io::write_atomic( &active_path(generation), &active_bytes, - crate::durable_io::AtomicWriteOptions { - overwrite: crate::durable_io::OverwriteMode::CreateNew, - permissions: crate::durable_io::PermissionPolicy::FixedMode(0o600), - symlink: crate::durable_io::SymlinkPolicy::Reject, + AtomicWriteOptions { + overwrite: OverwriteMode::CreateNew, + permissions: PermissionPolicy::FixedMode(0o600), + symlink: SymlinkPolicy::Reject, sync_file: true, sync_parent: true, }, @@ -604,17 +608,17 @@ pub(crate) fn publish_ready_from_environment() -> Result<()> { let record = OverlayReadyRecord { protocol_version: super::wire::DAEMON_CHILD_PROTOCOL_VERSION, generation: generation.to_owned(), - pid: std::process::id(), + pid: process::id(), process_start_ticks, }; let bytes = super::wire::canonical_json(&record, 1024)?; - crate::durable_io::write_atomic( + durable_io::write_atomic( &ready_path(generation), &bytes, - crate::durable_io::AtomicWriteOptions { - overwrite: crate::durable_io::OverwriteMode::CreateNew, - permissions: crate::durable_io::PermissionPolicy::FixedMode(0o600), - symlink: crate::durable_io::SymlinkPolicy::Reject, + AtomicWriteOptions { + overwrite: OverwriteMode::CreateNew, + permissions: PermissionPolicy::FixedMode(0o600), + symlink: SymlinkPolicy::Reject, sync_file: true, sync_parent: true, }, @@ -623,7 +627,7 @@ pub(crate) fn publish_ready_from_environment() -> Result<()> { } pub(crate) fn publish_signal_ready_from_environment() -> Result<()> { - let Some(generation) = std::env::var_os(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV) else { + let Some(generation) = env::var_os(env_vars::OVERLAY_CHILD_GENERATION_ENV) else { return Ok(()); }; let generation = generation @@ -632,19 +636,19 @@ pub(crate) fn publish_signal_ready_from_environment() -> Result<()> { super::wire::validate_id(generation)?; let active_bytes = super::linux::read_bounded_regular_file(&active_path(generation), 1024)?; let active: OverlayActiveRecord = super::wire::parse_canonical_json(&active_bytes, 1024)?; - if active.pid != std::process::id() + if active.pid != process::id() || active.process_start_ticks != super::linux::current_process_start_ticks()? || active.generation != generation { bail!("cannot publish signal readiness for a different overlay child"); } - crate::durable_io::write_atomic( + durable_io::write_atomic( &signals_path(generation), &active_bytes, - crate::durable_io::AtomicWriteOptions { - overwrite: crate::durable_io::OverwriteMode::CreateNew, - permissions: crate::durable_io::PermissionPolicy::FixedMode(0o600), - symlink: crate::durable_io::SymlinkPolicy::Reject, + AtomicWriteOptions { + overwrite: OverwriteMode::CreateNew, + permissions: PermissionPolicy::FixedMode(0o600), + symlink: SymlinkPolicy::Reject, sync_file: true, sync_parent: true, }, @@ -659,7 +663,7 @@ pub(crate) enum ActiveGeneration { } pub(crate) fn active_generation_from_environment() -> Result { - let Some(generation) = std::env::var_os(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV) else { + let Some(generation) = env::var_os(env_vars::OVERLAY_CHILD_GENERATION_ENV) else { return Ok(ActiveGeneration::Inactive); }; let generation = generation @@ -674,7 +678,7 @@ pub(crate) fn active_generation_from_environment() -> Result { Err(error) => return Err(error).context("failed to read active overlay proof"), }; let record: OverlayActiveRecord = super::wire::parse_canonical_json(&bytes, 1024)?; - let current_pid = std::process::id(); + let current_pid = process::id(); let current_start = super::linux::current_process_start_ticks()?; if record.protocol_version != super::wire::DAEMON_CHILD_PROTOCOL_VERSION || record.generation != generation @@ -710,7 +714,7 @@ pub(crate) fn active_generation_from_environment() -> Result { #[cfg(test)] pub(crate) fn enable_current_generation_for_test(daemon_token: &str) -> Result<()> { super::wire::validate_token(daemon_token)?; - let generation = std::env::var(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV)?; + let generation = env::var(env_vars::OVERLAY_CHILD_GENERATION_ENV)?; let bytes = super::linux::read_bounded_regular_file(&active_path(&generation), 1024)?; let active: OverlayActiveRecord = super::wire::parse_canonical_json(&bytes, 1024)?; let enabled = super::wire::canonical_json( @@ -723,28 +727,34 @@ pub(crate) fn enable_current_generation_for_test(daemon_token: &str) -> Result<( }, 1024, )?; - crate::durable_io::write_atomic( + durable_io::write_atomic( &enabled_path(&generation), &enabled, - crate::durable_io::AtomicWriteOptions::private_runtime_file(), + AtomicWriteOptions::private_runtime_file(), )?; Ok(()) } #[cfg(test)] mod tests { + use std::ffi::OsStr; + use std::time::Instant; + + use crate::process_broker::{HelperKind, HelperLifetime}; + use crate::{process_broker, test_env, test_temp}; + use super::*; #[test] fn child_generation_and_pidfd_own_signal_authority() { - let broker = crate::process_broker::start_for_runtime().unwrap(); + let broker = process_broker::start_for_runtime().unwrap(); let child = broker .broker() .spawn( - crate::process_broker::HelperKind::TestSleep, - crate::process_broker::HelperLifetime::OwnedChild, - std::ffi::OsStr::new("sleep"), - [std::ffi::OsStr::new("30")], + HelperKind::TestSleep, + HelperLifetime::OwnedChild, + OsStr::new("sleep"), + [OsStr::new("30")], Vec::new(), ) .unwrap(); @@ -758,10 +768,10 @@ mod tests { owner.mark_committing().unwrap(); owner.mark_ready().unwrap(); owner.signal(libc::SIGTERM).unwrap(); - let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1); + let deadline = Instant::now() + Duration::from_secs(1); while owner.try_wait().unwrap().is_none() { - assert!(std::time::Instant::now() < deadline); - std::thread::sleep(std::time::Duration::from_millis(5)); + assert!(Instant::now() < deadline); + thread::sleep(Duration::from_millis(5)); } assert_eq!(owner.phase(), OverlayChildPhase::Stopped); assert!(!generation.is_empty()); @@ -769,21 +779,21 @@ mod tests { #[test] fn readiness_requires_exact_generation_pid_and_start_identity() { - let _environment = crate::test_env::lock(); - let temp = crate::test_temp::tempdir().unwrap(); - let previous = std::env::var_os(crate::env_vars::XDG_RUNTIME_DIR_ENV); + let _environment = test_env::lock(); + let temp = test_temp::tempdir().unwrap(); + let previous = env::var_os(env_vars::XDG_RUNTIME_DIR_ENV); // SAFETY: serialized by the test environment mutex. - unsafe { std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, temp.path()) }; - let broker = crate::process_broker::start_for_runtime().unwrap(); + unsafe { env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, temp.path()) }; + let broker = process_broker::start_for_runtime().unwrap(); let mut owner = OverlayChildOwner::default(); let generation = owner.reserve().unwrap().to_owned(); let child = broker .broker() .spawn( - crate::process_broker::HelperKind::TestSleep, - crate::process_broker::HelperLifetime::OwnedChild, - std::ffi::OsStr::new("sleep"), - [std::ffi::OsStr::new("30")], + HelperKind::TestSleep, + HelperLifetime::OwnedChild, + OsStr::new("sleep"), + [OsStr::new("30")], Vec::new(), ) .unwrap(); @@ -817,20 +827,20 @@ mod tests { // SAFETY: this test still holds the environment mutex. unsafe { if let Some(previous) = previous { - std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, previous); + env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, previous); } else { - std::env::remove_var(crate::env_vars::XDG_RUNTIME_DIR_ENV); + env::remove_var(env_vars::XDG_RUNTIME_DIR_ENV); } } } #[test] fn recovery_removes_only_proofs_for_dead_child_identities() { - let _environment = crate::test_env::lock(); - let temp = crate::test_temp::tempdir().unwrap(); - let previous = std::env::var_os(crate::env_vars::XDG_RUNTIME_DIR_ENV); + let _environment = test_env::lock(); + let temp = test_temp::tempdir().unwrap(); + let previous = env::var_os(env_vars::XDG_RUNTIME_DIR_ENV); // SAFETY: serialized by the test environment mutex. - unsafe { std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, temp.path()) }; + unsafe { env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, temp.path()) }; let generation = super::super::ProtocolId::generate().unwrap().to_string(); fs::create_dir_all(ready_dir()).unwrap(); let active = OverlayActiveRecord { @@ -848,20 +858,20 @@ mod tests { // SAFETY: this test still holds the environment mutex. unsafe { if let Some(previous) = previous { - std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, previous); + env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, previous); } else { - std::env::remove_var(crate::env_vars::XDG_RUNTIME_DIR_ENV); + env::remove_var(env_vars::XDG_RUNTIME_DIR_ENV); } } } #[test] fn recovery_collects_a_leftover_record_temp_instead_of_failing() { - let _environment = crate::test_env::lock(); - let temp = crate::test_temp::tempdir().unwrap(); - let previous = std::env::var_os(crate::env_vars::XDG_RUNTIME_DIR_ENV); + let _environment = test_env::lock(); + let temp = test_temp::tempdir().unwrap(); + let previous = env::var_os(env_vars::XDG_RUNTIME_DIR_ENV); // SAFETY: serialized by the test environment mutex. - unsafe { std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, temp.path()) }; + unsafe { env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, temp.path()) }; let generation = super::super::ProtocolId::generate().unwrap().to_string(); fs::create_dir_all(ready_dir()).unwrap(); let leftover = ready_dir().join(format!(".{generation}.ready.1.2.3.tmp")); @@ -878,9 +888,9 @@ mod tests { // SAFETY: this test still holds the environment mutex. unsafe { if let Some(previous) = previous { - std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, previous); + env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, previous); } else { - std::env::remove_var(crate::env_vars::XDG_RUNTIME_DIR_ENV); + env::remove_var(env_vars::XDG_RUNTIME_DIR_ENV); } } } diff --git a/src/daemon/protocol_v2/command/claimed.rs b/src/daemon/protocol_v2/command/claimed.rs index 7c0e44edb..1c386649b 100644 --- a/src/daemon/protocol_v2/command/claimed.rs +++ b/src/daemon/protocol_v2/command/claimed.rs @@ -86,7 +86,7 @@ impl ClaimedCommand { latest.action_status = ActionStatus::Abandoned { action_id: prepared.action_id.clone(), action_order: prepared.action_order, - digest: prepared.digest.clone(), + digest: prepared.digest, reason: reason.to_owned(), }; bump_revision(&mut latest)?; @@ -187,9 +187,7 @@ impl ClaimedCommand { let (status, response) = match result { FinalEffect::Completed => ( EffectStatus::Completed, - CommandResponse::Succeeded { - effect_id: effect_id.clone(), - }, + CommandResponse::Succeeded { effect_id }, ), FinalEffect::Indeterminate => { let reason = bounded_reason( @@ -198,10 +196,7 @@ impl ClaimedCommand { ); ( EffectStatus::Indeterminate(reason.clone()), - CommandResponse::CommittedIndeterminate { - effect_id: effect_id.clone(), - reason, - }, + CommandResponse::CommittedIndeterminate { effect_id, reason }, ) } }; diff --git a/src/daemon/protocol_v2/command/client.rs b/src/daemon/protocol_v2/command/client.rs index 4da4b6163..08ed38470 100644 --- a/src/daemon/protocol_v2/command/client.rs +++ b/src/daemon/protocol_v2/command/client.rs @@ -1,5 +1,6 @@ -use std::fs; +use std::path::Path; use std::time::Duration; +use std::{fs, process, thread}; use anyhow::{Context, Result, bail}; @@ -39,10 +40,7 @@ impl ClientCommand { fn publish_with_final_control_write( request: &DaemonRequestV2, daemon_token: &str, - final_control_write: impl FnOnce( - &std::path::Path, - &super::super::wire::CommandControl, - ) -> Result<()>, + final_control_write: impl FnOnce(&Path, &super::super::wire::CommandControl) -> Result<()>, ) -> Result { request.validate()?; validate_token(daemon_token)?; @@ -73,7 +71,7 @@ impl ClientCommand { identity: identity.clone(), target_daemon_token: daemon_token.to_owned(), caller_identity: CallerIdentity { - pid: std::process::id(), + pid: process::id(), process_start_ticks: super::super::linux::current_process_start_ticks()?, nonce: caller_nonce, }, @@ -168,7 +166,7 @@ impl ClientCommand { } if let Some(response) = control.response.clone() { let result = terminal_result(&response); - acknowledge(&mut control, response)?; + acknowledge(&mut control, &response)?; write_control(&self.control_path, &control)?; unlock(&self.decision_lock)?; unlock(&self.caller_lease)?; @@ -192,7 +190,7 @@ impl ClientCommand { )); } unlock(&self.decision_lock)?; - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } } @@ -216,7 +214,7 @@ impl ClientCommand { .clone() .ok_or_else(|| anyhow!("cancellation lost to a nonterminal committed effect"))?; let result = terminal_result(&response); - acknowledge(&mut control, response)?; + acknowledge(&mut control, &response)?; write_control(&self.control_path, &control)?; unlock(&self.decision_lock)?; unlock(&self.caller_lease)?; @@ -239,13 +237,13 @@ fn cancel_open(control: &mut CommandControl) -> Result { Ok(true) } -fn acknowledge(control: &mut CommandControl, response: CommandResponse) -> Result<()> { +fn acknowledge(control: &mut CommandControl, response: &CommandResponse) -> Result<()> { if !matches!(control.caller_disposition, CallerDisposition::Waiting) { bail!("command response was already acknowledged"); } bump_revision(control)?; control.caller_disposition = CallerDisposition::Acknowledged { - response_digest: response_digest(&response)?, + response_digest: response_digest(response)?, }; Ok(()) } diff --git a/src/daemon/protocol_v2/command/layout.rs b/src/daemon/protocol_v2/command/layout.rs index 13d84f6cc..9f95fca0d 100644 --- a/src/daemon/protocol_v2/command/layout.rs +++ b/src/daemon/protocol_v2/command/layout.rs @@ -4,9 +4,14 @@ use std::io::{self, ErrorKind}; use std::os::fd::AsRawFd; use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; use std::path::{Path, PathBuf}; +use std::thread; use std::time::Duration; use anyhow::{Context, Result, anyhow, bail}; +use serde::de::DeserializeOwned; + +use crate::durable_io::AtomicWriteOptions; +use crate::{durable_io, paths}; pub(super) use super::super::linux::create_private_directory; use super::super::wire::{ @@ -20,7 +25,7 @@ use super::{ }; pub(crate) fn command_root() -> PathBuf { - crate::paths::daemon_command_dir().join("v2") + paths::daemon_command_dir().join("v2") } pub(super) fn creating_dir(root: &Path) -> PathBuf { @@ -117,7 +122,7 @@ fn check_root_shape(root: &Path, collect_temps: bool) -> Result<()> { if allowed.contains(name.as_str()) { continue; } - if !crate::durable_io::is_temp_file_for(&name, "admission.json") { + if !durable_io::is_temp_file_for(&name, "admission.json") { bail!("unexpected v2 command-root entry {name}"); } @@ -311,7 +316,7 @@ pub(super) fn try_lock_until( if BootClock::now()? >= deadline { return Ok(false); } - std::thread::sleep(LOCK_RETRY); + thread::sleep(LOCK_RETRY); } Err(error) if error.kind() == ErrorKind::Interrupted => continue, Err(error) => return Err(error).context("failed to acquire protocol lock"), @@ -345,15 +350,11 @@ pub(super) fn try_admission_lock(root: &Path, deadline: BootDeadline) -> Result< pub(super) fn write_record(path: &Path, value: &T, cap: usize) -> Result<()> { let bytes = canonical_json(value, cap)?; - crate::durable_io::write_atomic( - path, - &bytes, - crate::durable_io::AtomicWriteOptions::private_runtime_file(), - ) - .with_context(|| format!("failed to publish protocol record {}", path.display())) + durable_io::write_atomic(path, &bytes, AtomicWriteOptions::private_runtime_file()) + .with_context(|| format!("failed to publish protocol record {}", path.display())) } -pub(super) fn read_record( +pub(super) fn read_record( path: &Path, cap: usize, ) -> Result { diff --git a/src/daemon/protocol_v2/command/owner.rs b/src/daemon/protocol_v2/command/owner.rs index d1797cb1e..9542492e1 100644 --- a/src/daemon/protocol_v2/command/owner.rs +++ b/src/daemon/protocol_v2/command/owner.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::io::{self, ErrorKind}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::time::Duration; use anyhow::{Context, Result, bail}; @@ -179,7 +179,7 @@ impl CommandOwner { }; if use_claimed { let (_order, identity, path) = claimed.expect("selected claimed command exists"); - return self.claim_ref_less_control(identity, path); + return self.claim_ref_less_control(&identity, path); } let Some((_order, (identity, reference_path, reference))) = reference else { return Ok(None); @@ -347,7 +347,7 @@ impl CommandOwner { fn recover_abandoned_unqueued( &self, - path: &std::path::Path, + path: &Path, observed: &super::super::wire::CommandControl, ) -> Result<()> { let caller_lease = open_lock(&path.join("caller.lease"), false)?; @@ -389,7 +389,7 @@ impl CommandOwner { fn claim_ref_less_control( &self, - identity: String, + identity: &str, path: PathBuf, ) -> Result> { if !matches!( diff --git a/src/daemon/protocol_v2/command/tests.rs b/src/daemon/protocol_v2/command/tests.rs index 0e778454d..fdf5d2646 100644 --- a/src/daemon/protocol_v2/command/tests.rs +++ b/src/daemon/protocol_v2/command/tests.rs @@ -1,6 +1,8 @@ use std::env; use std::time::Duration; +use crate::{test_env, test_temp}; + use super::super::BootClock; use super::super::action::ActionJournal; use super::super::wire::*; @@ -13,8 +15,8 @@ use crate::env_vars::XDG_RUNTIME_DIR_ENV; use crate::tray_action::TrayAction; fn with_runtime(run: impl FnOnce() -> T) -> T { - let _guard = crate::test_env::lock(); - let temp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let temp = test_temp::tempdir().unwrap(); let previous = env::var_os(XDG_RUNTIME_DIR_ENV); // SAFETY: serialized by the test environment mutex. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, temp.path()) }; @@ -717,14 +719,14 @@ fn lock_deadline_is_reported_without_parsing_error_text() { let deadline = BootClock::now() .unwrap() - .checked_add(std::time::Duration::from_millis(5)) + .checked_add(Duration::from_millis(5)) .unwrap(); assert!(!try_lock_until(&contender, libc::LOCK_EX, deadline).unwrap()); unlock(&held).unwrap(); let deadline = BootClock::now() .unwrap() - .checked_add(std::time::Duration::from_millis(5)) + .checked_add(Duration::from_millis(5)) .unwrap(); assert!(try_lock_until(&contender, libc::LOCK_EX, deadline).unwrap()); unlock(&contender).unwrap(); diff --git a/src/daemon/protocol_v2/digest.rs b/src/daemon/protocol_v2/digest.rs index 0fc304958..a78f59fd9 100644 --- a/src/daemon/protocol_v2/digest.rs +++ b/src/daemon/protocol_v2/digest.rs @@ -41,7 +41,7 @@ mod tests { .0 .iter() .map(|pair| { - let pair = std::str::from_utf8(pair).unwrap(); + let pair = str::from_utf8(pair).unwrap(); u8::from_str_radix(pair, 16).unwrap() }) .collect() diff --git a/src/daemon/protocol_v2/linux.rs b/src/daemon/protocol_v2/linux.rs index 26a1680f3..0cf68e51c 100644 --- a/src/daemon/protocol_v2/linux.rs +++ b/src/daemon/protocol_v2/linux.rs @@ -1,10 +1,11 @@ -use std::fmt; -use std::fs::{File, OpenOptions}; +use std::ffi::CString; +use std::fs::{File, Metadata, OpenOptions, Permissions}; use std::io::{self, Read}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd}; use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; +use std::{fmt, fs, process, ptr}; const BOOT_ID_PATH: &str = "/proc/sys/kernel/random/boot_id"; const TIME_NAMESPACE_PATH: &str = "/proc/self/ns/time"; @@ -54,7 +55,7 @@ pub(crate) struct CommandWatchDrain { pub(crate) struct CommandQueueWatcher { fd: OwnedFd, watch_descriptor: i32, - queue_path: std::path::PathBuf, + queue_path: PathBuf, queue_identity: FileIdentity, _queue_fd: File, buffer: Box<[u8; COMMAND_WATCH_BUFFER_BYTES]>, @@ -196,7 +197,7 @@ pub(crate) fn read_bounded_private_file(path: &Path, cap: usize) -> io::Result bool { +pub(crate) fn is_private(metadata: &Metadata) -> bool { // SAFETY: geteuid has no preconditions and cannot fail. metadata.uid() == unsafe { libc::geteuid() } && metadata.mode() & 0o077 == 0 } @@ -207,25 +208,25 @@ pub(crate) fn is_private(metadata: &std::fs::Metadata) -> bool { pub(crate) fn create_private_directory(path: &Path) -> anyhow::Result<()> { use std::os::unix::fs::PermissionsExt; - match std::fs::create_dir(path) { + match fs::create_dir(path) { Ok(()) => {} Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {} Err(error) => return Err(error.into()), } - let metadata = std::fs::symlink_metadata(path)?; + let metadata = fs::symlink_metadata(path)?; // SAFETY: geteuid has no preconditions and cannot fail. if !metadata.is_dir() || metadata.uid() != unsafe { libc::geteuid() } { anyhow::bail!("{} is not a no-follow private directory", path.display()); } - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?; + fs::set_permissions(path, Permissions::from_mode(0o700))?; Ok(()) } fn read_bounded_file( path: &Path, cap: usize, - trusted: impl Fn(&std::fs::Metadata) -> bool, + trusted: impl Fn(&Metadata) -> bool, ) -> io::Result> { let mut options = OpenOptions::new(); options @@ -279,10 +280,10 @@ pub(crate) const QUARANTINE_RETAINED_ENTRIES: usize = 64; /// another way to wedge the daemon — and a concurrently removed entry is not /// an error. pub(crate) fn gc_quarantine_tail(dir: &Path, retain: usize) -> io::Result { - let mut entries: Vec<(i64, i64, std::path::PathBuf)> = Vec::new(); - for entry in std::fs::read_dir(dir)? { + let mut entries: Vec<(i64, i64, PathBuf)> = Vec::new(); + for entry in fs::read_dir(dir)? { let path = entry?.path(); - match std::fs::symlink_metadata(&path) { + match fs::symlink_metadata(&path) { Ok(metadata) => entries.push((metadata.ctime(), metadata.ctime_nsec(), path)), Err(error) if error.kind() == io::ErrorKind::NotFound => {} Err(error) => return Err(error), @@ -295,13 +296,13 @@ pub(crate) fn gc_quarantine_tail(dir: &Path, retain: usize) -> io::Result let excess = entries.len() - retain; let mut removed = 0; for (_, _, path) in entries.into_iter().take(excess) { - let is_dir = std::fs::symlink_metadata(&path) + let is_dir = fs::symlink_metadata(&path) .map(|metadata| metadata.is_dir()) .unwrap_or(false); let result = if is_dir { - std::fs::remove_dir_all(&path) + fs::remove_dir_all(&path) } else { - std::fs::remove_file(&path) + fs::remove_file(&path) }; match result { Ok(()) => { @@ -359,12 +360,12 @@ pub(crate) fn revalidate_path_identity(path: &Path, expected: FileIdentity) -> i } pub(crate) fn current_process_start_ticks() -> io::Result { - process_start_ticks(std::process::id()) + process_start_ticks(process::id()) } pub(crate) fn process_start_ticks(pid: u32) -> io::Result { let path = format!("/proc/{pid}/stat"); - let contents = std::fs::read_to_string(path)?; + let contents = fs::read_to_string(path)?; let close = contents.rfind(')').ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, @@ -447,7 +448,7 @@ pub(crate) fn validate_pidfd(fd: BorrowedFd<'_>) -> io::Result<()> { libc::SYS_pidfd_send_signal, fd.as_raw_fd(), 0, - std::ptr::null::(), + ptr::null::(), 0, ) } == 0 @@ -508,7 +509,7 @@ impl BootDeadline { } impl BootDeadlineSource { - pub(crate) fn new() -> io::Result { + pub(crate) fn try_new() -> io::Result { // SAFETY: timerfd_create returns a new owned descriptor on success. let raw = unsafe { libc::timerfd_create(libc::CLOCK_BOOTTIME, libc::TFD_NONBLOCK | libc::TFD_CLOEXEC) @@ -516,8 +517,8 @@ impl BootDeadlineSource { if raw < 0 { return Err(io::Error::last_os_error()); } - // SAFETY: raw is a newly created descriptor not wrapped elsewhere. Ok(Self { + // SAFETY: raw is a newly created descriptor not wrapped elsewhere. fd: unsafe { OwnedFd::from_raw_fd(raw) }, }) } @@ -549,7 +550,7 @@ impl BootDeadlineSource { self.fd.as_raw_fd(), libc::TFD_TIMER_ABSTIME, &value, - std::ptr::null_mut(), + ptr::null_mut(), ) } != 0 { @@ -570,9 +571,7 @@ impl BootDeadlineSource { }, }; // SAFETY: value is initialized and fd owns a timerfd. - if unsafe { libc::timerfd_settime(self.fd.as_raw_fd(), 0, &value, std::ptr::null_mut()) } - != 0 - { + if unsafe { libc::timerfd_settime(self.fd.as_raw_fd(), 0, &value, ptr::null_mut()) } != 0 { return Err(io::Error::last_os_error()); } Ok(()) @@ -609,7 +608,7 @@ impl BootDeadlineSource { } impl CommandQueueWatcher { - pub(crate) fn new(queue_path: &Path) -> io::Result { + pub(crate) fn try_new(queue_path: &Path) -> io::Result { let (queue_fd, queue_identity) = open_nofollow_directory(queue_path)?; // SAFETY: inotify_init1 returns a newly owned descriptor on success. let raw = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) }; @@ -618,7 +617,7 @@ impl CommandQueueWatcher { } // SAFETY: raw is a new descriptor not wrapped elsewhere. let fd = unsafe { OwnedFd::from_raw_fd(raw) }; - let path = std::ffi::CString::new(queue_path.as_os_str().as_encoded_bytes()) + let path = CString::new(queue_path.as_os_str().as_encoded_bytes()) .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "queue path contains NUL"))?; let mask = libc::IN_MOVED_TO | libc::IN_CREATE @@ -705,9 +704,8 @@ impl CommandQueueWatcher { } // SAFETY: length was checked and read_unaligned copies the fixed // header without retaining the pointer. - let event = unsafe { - std::ptr::read_unaligned(remaining.as_ptr().cast::()) - }; + let event = + unsafe { ptr::read_unaligned(remaining.as_ptr().cast::()) }; let event_length = size_of::() .checked_add(event.len as usize) .ok_or_else(|| { @@ -751,7 +749,7 @@ impl CommandQueueWatcher { #[cfg(test)] pub(crate) fn open_self_pidfd() -> io::Result { - open_pidfd(std::process::id()) + open_pidfd(process::id()) } /// Immediately terminate the process without Rust or libc cleanup. diff --git a/src/daemon/protocol_v2/mod.rs b/src/daemon/protocol_v2/mod.rs index ee2d323d6..f4fbf3f57 100644 --- a/src/daemon/protocol_v2/mod.rs +++ b/src/daemon/protocol_v2/mod.rs @@ -8,13 +8,16 @@ mod runtime; mod session_target; mod wire; -pub(crate) use child::OverlayChildOwner; +use std::fs; +use std::io::ErrorKind; + #[cfg(test)] pub(crate) use child::{ActiveGeneration, active_generation_from_environment}; pub(crate) use child::{ - open_daemon_watchdog, recover_stale_child_records, start_daemon_watchdog_from_environment, + OverlayChildOwner, open_daemon_watchdog, publish_ready_from_environment, + publish_signal_ready_from_environment, recover_stale_child_records, + start_daemon_watchdog_from_environment, }; -pub(crate) use child::{publish_ready_from_environment, publish_signal_ready_from_environment}; pub(crate) use command::{ ClientCommand, CommandOwner, FinalEffect, TerminalCommandResult, command_root, }; @@ -28,23 +31,24 @@ pub(crate) use runtime::{ClassifiedRuntimeRecord, read_runtime_record, write_run pub(crate) use session_target::{ ReportedSession, clear_stale_session_reports, publish_session_from_environment, }; -pub(crate) use wire::EffectKind; -pub(crate) use wire::{DaemonRequestV2, DaemonRuntimeRecordV2}; +pub(crate) use wire::{DaemonRequestV2, DaemonRuntimeRecordV2, EffectKind}; #[cfg(test)] mod tests; pub(crate) use action::{ActionClaimOutcome, ActionFinishOutcome, ActionJournal, ClaimedAction}; +use crate::paths; + pub(crate) fn try_claim_overlay_action() -> anyhow::Result { let enabled_daemon_token = match child::active_generation_from_environment()? { child::ActiveGeneration::Inactive => return Ok(ActionClaimOutcome::Idle), child::ActiveGeneration::Pending => return Ok(ActionClaimOutcome::Deferred), child::ActiveGeneration::Enabled { daemon_token } => daemon_token, }; - let runtime_path = crate::paths::daemon_pid_file(); - match std::fs::symlink_metadata(&runtime_path) { + let runtime_path = paths::daemon_pid_file(); + match fs::symlink_metadata(&runtime_path) { Ok(_) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + Err(error) if error.kind() == ErrorKind::NotFound => { return Ok(ActionClaimOutcome::Idle); } Err(error) => return Err(error.into()), @@ -64,10 +68,10 @@ pub(crate) fn try_claim_overlay_action() -> anyhow::Result { pub(crate) fn prepare_rollback_compatibility() -> anyhow::Result<()> { let root = command_root(); - match std::fs::symlink_metadata(&root) { + match fs::symlink_metadata(&root) { Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {} Ok(_) => anyhow::bail!("v2 command root is not a no-follow directory"), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(()), Err(error) => return Err(error.into()), } let token = ProtocolToken::generate()?.to_string(); diff --git a/src/daemon/protocol_v2/runtime.rs b/src/daemon/protocol_v2/runtime.rs index ee0314164..871deee4b 100644 --- a/src/daemon/protocol_v2/runtime.rs +++ b/src/daemon/protocol_v2/runtime.rs @@ -4,6 +4,9 @@ use std::path::Path; use anyhow::{Context, Result, bail}; use serde::Deserialize; +use crate::durable_io; +use crate::durable_io::AtomicWriteOptions; + use super::wire::{ DaemonRuntimeRecordV2, MAX_RUNTIME_RECORD_BYTES, canonical_json, parse_canonical_json, }; @@ -34,7 +37,7 @@ pub(crate) fn read_runtime_record(path: &Path) -> Result {} } - let text = std::str::from_utf8(&bytes).context("daemon runtime record is not UTF-8")?; + let text = str::from_utf8(&bytes).context("daemon runtime record is not UTF-8")?; if let Ok(record) = serde_json::from_str::(text) { if record.pid == 0 { bail!("legacy daemon runtime pid is zero"); @@ -78,27 +81,26 @@ pub(crate) fn write_runtime_record_v2(path: &Path, record: &DaemonRuntimeRecordV fs::create_dir_all(parent) .with_context(|| format!("failed to create {}", parent.display()))?; } - crate::durable_io::write_atomic( - path, - &bytes, - crate::durable_io::AtomicWriteOptions::private_runtime_file(), + durable_io::write_atomic(path, &bytes, AtomicWriteOptions::private_runtime_file()).with_context( + || { + format!( + "failed to publish daemon v2 runtime record {}", + path.display() + ) + }, ) - .with_context(|| { - format!( - "failed to publish daemon v2 runtime record {}", - path.display() - ) - }) } #[cfg(test)] mod tests { + use crate::test_temp; + use super::*; use crate::daemon::protocol_v2::ProtocolToken; #[test] fn exact_v2_is_strict_but_v1_remains_permissive() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime"); let v2 = DaemonRuntimeRecordV2::current(ProtocolToken::generate().unwrap()).unwrap(); write_runtime_record_v2(&path, &v2).unwrap(); @@ -119,7 +121,7 @@ mod tests { #[test] fn malformed_v2_shaped_records_never_downgrade_to_legacy() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime"); for record in [ r#" {"pid":42,"runtime_record_version":2}"#, diff --git a/src/daemon/protocol_v2/session_target.rs b/src/daemon/protocol_v2/session_target.rs index 55b6adcc2..53c6b663d 100644 --- a/src/daemon/protocol_v2/session_target.rs +++ b/src/daemon/protocol_v2/session_target.rs @@ -7,12 +7,17 @@ //! know, and an older daemon reads only the plain files directly in //! `daemon-commands/`. +use std::io::ErrorKind; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; +use std::{env, fs, process}; use anyhow::{Context, Result, anyhow, bail}; use serde::{Deserialize, Serialize}; +use crate::durable_io::AtomicWriteOptions; +use crate::{durable_io, env_vars, paths}; + use super::child::{ActiveGeneration, active_generation_from_environment}; const REPORT_SCHEMA: u16 = 1; @@ -41,7 +46,7 @@ struct SessionTargetRecord { } fn report_dir() -> PathBuf { - crate::paths::daemon_command_dir().join("overlay-targets") + paths::daemon_command_dir().join("overlay-targets") } fn report_name(generation: &str) -> String { @@ -56,12 +61,10 @@ fn report_path(generation: &str) -> PathBuf { /// earlier report. Returns whether a report was written: a standalone overlay, /// or one an older daemon launched, has no daemon that reads reports. pub(crate) fn publish_session_from_environment(session: &ReportedSession) -> Result { - if std::env::var_os(crate::env_vars::OVERLAY_SESSION_REPORTS_ENV) - .is_none_or(|value| value != "1") - { + if env::var_os(env_vars::OVERLAY_SESSION_REPORTS_ENV).is_none_or(|value| value != "1") { return Ok(false); } - let generation = std::env::var(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV) + let generation = env::var(env_vars::OVERLAY_CHILD_GENERATION_ENV) .context("a daemon overlay reports its session under its generation")?; super::wire::validate_id(&generation)?; // Only the process that published this generation's identity reports for it. @@ -79,16 +82,16 @@ pub(crate) fn publish_session_from_environment(session: &ReportedSession) -> Res let record = SessionTargetRecord { schema: REPORT_SCHEMA, generation, - pid: std::process::id(), + pid: process::id(), process_start_ticks: super::linux::current_process_start_ticks()?, target, }; let bytes = super::wire::canonical_json(&record, MAX_REPORT_BYTES)?; super::linux::create_private_directory(&report_dir())?; - crate::durable_io::write_atomic( + durable_io::write_atomic( &report_path(&record.generation), &bytes, - crate::durable_io::AtomicWriteOptions::private_runtime_file(), + AtomicWriteOptions::private_runtime_file(), )?; Ok(true) @@ -99,7 +102,7 @@ fn report_path_text(path: &Path) -> Result { let path = if path.is_absolute() { path.to_path_buf() } else { - std::env::current_dir() + env::current_dir() .context("failed to resolve a relative session path")? .join(path) }; @@ -116,12 +119,12 @@ fn report_path_text(path: &Path) -> Result { /// a symlink to another directory. fn private_report_dir() -> Result> { let directory = report_dir(); - match std::fs::symlink_metadata(&directory) { + match fs::symlink_metadata(&directory) { Ok(metadata) if metadata.is_dir() && super::linux::is_private(&metadata) => { Ok(Some(directory)) } Ok(_) => bail!("{} is not a private report directory", directory.display()), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(error) if error.kind() == ErrorKind::NotFound => Ok(None), Err(error) => { Err(error).with_context(|| format!("failed to inspect {}", directory.display())) } @@ -143,7 +146,7 @@ fn read_session_report( let path = directory.join(report_name(generation)); let bytes = match super::linux::read_bounded_private_file(&path, MAX_REPORT_BYTES) { Ok(bytes) => bytes, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None), Err(error) => return Err(error).context("failed to read the session report"), }; let record: SessionTargetRecord = super::wire::parse_canonical_json(&bytes, MAX_REPORT_BYTES)?; @@ -228,14 +231,14 @@ fn remove_reports(report: Option<&str>, is_removed: impl Fn(&str) -> bool) -> Re remove_entry(&directory.join(report))?; } - let entries = std::fs::read_dir(&directory) + let entries = fs::read_dir(&directory) .with_context(|| format!("failed to list {}", directory.display()))?; for entry in entries.take(MAX_CLEANUP_ENTRIES) { let entry = entry?; let Ok(name) = entry.file_name().into_string() else { continue; }; - let report = crate::durable_io::temp_file_target(&name).unwrap_or(&name); + let report = durable_io::temp_file_target(&name).unwrap_or(&name); if is_removed(report) { remove_entry(&entry.path())?; } @@ -244,9 +247,9 @@ fn remove_reports(report: Option<&str>, is_removed: impl Fn(&str) -> bool) -> Re } fn remove_entry(path: &Path) -> Result<()> { - match std::fs::remove_file(path) { + match fs::remove_file(path) { Ok(()) => Ok(()), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) if error.kind() == ErrorKind::NotFound => Ok(()), Err(error) => Err(error).with_context(|| format!("failed to remove {}", path.display())), } } diff --git a/src/daemon/protocol_v2/session_target/tests.rs b/src/daemon/protocol_v2/session_target/tests.rs index bc7740edd..d677e5a42 100644 --- a/src/daemon/protocol_v2/session_target/tests.rs +++ b/src/daemon/protocol_v2/session_target/tests.rs @@ -1,4 +1,8 @@ use std::ffi::OsStr; +use std::fs::Permissions; +use std::os::unix; + +use crate::{test_env, test_temp}; use super::*; use crate::env_vars::{ @@ -8,10 +12,10 @@ use crate::env_vars::{ /// Runs `body` as the overlay child `generation` of a daemon that reads /// session reports when `reports` is set, in a private runtime directory. fn as_daemon_overlay(reports: Option<&str>, body: impl FnOnce(&str)) { - let runtime = crate::test_temp::tempdir().unwrap(); + let runtime = test_temp::tempdir().unwrap(); let generation = super::super::ProtocolId::generate().unwrap().to_string(); - crate::test_env::with_env_vars( + test_env::with_env_vars( &[ (XDG_RUNTIME_DIR_ENV, Some(runtime.path().as_os_str())), (OVERLAY_CHILD_GENERATION_ENV, Some(OsStr::new(&generation))), @@ -22,7 +26,7 @@ fn as_daemon_overlay(reports: Option<&str>, body: impl FnOnce(&str)) { } fn read_record(generation: &str) -> SessionTargetRecord { - let bytes = std::fs::read(report_path(generation)).unwrap(); + let bytes = fs::read(report_path(generation)).unwrap(); super::super::wire::parse_canonical_json(&bytes, MAX_REPORT_BYTES).unwrap() } @@ -37,12 +41,12 @@ fn a_daemon_overlay_reports_its_session_under_its_own_identity() { let record = read_record(generation); assert_eq!(record.schema, REPORT_SCHEMA); assert_eq!(record.generation, generation); - assert_eq!(record.pid, std::process::id()); + assert_eq!(record.pid, process::id()); assert_eq!( record.process_start_ticks, super::super::linux::current_process_start_ticks().unwrap() ); - let expected = std::env::current_dir() + let expected = env::current_dir() .unwrap() .join("lectures/b.wayscriber-session"); assert_eq!(record.target.as_deref(), expected.to_str()); @@ -75,7 +79,7 @@ fn an_overlay_without_its_child_identity_cannot_report() { fn current_identity() -> (u32, u64) { ( - std::process::id(), + process::id(), super::super::linux::current_process_start_ticks().unwrap(), ) } @@ -86,11 +90,11 @@ fn write_entry(name: &str, bytes: &[u8], mode: u32) -> PathBuf { use std::os::unix::fs::PermissionsExt; // An overlay's identity proof creates this before any report is written. - std::fs::create_dir_all(crate::paths::daemon_command_dir()).unwrap(); + fs::create_dir_all(paths::daemon_command_dir()).unwrap(); super::super::linux::create_private_directory(&report_dir()).unwrap(); let path = report_dir().join(name); - std::fs::write(&path, bytes).unwrap(); - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)).unwrap(); + fs::write(&path, bytes).unwrap(); + fs::set_permissions(&path, Permissions::from_mode(mode)).unwrap(); path } @@ -162,9 +166,9 @@ fn an_untrusted_report_is_ignored_and_still_removed() { } let target = write_entry("elsewhere", &valid, 0o600); - std::os::unix::fs::symlink(&target, report_path(generation)).unwrap(); + unix::fs::symlink(&target, report_path(generation)).unwrap(); assert_eq!(take_final_session_report(generation, pid, ticks), None); - assert!(std::fs::symlink_metadata(report_path(generation)).is_err()); + assert!(fs::symlink_metadata(report_path(generation)).is_err()); assert!(target.exists(), "only the report's own name is removed"); }); } @@ -222,11 +226,11 @@ fn startup_removes_every_stale_report_and_nothing_else() { fn reports_are_never_read_or_removed_through_a_symlinked_directory() { as_daemon_overlay(None, |generation| { let (pid, ticks) = current_identity(); - let elsewhere = crate::test_temp::tempdir().unwrap(); + let elsewhere = test_temp::tempdir().unwrap(); let report = elsewhere.path().join(format!("{generation}.target")); - std::fs::write(&report, record_bytes(generation, pid, ticks, None)).unwrap(); - std::fs::create_dir_all(crate::paths::daemon_command_dir()).unwrap(); - std::os::unix::fs::symlink(elsewhere.path(), report_dir()).unwrap(); + fs::write(&report, record_bytes(generation, pid, ticks, None)).unwrap(); + fs::create_dir_all(paths::daemon_command_dir()).unwrap(); + unix::fs::symlink(elsewhere.path(), report_dir()).unwrap(); assert!(read_session_report(generation, pid, ticks).is_err()); assert!(clear_stale_session_reports().is_err()); diff --git a/src/daemon/protocol_v2/tests.rs b/src/daemon/protocol_v2/tests.rs index 727455975..06f237c30 100644 --- a/src/daemon/protocol_v2/tests.rs +++ b/src/daemon/protocol_v2/tests.rs @@ -1,5 +1,10 @@ use std::os::fd::AsRawFd; -use std::time::Duration; +use std::process::Command; +use std::time::{Duration, Instant}; +use std::{env, process}; + +use crate::tray_action::TrayAction; +use crate::{env_vars, test_env, test_temp}; use super::*; @@ -57,7 +62,7 @@ fn kernel_boot_and_namespace_identities_are_stable() { #[test] fn boottime_deadline_source_arms_absolutely_and_drains_once() { - let source = linux::BootDeadlineSource::new().unwrap(); + let source = linux::BootDeadlineSource::try_new().unwrap(); assert!(!source.drain().unwrap()); let deadline = BootClock::now() @@ -89,7 +94,7 @@ fn pidfd_wait_times_out_while_alive_and_returns_once_exited() { use std::os::fd::AsFd; use std::time::{Duration, Instant}; - let mut child = std::process::Command::new("/bin/sh") + let mut child = Command::new("/bin/sh") .args(["-c", "sleep 30"]) .spawn() .expect("spawn a child to wait on"); @@ -137,9 +142,9 @@ fn self_pidfd_is_not_readable_while_process_is_alive() { fn bounded_nofollow_reads_reject_symlinks_and_oversize_records() { use std::os::unix::fs::symlink; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let regular = temp.path().join("record"); - std::fs::write(®ular, b"bounded").unwrap(); + fs::write(®ular, b"bounded").unwrap(); assert_eq!( linux::read_bounded_regular_file(®ular, 7).unwrap(), b"bounded" @@ -153,32 +158,32 @@ fn bounded_nofollow_reads_reject_symlinks_and_oversize_records() { #[test] fn directory_identity_detects_path_replacement() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let first = temp.path().join("first"); let replacement = temp.path().join("replacement"); - std::fs::create_dir(&first).unwrap(); - std::fs::create_dir(&replacement).unwrap(); + fs::create_dir(&first).unwrap(); + fs::create_dir(&replacement).unwrap(); let (_file, identity) = linux::open_nofollow_directory(&first).unwrap(); linux::revalidate_path_identity(&first, identity).unwrap(); - std::fs::rename(&first, temp.path().join("old")).unwrap(); - std::fs::rename(&replacement, &first).unwrap(); + fs::rename(&first, temp.path().join("old")).unwrap(); + fs::rename(&replacement, &first).unwrap(); assert!(linux::revalidate_path_identity(&first, identity).is_err()); } #[test] fn process_start_identity_is_nonzero_and_pidfd_matches_live_process() { assert!(linux::current_process_start_ticks().unwrap() > 0); - let _pidfd = linux::open_pidfd(std::process::id()).unwrap(); + let _pidfd = linux::open_pidfd(process::id()).unwrap(); } #[test] fn command_queue_watch_reports_atomic_publication_and_rejects_replacement() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let queue = temp.path().join("queue"); - std::fs::create_dir(&queue).unwrap(); - let mut watcher = CommandQueueWatcher::new(&queue).unwrap(); - std::fs::write(temp.path().join("temporary"), b"request").unwrap(); - std::fs::rename( + fs::create_dir(&queue).unwrap(); + let mut watcher = CommandQueueWatcher::try_new(&queue).unwrap(); + fs::write(temp.path().join("temporary"), b"request").unwrap(); + fs::rename( temp.path().join("temporary"), queue.join("0000000000000001-00000000000000000000000000000000.request"), ) @@ -193,8 +198,8 @@ fn command_queue_watch_reports_atomic_publication_and_rejects_replacement() { assert_eq!(unsafe { libc::poll(&mut poll_fd, 1, 500) }, 1); assert!(watcher.drain().unwrap().scan_pending); - std::fs::rename(&queue, temp.path().join("old-queue")).unwrap(); - std::fs::create_dir(&queue).unwrap(); + fs::rename(&queue, temp.path().join("old-queue")).unwrap(); + fs::create_dir(&queue).unwrap(); assert!(watcher.revalidate().is_err()); } @@ -205,24 +210,24 @@ fn fail_stop_has_a_non_returning_signature() { #[test] fn only_an_identified_internal_overlay_can_claim_daemon_actions() { - let _environment = crate::test_env::lock(); - let temp = crate::test_temp::tempdir().unwrap(); - let previous_runtime = std::env::var_os(crate::env_vars::XDG_RUNTIME_DIR_ENV); - let previous_generation = std::env::var_os(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV); + let _environment = test_env::lock(); + let temp = test_temp::tempdir().unwrap(); + let previous_runtime = env::var_os(env_vars::XDG_RUNTIME_DIR_ENV); + let previous_generation = env::var_os(env_vars::OVERLAY_CHILD_GENERATION_ENV); // SAFETY: serialized by the test environment mutex. unsafe { - std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, temp.path()); - std::env::remove_var(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV); + env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, temp.path()); + env::remove_var(env_vars::OVERLAY_CHILD_GENERATION_ENV); } let token = ProtocolToken::generate().unwrap(); let token_text = token.to_string(); let _owner = CommandOwner::open(&token_text).unwrap(); let journal = ActionJournal::open().unwrap(); journal - .publish_anonymous(&token_text, crate::tray_action::TrayAction::ToggleFreeze) + .publish_anonymous(&token_text, TrayAction::ToggleFreeze) .unwrap(); let runtime = DaemonRuntimeRecordV2::current(token).unwrap(); - write_runtime_record_v2(&crate::paths::daemon_pid_file(), &runtime).unwrap(); + write_runtime_record_v2(&paths::daemon_pid_file(), &runtime).unwrap(); assert!(matches!( try_claim_overlay_action().unwrap(), @@ -232,16 +237,16 @@ fn only_an_identified_internal_overlay_can_claim_daemon_actions() { let generation = ProtocolId::generate().unwrap().to_string(); // SAFETY: serialized by the test environment mutex. unsafe { - std::env::set_var(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV, &generation); + env::set_var(env_vars::OVERLAY_CHILD_GENERATION_ENV, &generation); } publish_ready_from_environment().unwrap(); - let started = std::time::Instant::now(); + let started = Instant::now(); assert!(matches!( try_claim_overlay_action().unwrap(), ActionClaimOutcome::Deferred )); assert!( - started.elapsed() < std::time::Duration::from_millis(100), + started.elapsed() < Duration::from_millis(100), "missing enable proof blocked instead of deferring" ); child::enable_current_generation_for_test(&token_text).unwrap(); @@ -253,12 +258,12 @@ fn only_an_identified_internal_overlay_can_claim_daemon_actions() { // SAFETY: this test still holds the environment mutex. unsafe { match previous_runtime { - Some(value) => std::env::set_var(crate::env_vars::XDG_RUNTIME_DIR_ENV, value), - None => std::env::remove_var(crate::env_vars::XDG_RUNTIME_DIR_ENV), + Some(value) => env::set_var(env_vars::XDG_RUNTIME_DIR_ENV, value), + None => env::remove_var(env_vars::XDG_RUNTIME_DIR_ENV), } match previous_generation { - Some(value) => std::env::set_var(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV, value), - None => std::env::remove_var(crate::env_vars::OVERLAY_CHILD_GENERATION_ENV), + Some(value) => env::set_var(env_vars::OVERLAY_CHILD_GENERATION_ENV, value), + None => env::remove_var(env_vars::OVERLAY_CHILD_GENERATION_ENV), } } } diff --git a/src/daemon/protocol_v2/wire.rs b/src/daemon/protocol_v2/wire.rs index d8461e0e4..b28eab43b 100644 --- a/src/daemon/protocol_v2/wire.rs +++ b/src/daemon/protocol_v2/wire.rs @@ -1,8 +1,10 @@ use std::os::fd::AsFd; use std::path::PathBuf; +use std::process; use anyhow::{Context, Result, anyhow, bail}; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; use super::digest::sha256_hex; use super::{BootIdentity, NamespaceIdentity, ProtocolId, ProtocolToken}; @@ -52,7 +54,7 @@ impl From for NamespaceIdentityV2 { impl DaemonRuntimeRecordV2 { pub(crate) fn current(token: ProtocolToken) -> Result { Ok(Self { - pid: std::process::id(), + pid: process::id(), runtime_record_version: DAEMON_COMMAND_PROTOCOL_VERSION, typed_control_protocol_version: DAEMON_COMMAND_PROTOCOL_VERSION, boot_id: BootIdentity::read()?.as_str().to_owned(), diff --git a/src/daemon/setup.rs b/src/daemon/setup.rs index f2cb7250f..b044a917f 100644 --- a/src/daemon/setup.rs +++ b/src/daemon/setup.rs @@ -1,3 +1,5 @@ +use std::{env, iter}; + use crate::durable_io::{AtomicWriteOptions, OverwriteMode, PermissionPolicy, SymlinkPolicy}; use anyhow::{Context, Result, bail}; use std::ffi::OsStr; @@ -6,9 +8,11 @@ use std::io::ErrorKind; use std::path::{Path, PathBuf}; use std::time::Duration; +use crate::process_broker::HelperKind; use crate::systemd_user_service::{ USER_SERVICE_NAME, render_user_service_unit, user_service_unit_path, }; +use crate::{durable_io, process_broker}; #[derive(Debug, Clone)] pub(crate) struct BackgroundModeSetupSummary { @@ -34,7 +38,7 @@ fn ensure_user_service_file() -> Result { })?; } - let executable = std::env::current_exe().context("failed to resolve wayscriber executable")?; + let executable = env::current_exe().context("failed to resolve wayscriber executable")?; let executable = service_executable(executable)?; let service_contents = render_user_service_unit(&executable); write_if_changed(&service_path, &service_contents)?; @@ -69,7 +73,7 @@ fn write_if_changed(path: &Path, content: &str) -> Result<()> { } } - crate::durable_io::write_text_atomic( + durable_io::write_text_atomic( path, content, AtomicWriteOptions { @@ -86,10 +90,10 @@ fn write_if_changed(path: &Path, content: &str) -> Result<()> { fn run_systemctl_user(args: &[&str]) -> Result<()> { let arguments = systemctl_user_arguments(args); - let output = crate::process_broker::current() + let output = process_broker::current() .and_then(|broker| { broker.run( - crate::process_broker::HelperKind::Systemctl, + HelperKind::Systemctl, OsStr::new("systemctl"), &arguments, Vec::new(), @@ -119,7 +123,7 @@ fn run_systemctl_user(args: &[&str]) -> Result<()> { } fn systemctl_user_arguments<'a>(args: &'a [&'a str]) -> Vec<&'a OsStr> { - std::iter::once(OsStr::new("--user")) + iter::once(OsStr::new("--user")) .chain(args.iter().map(OsStr::new)) .collect() } @@ -135,6 +139,8 @@ fn systemctl_error_detail(stdout: &str, stderr: &str) -> String { #[cfg(test)] mod tests { + use crate::test_temp; + use super::*; #[test] @@ -156,7 +162,7 @@ mod tests { #[test] fn a_replaced_or_missing_executable_is_not_written_into_the_unit() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let installed = temp.path().join("wayscriber"); fs::write(&installed, b"binary").unwrap(); diff --git a/src/daemon/tests.rs b/src/daemon/tests.rs index 9cfa07148..cebe52b02 100644 --- a/src/daemon/tests.rs +++ b/src/daemon/tests.rs @@ -1,3 +1,18 @@ +#[cfg(feature = "tray")] +use crate::backend::wayland::RuntimeWakeSource; +#[cfg(feature = "tray")] +use crate::env_vars; +#[cfg(feature = "tray")] +use crate::process_broker; +#[cfg(feature = "tray")] +use crate::test_env; +#[cfg(feature = "tray")] +use crate::test_fake_helper; +#[cfg(feature = "tray")] +use crate::test_fake_helper::Role; +#[cfg(feature = "tray")] +use crate::test_temp; + use super::core::Daemon; #[cfg(feature = "tray")] use super::tray::WayscriberTray; @@ -8,9 +23,17 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering}; #[cfg(feature = "tray")] -use ksni::{Tray, menu::MenuItem}; +use ksni::Tray; +#[cfg(feature = "tray")] +use ksni::menu::MenuItem; #[cfg(feature = "tray")] use std::sync::atomic::AtomicBool; +#[cfg(feature = "tray")] +use std::thread; +#[cfg(feature = "tray")] +use std::time::Duration; +#[cfg(feature = "tray")] +use std::time::Instant; #[test] fn daemon_session_resume_override_reflects_constructor_value() { @@ -135,12 +158,12 @@ fn menu_labels(tray: &WayscriberTray) -> Vec { fn tray_toggle_action_sets_flag() { let toggle = Arc::new(AtomicBool::new(false)); let quit = Arc::new(AtomicBool::new(false)); - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut tray = WayscriberTray::new_for_tests_with_wake(toggle.clone(), quit, wake.handle()); activate_menu_item(&mut tray, "Toggle Overlay"); assert!(toggle.load(AtomicOrdering::SeqCst)); - assert!(wake.wait_readable(Some(std::time::Duration::ZERO)).unwrap()); + assert!(wake.wait_readable(Some(Duration::ZERO)).unwrap()); } #[cfg(feature = "tray")] @@ -185,12 +208,12 @@ fn tray_menu_groups_actions_to_fit_short_displays() { fn tray_quit_action_sets_quit_flag() { let toggle = Arc::new(AtomicBool::new(false)); let quit = Arc::new(AtomicBool::new(false)); - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let mut tray = WayscriberTray::new_for_tests_with_wake(toggle, quit.clone(), wake.handle()); activate_menu_item(&mut tray, "Quit"); assert!(quit.load(AtomicOrdering::SeqCst)); - assert!(wake.wait_readable(Some(std::time::Duration::ZERO)).unwrap()); + assert!(wake.wait_readable(Some(Duration::ZERO)).unwrap()); } /// The tray used to answer a session-resume click by rewriting `[session]`. It @@ -231,29 +254,23 @@ fn tray_settings_items_open_their_configurator_destinations() { "--open\nui/toolbar?search=language\n", ), ] { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let recorded = temp.path().join("arguments"); - let recorder = crate::test_fake_helper::link(temp.path(), "recording-configurator"); + let recorder = test_fake_helper::link(temp.path(), "recording-configurator"); // The configurator override makes the broker accept the stand-in; the // config home keeps the launch failure path away from the developer's own // file. The previous values come back before the assertion. let mut variables = vec![ - ( - crate::env_vars::CONFIGURATOR_ENV, - Some(recorder.as_os_str()), - ), - ( - crate::env_vars::XDG_CONFIG_HOME_ENV, - Some(temp.path().as_os_str()), - ), + (env_vars::CONFIGURATOR_ENV, Some(recorder.as_os_str())), + (env_vars::XDG_CONFIG_HOME_ENV, Some(temp.path().as_os_str())), ]; - variables.extend(crate::test_fake_helper::environment( + variables.extend(test_fake_helper::environment( &recorder, - crate::test_fake_helper::Role::RecordArguments, + Role::RecordArguments, &recorded, )); - let launched = crate::test_env::with_env_vars(&variables, || { + let launched = test_env::with_env_vars(&variables, || { let toggle = Arc::new(AtomicBool::new(false)); let quit = Arc::new(AtomicBool::new(false)); let mut tray = WayscriberTray::new_for_tests_with_configurator( @@ -281,19 +298,19 @@ fn tray_settings_items_open_their_configurator_destinations() { #[cfg(feature = "tray")] fn record_configurator_menu_launch( tray: &mut WayscriberTray, - recorded: &std::path::Path, + recorded: &Path, label: &str, ) -> Option { for _ in 0..3 { - let _broker = crate::process_broker::start_for_runtime().ok()?; + let _broker = process_broker::start_for_runtime().ok()?; activate_menu_item(tray, label); - let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); - while std::time::Instant::now() < deadline { - if let Ok(arguments) = std::fs::read_to_string(recorded) { + let deadline = Instant::now() + Duration::from_secs(2); + while Instant::now() < deadline { + if let Ok(arguments) = fs::read_to_string(recorded) { return Some(arguments); } - std::thread::sleep(std::time::Duration::from_millis(10)); + thread::sleep(Duration::from_millis(10)); } } None diff --git a/src/daemon/tray/helpers.rs b/src/daemon/tray/helpers.rs index 2c67c9d4c..8027d61aa 100644 --- a/src/daemon/tray/helpers.rs +++ b/src/daemon/tray/helpers.rs @@ -1,18 +1,32 @@ #[cfg(feature = "tray")] use super::WayscriberTray; #[cfg(feature = "tray")] +use crate::about_window; +#[cfg(feature = "tray")] use crate::config::Config; #[cfg(feature = "tray")] use crate::configurator_destination::{ConfiguratorDestination, configurator_launch_arguments}; #[cfg(feature = "tray")] use crate::daemon::icons::{decode_tray_icon_png, fallback_tray_icon}; #[cfg(feature = "tray")] +use crate::desktop_open; +#[cfg(feature = "tray")] use crate::env_vars::CONFIGURATOR_ENV; #[cfg(feature = "tray")] use crate::i18n::tr; #[cfg(feature = "tray")] +use crate::notification; +#[cfg(feature = "tray")] use crate::paths::log_dir; #[cfg(feature = "tray")] +use crate::process_broker; +#[cfg(feature = "tray")] +use crate::process_broker::BrokerChild; +#[cfg(feature = "tray")] +use crate::process_broker::HelperKind; +#[cfg(feature = "tray")] +use crate::process_broker::HelperLifetime; +#[cfg(feature = "tray")] use crate::session::{clear_session, options_from_config}; #[cfg(feature = "tray")] use crate::tray_action::TrayAction; @@ -24,16 +38,20 @@ use std::env; use std::ffi::{OsStr, OsString}; #[cfg(feature = "tray")] use std::fs; +#[cfg(feature = "tray")] +use tokio::runtime::Handle; +#[cfg(feature = "tray")] +use tokio::runtime::Runtime; #[cfg(feature = "tray")] fn spawn_detached( - kind: crate::process_broker::HelperKind, + kind: HelperKind, program: &OsStr, arguments: &[OsString], -) -> anyhow::Result { - crate::process_broker::current()?.spawn( +) -> anyhow::Result { + process_broker::current()?.spawn( kind, - crate::process_broker::HelperLifetime::DetachedAfterExec, + HelperLifetime::DetachedAfterExec, program, arguments, Vec::new(), @@ -53,10 +71,10 @@ fn configurator_fallback_body(opened_config: bool) -> &'static str { #[cfg(feature = "tray")] impl WayscriberTray { /// Open the configurator, optionally at the screen for the menu item used. - pub(super) fn launch_configurator(&self, destination: Option) { - let arguments = configurator_launch_arguments(destination.as_ref()); + pub(super) fn launch_configurator(&self, destination: Option<&ConfiguratorDestination>) { + let arguments = configurator_launch_arguments(destination); match spawn_detached( - crate::process_broker::HelperKind::Configurator, + HelperKind::Configurator, OsStr::new(&self.configurator_binary), &arguments, ) { @@ -80,16 +98,16 @@ impl WayscriberTray { #[cfg(feature = "dbus")] { let body = configurator_fallback_body(opened_config); - match tokio::runtime::Handle::try_current() { - Ok(handle) => crate::notification::send_notification_async( + match Handle::try_current() { + Ok(handle) => notification::send_notification_async( &handle, tr("Configurator unavailable").to_string(), body.to_string(), Some("dialog-error".to_string()), ), Err(_) => { - if let Ok(rt) = tokio::runtime::Runtime::new() { - let _ = rt.block_on(crate::notification::send_notification( + if let Ok(rt) = Runtime::new() { + let _ = rt.block_on(notification::send_notification( tr("Configurator unavailable"), body, Some("dialog-error"), @@ -115,9 +133,9 @@ impl WayscriberTray { }; match spawn_detached( - crate::process_broker::HelperKind::About, + HelperKind::About, exe.as_os_str(), - &crate::about_window::launch_arguments(), + &about_window::launch_arguments(), ) { Ok(child) => { info!("Launched About window (pid {})", child.id()); @@ -131,14 +149,14 @@ impl WayscriberTray { /// Open the update instructions the watcher recorded. The URL comes from /// `update_check`, which only ever hands out validated wayscriber.com links. pub(super) fn open_update_instructions(&self, url: &str) { - let invocation = match crate::desktop_open::trusted_url(url) { + let invocation = match desktop_open::trusted_url(url) { Ok(invocation) => invocation, Err(err) => { warn!("Refused to open update instructions {url:?}: {err:#}"); return; } }; - match crate::desktop_open::open_in_background(invocation) { + match desktop_open::open_in_background(invocation) { Ok(_worker) => info!("Opening update instructions"), Err(err) => warn!("Failed to open update instructions {url}: {err}"), } @@ -193,15 +211,15 @@ impl WayscriberTray { return; } - let invocation = crate::desktop_open::path(&dir); - match crate::desktop_open::open_in_background(invocation) { + let invocation = desktop_open::path(&dir); + match desktop_open::open_in_background(invocation) { Ok(_worker) => info!("Opening log directory via desktop integration"), Err(err) => warn!("Failed to open log directory {}: {}", dir.display(), err), } } pub(super) fn open_config_file(&self) -> bool { - let path = match Config::get_config_path() { + let path = match Config::config_path() { Ok(p) => p, Err(err) => { warn!("Unable to resolve config path: {}", err); @@ -209,8 +227,8 @@ impl WayscriberTray { } }; - let invocation = crate::desktop_open::path(&path); - match crate::desktop_open::open_in_background(invocation) { + let invocation = desktop_open::path(&path); + match desktop_open::open_in_background(invocation) { Ok(_worker) => { info!("Opening config file at {}", path.display()); true diff --git a/src/daemon/tray/icon_theme_path.rs b/src/daemon/tray/icon_theme_path.rs index 861c56735..aa161ba98 100644 --- a/src/daemon/tray/icon_theme_path.rs +++ b/src/daemon/tray/icon_theme_path.rs @@ -8,9 +8,8 @@ use crate::env_vars::ICON_THEME_PATH_ENV; use crate::paths; -use std::env; -use std::fs; use std::path::{Path, PathBuf}; +use std::{env, fs}; /// The `IconThemePath` to advertise. /// @@ -62,16 +61,20 @@ fn installed_icon_theme_path(exe_path: &Path, standard_icon_dirs: &[PathBuf]) -> #[cfg(test)] mod tests { + use std::os::unix; + + use crate::test_temp; + use super::installed_icon_theme_path; use std::fs; #[test] fn installed_icon_dir_is_withheld_when_hosts_already_search_it() { - let root = crate::test_temp::tempdir().unwrap(); + let root = test_temp::tempdir().unwrap(); let share = root.path().join("usr/share"); fs::create_dir_all(share.join("icons")).unwrap(); let linked_share = root.path().join("linked-share"); - std::os::unix::fs::symlink(&share, &linked_share).unwrap(); + unix::fs::symlink(&share, &linked_share).unwrap(); let exe = root.path().join("usr/bin/wayscriber"); assert_eq!( @@ -86,7 +89,7 @@ mod tests { #[test] fn installed_icon_dir_is_advertised_for_a_private_prefix() { - let root = crate::test_temp::tempdir().unwrap(); + let root = test_temp::tempdir().unwrap(); let private_icons = root.path().join("opt/wayscriber/share/icons"); let system_icons = root.path().join("usr/share/icons"); fs::create_dir_all(&private_icons).unwrap(); diff --git a/src/daemon/tray/ksni.rs b/src/daemon/tray/ksni.rs index 7ae052849..dd7791d39 100644 --- a/src/daemon/tray/ksni.rs +++ b/src/daemon/tray/ksni.rs @@ -20,6 +20,7 @@ use crate::i18n::{tr, trf}; use crate::label_format::format_binding_label; #[cfg(feature = "tray")] use crate::tray_action::TrayAction; +use crate::{build_info, configurator_destination}; #[cfg(feature = "tray")] use log::{info, warn}; #[cfg(feature = "tray")] @@ -118,7 +119,7 @@ impl ksni::Tray for WayscriberTray { String::new() }, icon_pixmap: vec![], - title: trf("Wayscriber {}", &[&crate::build_info::version()]), + title: trf("Wayscriber {}", &[&build_info::version()]), description, } } @@ -136,7 +137,7 @@ impl ksni::Tray for WayscriberTray { } fn menu(&self) -> Vec> { - use ksni::menu::*; + use ksni::menu::{MenuItem, StandardItem, SubMenu}; let use_theme_icons = menu_theme_icons_enabled(); let overlay_active = self.overlay_active.load(Ordering::Acquire); let toggle_label = toggle_overlay_menu_label(); @@ -304,7 +305,7 @@ impl ksni::Tray for WayscriberTray { icon_name: menu_icon_name("preferences-desktop-locale", use_theme_icons), activate: Box::new(|this: &mut Self| { this.launch_configurator(Some( - crate::configurator_destination::language_destination(), + &configurator_destination::language_destination(), )); }), ..Default::default() @@ -323,7 +324,7 @@ impl ksni::Tray for WayscriberTray { label: session_settings_label().to_string(), icon_name: menu_icon_name("document-save", use_theme_icons), activate: Box::new(|this: &mut Self| { - this.launch_configurator(Some(ConfiguratorDestination::new( + this.launch_configurator(Some(&ConfiguratorDestination::new( ConfiguratorScreen::Session, ))); }), diff --git a/src/daemon/tray/mod.rs b/src/daemon/tray/mod.rs index ed154b282..480b4034f 100644 --- a/src/daemon/tray/mod.rs +++ b/src/daemon/tray/mod.rs @@ -15,6 +15,10 @@ use super::types::{ }; #[cfg(all(test, feature = "tray"))] use super::types::{OverlayActionIntents, VisibilityIntents}; +#[cfg(all(test, feature = "tray"))] +use crate::backend::wayland::RuntimeWakeHandle; +#[cfg(all(test, feature = "tray"))] +use crate::backend::wayland::RuntimeWakeSource; #[cfg(feature = "tray")] use crate::config::TrayIconStyle; #[cfg(feature = "tray")] @@ -69,7 +73,7 @@ impl WayscriberTray { } #[cfg(test)] pub(crate) fn new_for_tests(toggle_flag: Arc, quit_flag: Arc) -> Self { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); Self::new_for_tests_with_wake(toggle_flag, quit_flag, wake.handle()) } @@ -77,7 +81,7 @@ impl WayscriberTray { pub(crate) fn new_for_tests_with_wake( toggle_flag: Arc, quit_flag: Arc, - control_wake: crate::backend::wayland::RuntimeWakeHandle, + control_wake: RuntimeWakeHandle, ) -> Self { Self::for_tests(toggle_flag, quit_flag, control_wake, "true".into()) } @@ -91,7 +95,7 @@ impl WayscriberTray { quit_flag: Arc, configurator_binary: String, ) -> Self { - let wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); Self::for_tests(toggle_flag, quit_flag, wake.handle(), configurator_binary) } @@ -99,7 +103,7 @@ impl WayscriberTray { fn for_tests( toggle_flag: Arc, quit_flag: Arc, - control_wake: crate::backend::wayland::RuntimeWakeHandle, + control_wake: RuntimeWakeHandle, configurator_binary: String, ) -> Self { let visibility_intents = Arc::new(VisibilityIntents::with_ready(toggle_flag)); diff --git a/src/daemon/tray/runtime.rs b/src/daemon/tray/runtime.rs index f09cb14ef..448295721 100644 --- a/src/daemon/tray/runtime.rs +++ b/src/daemon/tray/runtime.rs @@ -1,14 +1,20 @@ +#[cfg(feature = "tray")] +use std::env; + use anyhow::{Context, Result, anyhow}; #[cfg(feature = "tray")] use ksni::TrayMethods; use log::{debug, info, warn}; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc; +use std::sync::{Arc, mpsc}; use std::thread; use std::thread::JoinHandle; use std::time::{Duration, Instant}; #[cfg(feature = "tray")] +use tokio::runtime::Runtime; +#[cfg(feature = "tray")] +use tokio::time; +#[cfg(feature = "tray")] use zbus::{Connection, Proxy}; #[cfg(feature = "tray")] @@ -59,7 +65,7 @@ pub(crate) fn start_system_tray( tray_status: Arc, ) -> Result { let configurator_binary = - std::env::var(CONFIGURATOR_ENV).unwrap_or_else(|_| "wayscriber-configurator".to_string()); + env::var(CONFIGURATOR_ENV).unwrap_or_else(|_| "wayscriber-configurator".to_string()); let icon_style = load_tray_settings_from_config(); let tray_quit = quit.clone(); @@ -67,7 +73,7 @@ pub(crate) fn start_system_tray( TrayControl { visibility, action, - quit: quit.clone(), + quit, }, configurator_binary, icon_style, @@ -79,7 +85,7 @@ pub(crate) fn start_system_tray( let runtime = TrayRuntime::start( move |worker| { - let rt = match tokio::runtime::Runtime::new() { + let rt = match Runtime::new() { Ok(runtime) => runtime, Err(e) => { warn!("Failed to create Tokio runtime for system tray: {}", e); @@ -121,7 +127,7 @@ pub(crate) fn start_system_tray( let _ = handle.shutdown().await; break; } - () = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => { + () = time::sleep(time::Duration::from_millis(100)) => { let revision = tray_status.revision(); if revision != last_revision { if handle.update(|_| {}).await.is_none() { @@ -159,7 +165,10 @@ pub(crate) fn start_system_tray( info!("Tray feature disabled; skipping system tray startup"); TrayRuntime::start( |worker| { - debug_assert!(!worker.is_cancelled()); + debug_assert!( + !worker.is_cancelled(), + "the stub tray worker reports readiness before startup can time out" + ); worker.report_readiness(Ok(())); }, TRAY_START_TIMEOUT, @@ -189,7 +198,7 @@ impl TrayWorker { #[cfg(feature = "tray")] async fn cancelled(&self) { while !self.is_cancelled() { - tokio::time::sleep(Duration::from_millis(10)).await; + time::sleep(Duration::from_millis(10)).await; } } } diff --git a/src/daemon/tray/shortcut_hint_io.rs b/src/daemon/tray/shortcut_hint_io.rs index c7b35b8ea..d665f085d 100644 --- a/src/daemon/tray/shortcut_hint_io.rs +++ b/src/daemon/tray/shortcut_hint_io.rs @@ -1,5 +1,7 @@ #[cfg(feature = "tray")] use crate::env_vars::{XDG_CURRENT_DESKTOP_ENV, XDG_SESSION_DESKTOP_ENV}; +use crate::process_broker; +use crate::process_broker::HelperKind; #[cfg(feature = "tray")] use crate::shortcut_hint::{ GNOME_MEDIA_KEYS_KEY, GNOME_MEDIA_KEYS_SCHEMA, PORTAL_SHORTCUT_ENV, ShortcutRuntimeBackend, @@ -55,9 +57,9 @@ fn read_gnome_shortcut_outputs() -> Option<(String, String)> { #[cfg(feature = "tray")] fn read_gsettings_value(schema: &str, key: &str) -> Option { let arguments = gsettings_get_arguments(schema, key); - let output = match crate::process_broker::current().and_then(|broker| { + let output = match process_broker::current().and_then(|broker| { broker.run( - crate::process_broker::HelperKind::Gsettings, + HelperKind::Gsettings, OsStr::new("gsettings"), arguments, Vec::new(), diff --git a/src/daemon/types.rs b/src/daemon/types.rs index 4057c9e3e..b761c876b 100644 --- a/src/daemon/types.rs +++ b/src/daemon/types.rs @@ -1,16 +1,16 @@ use crate::tray_action::TrayAction; -#[cfg(feature = "tray")] -use log::warn; use std::collections::VecDeque; use std::ffi::OsString; -use std::io; -use std::sync::Arc; -use std::sync::Mutex; +use std::fmt::{Display, Formatter}; +#[cfg(feature = "tray")] +use std::sync::MutexGuard; #[cfg(feature = "tray")] use std::sync::atomic::AtomicU64; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; #[cfg(feature = "tray")] use std::time::Instant; +use std::{error, fmt, io, mem}; use crate::backend::wayland::RuntimeWakeHandle; #[cfg(all(test, feature = "portal"))] @@ -60,7 +60,7 @@ impl DaemonControlEvent { #[cfg(all(test, feature = "portal"))] pub(super) fn for_test(flag: Arc) -> (Self, RuntimeWakeSource) { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().expect("create the test runtime wake source"); (Self::new(flag, wake.handle()), wake) } } @@ -109,13 +109,13 @@ impl VisibilityIntents { let mut state = self .state .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("daemon intents are never locked across a panic"); if !self.ready.swap(false, Ordering::Acquire) { return None; } Some(VisibilityIntent { activation_token: state.activation_token.take(), - signal_requested: std::mem::take(&mut state.signal_requested), + signal_requested: mem::take(&mut state.signal_requested), }) } } @@ -131,7 +131,7 @@ impl VisibilityPublisher { .intents .state .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("daemon intents are never locked across a panic"); if activation_token.is_some() { state.activation_token = activation_token; } @@ -248,11 +248,10 @@ impl TrayStatusShared { self.revision.load(Ordering::Acquire) } - fn lock_status(&self) -> std::sync::MutexGuard<'_, TrayStatus> { - self.inner.lock().unwrap_or_else(|poisoned| { - warn!("tray status mutex poisoned; recovering"); - poisoned.into_inner() - }) + fn lock_status(&self) -> MutexGuard<'_, TrayStatus> { + self.inner + .lock() + .expect("tray status is never locked across a panic") } fn bump_revision(&self) { @@ -263,13 +262,13 @@ impl TrayStatusShared { #[derive(Debug)] pub struct AlreadyRunningError; -impl std::fmt::Display for AlreadyRunningError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for AlreadyRunningError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "wayscriber daemon is already running") } } -impl std::error::Error for AlreadyRunningError {} +impl error::Error for AlreadyRunningError {} /// Daemon state manager pub type BackendRunner = dyn Fn(Option) -> anyhow::Result<()> + Send + Sync; @@ -305,6 +304,13 @@ pub(super) enum OverlayActionPublishError { } impl OverlayActionIntents { + #[cfg_attr( + not(any(feature = "tray", test)), + expect( + clippy::needless_pass_by_value, + reason = "the publisher keeps the wake handle only in tray and test builds" + ) + )] pub(super) fn publisher(self: &Arc, wake: RuntimeWakeHandle) -> OverlayActionPublisher { #[cfg(not(any(feature = "tray", test)))] let _ = wake; @@ -324,7 +330,7 @@ impl OverlayActionIntents { let mut state = self .state .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("daemon intents are never locked across a panic"); if !self.ready.swap(false, Ordering::Acquire) { return Vec::new(); } @@ -338,7 +344,7 @@ impl OverlayActionIntents { let mut state = self .state .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("daemon intents are never locked across a panic"); assert!( completed <= state.in_flight, "completed action count exceeds in-flight ownership" @@ -355,7 +361,7 @@ impl OverlayActionIntents { let state = self .state .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("daemon intents are never locked across a panic"); (state.queue.len(), state.in_flight) } } @@ -367,7 +373,7 @@ impl OverlayActionPublisher { .intents .state .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("daemon intents are never locked across a panic"); if state.queue.len() + state.in_flight >= MAX_OVERLAY_ACTION_INTENTS { return Err(OverlayActionPublishError::QueueFull); } @@ -380,6 +386,8 @@ impl OverlayActionPublisher { #[cfg(test)] mod control_tests { + use std::thread; + use super::*; use std::sync::Barrier; use std::sync::atomic::AtomicUsize; @@ -423,7 +431,7 @@ mod control_tests { let claimant_intents = Arc::clone(&intents); let claimant_claim = Arc::clone(&claim_barrier); let claimant_release = Arc::clone(&release_barrier); - let claimant = std::thread::spawn(move || { + let claimant = thread::spawn(move || { claimant_intents.claim_batch_with_hook(|| { claimant_claim.wait(); claimant_release.wait(); @@ -431,8 +439,8 @@ mod control_tests { }); claim_barrier.wait(); - let late_publisher = publisher.clone(); - let late = std::thread::spawn(move || { + let late_publisher = publisher; + let late = thread::spawn(move || { late_publisher.publish(TrayAction::ToggleHelp).unwrap(); }); release_barrier.wait(); diff --git a/src/daemon/update_watch.rs b/src/daemon/update_watch.rs index 8f9238318..260d6fbf7 100644 --- a/src/daemon/update_watch.rs +++ b/src/daemon/update_watch.rs @@ -12,9 +12,11 @@ use std::thread::{self, JoinHandle}; use std::time::{Duration, Instant}; use log::{debug, info, warn}; +use tokio::runtime::Builder; use crate::config::{Config, UpdatesConfig}; use crate::i18n::trf; +use crate::notification; use crate::notification::NotificationError; use crate::update_check::{self, AvailableUpdate, CachedStatus, CheckOutcome}; @@ -64,20 +66,20 @@ pub(super) fn start_update_watch( ); Some(thread::spawn(move || { - watch(quit, overlay_active, sink, updates); + watch(&quit, &overlay_active, &sink, updates); })) } fn watch( - quit: super::types::DaemonControlEvent, - overlay_active: Arc, - sink: StatusSink, + quit: &super::types::DaemonControlEvent, + overlay_active: &AtomicBool, + sink: &StatusSink, updates: UpdatesConfig, ) { // Seed from the cache first: a daemon restart should not lose a pending // notice, and reading the cache costs no network. let mut pending = update_from_cache(); - publish(&sink, pending.as_ref()); + publish(sink, pending.as_ref()); let mut next_check = Instant::now() + STARTUP_DELAY; let mut next_announce = next_check; @@ -94,7 +96,7 @@ fn watch( CheckOutcome::UpToDate { .. } => None, }; announced = false; - publish(&sink, pending.as_ref()); + publish(sink, pending.as_ref()); } next_check = now + POLL_INTERVAL; } @@ -104,7 +106,7 @@ fn watch( // rather than at the next daily check. if !announced && now >= next_announce { announced = match pending.as_ref() { - Some(update) => announce(update, updates.notify, &overlay_active), + Some(update) => announce(update, updates.notify, overlay_active), None => false, }; next_announce = now + ANNOUNCE_RETRY; @@ -205,10 +207,7 @@ fn send_notification(update: &AvailableUpdate) -> Result<(), NotificationError> let summary = trf("Wayscriber {} is available", &[&update.version]); let body = notification_body(update_check::current_version(), update); - let runtime = match tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - { + let runtime = match Builder::new_current_thread().enable_all().build() { Ok(runtime) => runtime, Err(err) => { warn!("Failed to create a runtime for the update notification: {err}"); @@ -218,7 +217,7 @@ fn send_notification(update: &AvailableUpdate) -> Result<(), NotificationError> } }; - runtime.block_on(crate::notification::send_notification( + runtime.block_on(notification::send_notification( &summary, &body, Some(NOTIFICATION_ICON), @@ -248,6 +247,19 @@ fn publish(sink: &StatusSink, update: Option<&AvailableUpdate>) { #[cfg(test)] mod tests { + #[cfg(not(feature = "dbus"))] + use std::env; + use std::fs; + + use crate::config; + use crate::config::test_helpers; + #[cfg(not(feature = "dbus"))] + use crate::env_vars; + #[cfg(not(feature = "dbus"))] + use crate::test_env; + #[cfg(not(feature = "dbus"))] + use crate::test_temp; + use super::*; use crate::update_check::{DEFAULT_NOTES_URL, DEFAULT_UPDATE_URL}; @@ -288,11 +300,12 @@ mod tests { #[cfg(not(feature = "dbus"))] #[test] fn unavailable_notifications_settle_without_claiming_the_release() { - let _lock = crate::test_env::lock(); - let cache_home = crate::test_temp::tempdir().expect("temporary cache home"); - let previous = std::env::var_os(crate::env_vars::XDG_CACHE_HOME_ENV); + let _lock = test_env::lock(); + let cache_home = test_temp::tempdir().expect("temporary cache home"); + let previous = env::var_os(env_vars::XDG_CACHE_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_lock`. unsafe { - std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, cache_home.path()); + env::set_var(env_vars::XDG_CACHE_HOME_ENV, cache_home.path()); } let update = update(); @@ -300,25 +313,24 @@ mod tests { assert!(announce(&update, true, &AtomicBool::new(false))); assert!(update_check::notification_pending(&update)); - match previous { - Some(value) => unsafe { - std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, value); - }, - None => unsafe { - std::env::remove_var(crate::env_vars::XDG_CACHE_HOME_ENV); - }, + // SAFETY: this test still holds the environment mutex in `_lock`. + unsafe { + match previous { + Some(value) => env::set_var(env_vars::XDG_CACHE_HOME_ENV, value), + None => env::remove_var(env_vars::XDG_CACHE_HOME_ENV), + } } } /// A broken config must not resurrect a check the user switched off. #[test] fn an_unparseable_config_disables_the_check_rather_than_defaulting_it_on() { - crate::config::test_helpers::with_temp_config_home(|config_root| { - let config_dir = config_root.join(crate::config::PRIMARY_CONFIG_DIR); - std::fs::create_dir_all(&config_dir).unwrap(); + test_helpers::with_temp_config_home(|config_root| { + let config_dir = config_root.join(config::PRIMARY_CONFIG_DIR); + fs::create_dir_all(&config_dir).unwrap(); // A `check = false` the user wrote, plus an unrelated syntax error. - std::fs::write( + fs::write( config_dir.join("config.toml"), "[updates]\ncheck = false\n\n[ui\nbroken = ", ) @@ -329,10 +341,10 @@ mod tests { assert!(!update_check::background_checks_enabled(updates.check)); // The same file without the syntax error is honored as written. - std::fs::write(config_dir.join("config.toml"), "[updates]\ncheck = false\n").unwrap(); + fs::write(config_dir.join("config.toml"), "[updates]\ncheck = false\n").unwrap(); assert!(!load_updates_config().check); - std::fs::write(config_dir.join("config.toml"), "[updates]\ncheck = true\n").unwrap(); + fs::write(config_dir.join("config.toml"), "[updates]\ncheck = true\n").unwrap(); assert!(load_updates_config().check); }); } diff --git a/src/desktop_open.rs b/src/desktop_open.rs index 3ef4142fe..68fef7419 100644 --- a/src/desktop_open.rs +++ b/src/desktop_open.rs @@ -10,11 +10,13 @@ use std::ffi::{OsStr, OsString}; use std::path::{Path, PathBuf}; +use std::thread::{Builder, JoinHandle}; use anyhow::{Context, Result, bail}; use crate::i18n::tr; -use crate::process_broker::{HelperKind, HelperLifetime}; +use crate::process_broker::{HelperKind, HelperLifetime, ProcessBroker}; +use crate::{process_broker, update_check}; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct DesktopOpenInvocation { @@ -68,7 +70,7 @@ impl DesktopOpenInvocation { /// Runtime owners that exit after the action use this form so teardown cannot /// cancel a helper that must outlive the overlay. pub(crate) fn open(invocation: &DesktopOpenInvocation) -> Result<()> { - let broker = crate::process_broker::current()?; + let broker = process_broker::current()?; spawn_with_broker(&broker, invocation) } @@ -78,11 +80,9 @@ pub(crate) fn open(invocation: &DesktopOpenInvocation) -> Result<()> { /// /// Callers that own the process broker must join the returned worker before /// tearing the broker down, or a still-in-flight spawn exchange can be cancelled. -pub(crate) fn open_in_background( - invocation: DesktopOpenInvocation, -) -> Result> { - let broker = crate::process_broker::current()?; - std::thread::Builder::new() +pub(crate) fn open_in_background(invocation: DesktopOpenInvocation) -> Result> { + let broker = process_broker::current()?; + Builder::new() .name("wayscriber-desktop-open".to_string()) .spawn(move || { if let Err(err) = spawn_with_broker(&broker, &invocation) { @@ -94,10 +94,7 @@ pub(crate) fn open_in_background( .context("failed to start desktop-open worker") } -fn spawn_with_broker( - broker: &crate::process_broker::ProcessBroker, - invocation: &DesktopOpenInvocation, -) -> Result<()> { +fn spawn_with_broker(broker: &ProcessBroker, invocation: &DesktopOpenInvocation) -> Result<()> { broker .spawn( HelperKind::DesktopOpen, @@ -117,7 +114,7 @@ pub(crate) fn path(path: &Path) -> DesktopOpenInvocation { /// Open an HTTPS URL on the update-check Wayscriber host allowlist /// (`wayscriber.com` and `www.wayscriber.com`). pub(crate) fn trusted_url(url: &str) -> Result { - if !crate::update_check::is_trusted_url(url) { + if !update_check::is_trusted_url(url) { bail!("refusing to open an untrusted Wayscriber URL: {url:?}"); } Ok(invocation(OsStr::new(url))) diff --git a/src/domain/action.rs b/src/domain/action.rs index b64ac0eb6..f974125db 100644 --- a/src/domain/action.rs +++ b/src/domain/action.rs @@ -237,6 +237,7 @@ pub enum Action { impl Action { /// Whether this action opens or addresses the native screen-region picker. + #[must_use] pub const fn is_region_capture(self) -> bool { matches!( self, diff --git a/src/domain/board.rs b/src/domain/board.rs index 111609fb5..71c67b4bb 100644 --- a/src/domain/board.rs +++ b/src/domain/board.rs @@ -11,6 +11,7 @@ pub enum BoardBackground { } impl BoardBackground { + #[must_use] pub fn is_transparent(&self) -> bool { matches!(self, BoardBackground::Transparent) } diff --git a/src/domain/board_grid.rs b/src/domain/board_grid.rs index 355b19a3c..1539d2c56 100644 --- a/src/domain/board_grid.rs +++ b/src/domain/board_grid.rs @@ -21,6 +21,7 @@ impl BoardGridKind { Self::IsometricDots, ]; + #[must_use] pub fn label(self) -> &'static str { match self { Self::None => "None", @@ -48,6 +49,7 @@ impl Default for BoardGrid { } impl BoardGrid { + #[must_use] pub fn new(kind: BoardGridKind, spacing: i64) -> Self { Self { kind, @@ -58,11 +60,13 @@ impl BoardGrid { } } + #[must_use] pub fn spacing(self) -> u16 { self.spacing } /// Disable decoration without discarding its spacing. + #[must_use] pub fn disabled(self) -> Self { Self { kind: BoardGridKind::None, diff --git a/src/domain/board_validation.rs b/src/domain/board_validation.rs index f989773f9..a74c58307 100644 --- a/src/domain/board_validation.rs +++ b/src/domain/board_validation.rs @@ -3,6 +3,7 @@ use std::collections::HashSet; /// Clamp RGB components to [0, 1]. Callers must replace NaN before calling; /// range clamping alone does not turn NaN into a usable color component. +#[must_use] pub fn clamp_board_rgb(mut rgb: [f64; 3]) -> ([f64; 3], bool) { let mut clamped = false; for component in &mut rgb { @@ -33,6 +34,7 @@ pub struct BoundaryBoardIdSet { } impl BoundaryBoardIdSet { + #[must_use] pub fn new() -> Self { Self::default() } diff --git a/src/domain/color.rs b/src/domain/color.rs index 01f445cbe..922b868c3 100644 --- a/src/domain/color.rs +++ b/src/domain/color.rs @@ -31,6 +31,7 @@ impl Color { /// All values should be in the range 0.0 to 1.0. /// This method is kept for future extensibility (custom colors in config file). #[allow(dead_code)] + #[must_use] pub fn new(r: f64, g: f64, b: f64, a: f64) -> Self { Self { r, g, b, a } } @@ -127,30 +128,31 @@ const fn rgb8(r: u8, g: u8, b: u8) -> Color { // keeps exact color equality intact, which swatch selection relies on. /// Tuned palette red `#F5333F` (named color "red", quick color slot 0) -pub const PALETTE_RED: Color = rgb8(0xF5, 0x33, 0x3F); +pub const PALETTE_RED: Color = rgb8(0xf5, 0x33, 0x3f); /// Tuned palette green `#2EC27E` (named color "green", quick color slot 1) -pub const PALETTE_GREEN: Color = rgb8(0x2E, 0xC2, 0x7E); +pub const PALETTE_GREEN: Color = rgb8(0x2e, 0xc2, 0x7e); /// Tuned palette blue `#3584E4` (named color "blue", quick color slot 2) -pub const PALETTE_BLUE: Color = rgb8(0x35, 0x84, 0xE4); +pub const PALETTE_BLUE: Color = rgb8(0x35, 0x84, 0xe4); /// Tuned palette yellow `#F6D32D` (named color "yellow", quick color slot 3) -pub const PALETTE_YELLOW: Color = rgb8(0xF6, 0xD3, 0x2D); +pub const PALETTE_YELLOW: Color = rgb8(0xf6, 0xd3, 0x2d); /// Tuned palette orange `#FF7800` (named color "orange", quick color slot 4) -pub const PALETTE_ORANGE: Color = rgb8(0xFF, 0x78, 0x00); +pub const PALETTE_ORANGE: Color = rgb8(0xff, 0x78, 0x00); /// Tuned palette pink `#C061CB` (named color "pink", quick color slot 5) -pub const PALETTE_PINK: Color = rgb8(0xC0, 0x61, 0xCB); +pub const PALETTE_PINK: Color = rgb8(0xc0, 0x61, 0xcb); /// Tuned palette white `#FFFFFF` (named color "white", quick color slot 6) pub const PALETTE_WHITE: Color = WHITE; /// Tuned palette black `#241F31` (named color "black", quick color slot 7) -pub const PALETTE_BLACK: Color = rgb8(0x24, 0x1F, 0x31); +pub const PALETTE_BLACK: Color = rgb8(0x24, 0x1f, 0x31); /// Convert an HSV triple (all components in 0.0–1.0) to an opaque RGB color. +#[must_use] pub fn hsv_to_rgb(h: f64, s: f64, v: f64) -> Color { let h = (h - h.floor()).clamp(0.0, 1.0) * 6.0; let i = h.floor(); @@ -172,6 +174,7 @@ pub fn hsv_to_rgb(h: f64, s: f64, v: f64) -> Color { /// Convert RGB components (0.0–1.0) to an HSV triple in 0.0–1.0. /// Hue is 0.0 for grays (delta = 0); callers that need hue continuity /// across gray colors must remember the last meaningful hue themselves. +#[must_use] pub fn rgb_to_hsv(r: f64, g: f64, b: f64) -> (f64, f64, f64) { let max = r.max(g).max(b); let min = r.min(g).min(b); diff --git a/src/domain/drawing.rs b/src/domain/drawing.rs index 54a21c5bd..720df19bf 100644 --- a/src/domain/drawing.rs +++ b/src/domain/drawing.rs @@ -7,6 +7,7 @@ pub const MAX_STROKE_THICKNESS: f64 = 50.0; /// A fractional width, such as one drawn before widths snapped or loaded from /// an older session, lands on the nearest whole pixel in the step's direction /// first: 30.8 steps up to 31 and down to 30, never skipping a value. +#[must_use] pub fn step_stroke_thickness(thickness: f64, delta: f64) -> f64 { let stepped = thickness + delta; let whole = if delta > 0.0 { diff --git a/src/domain/language.rs b/src/domain/language.rs index 73ecfee47..3203a42ee 100644 --- a/src/domain/language.rs +++ b/src/domain/language.rs @@ -29,6 +29,7 @@ impl Language { pub const ALL: [Self; 3] = [Self::Auto, Self::English, Self::SimplifiedChinese]; /// The spelling used in configuration and logs. + #[must_use] pub fn code(self) -> &'static str { match self { Self::Auto => "auto", @@ -39,6 +40,7 @@ impl Language { /// The translation catalog this language reads, or `None` when the source /// strings already are that language (English). + #[must_use] pub fn catalog_code(self) -> Option<&'static str> { match self { Self::Auto | Self::English => None, @@ -47,6 +49,7 @@ impl Language { } /// Whether this setting defers to the desktop locale. + #[must_use] pub fn is_auto(self) -> bool { matches!(self, Self::Auto) } @@ -54,6 +57,7 @@ impl Language { /// The language's name in itself, never translated, so a user can find /// their own language in a list drawn in one they cannot read. `None` for /// `Auto`, which is not a language. + #[must_use] pub fn native_name(self) -> Option<&'static str> { match self { Self::Auto => None, @@ -65,6 +69,7 @@ impl Language { /// The language's word for "language", never translated, so the setting /// that switches languages can be recognized from any of them. `None` for /// `Auto`, which is not a language. + #[must_use] pub fn word_for_language(self) -> Option<&'static str> { match self { Self::Auto => None, @@ -74,6 +79,7 @@ impl Language { } /// The language's English name, for command-line output and logs. + #[must_use] pub fn english_name(self) -> &'static str { match self { Self::Auto => "Automatic", diff --git a/src/domain/tests.rs b/src/domain/tests.rs index 62f676a22..efbd4d568 100644 --- a/src/domain/tests.rs +++ b/src/domain/tests.rs @@ -1,10 +1,18 @@ use std::fmt::Debug; use std::fs; +use std::os::unix; use std::path::{Path, PathBuf}; use serde::Serialize; use serde::de::DeserializeOwned; +use crate::config::keybindings; +use crate::draw::color; +use crate::input as input_module; +use crate::input::state::{PressureThicknessEditMode, PressureThicknessEntryMode}; +use crate::input::tool; +use crate::{config, draw, test_temp}; + use super::{ Action, BoardBackground, BoardSpec, Color, DragBindableTool, DragTool, EraserMode, Tool, }; @@ -351,35 +359,35 @@ fn color_serialization_matches_established_contract() { #[test] fn established_public_paths_reexport_domain_types() { - let action: crate::config::Action = Action::Exit; + let action: config::Action = Action::Exit; let _: Action = action; - let keybinding_action: crate::config::keybindings::Action = Action::Undo; + let keybinding_action: keybindings::Action = Action::Undo; let _: Action = keybinding_action; - let tool: crate::input::Tool = Tool::Pen; + let tool: input_module::Tool = Tool::Pen; let _: Tool = tool; - let nested_tool: crate::input::tool::Tool = Tool::Line; + let nested_tool: tool::Tool = Tool::Line; let _: Tool = nested_tool; - let drag_tool: crate::input::tool::DragTool = DragTool::Pen; + let drag_tool: tool::DragTool = DragTool::Pen; let _: DragTool = drag_tool; - let drag_bindable: crate::input::tool::DragBindableTool = DragBindableTool::Rect; + let drag_bindable: tool::DragBindableTool = DragBindableTool::Rect; let _: DragBindableTool = drag_bindable; - let eraser_mode: crate::input::EraserMode = EraserMode::Brush; + let eraser_mode: input_module::EraserMode = EraserMode::Brush; let _: EraserMode = eraser_mode; - let color: crate::draw::Color = Color { + let color: draw::Color = Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0, }; let _: Color = color; - let nested_color: crate::draw::color::Color = crate::draw::color::RED; + let nested_color: color::Color = color::RED; let _: Color = nested_color; - let background: crate::input::BoardBackground = BoardBackground::Transparent; + let background: input_module::BoardBackground = BoardBackground::Transparent; let _: BoardBackground = background; - let board: crate::input::BoardSpec = BoardSpec { + let board: input_module::BoardSpec = BoardSpec { id: "board".to_string(), name: "Board".to_string(), background: BoardBackground::Transparent, @@ -440,7 +448,7 @@ fn production_domain_sources_have_no_upward_crate_dependencies() { #[test] fn nested_domain_sources_are_checked_for_upward_dependencies() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let nested = temp.path().join("nested/deeper"); fs::create_dir_all(&nested).unwrap(); let source = nested.join("value.rs"); @@ -454,20 +462,20 @@ fn nested_domain_sources_are_checked_for_upward_dependencies() { fs::create_dir(&tests).unwrap(); fs::write(tests.join("fixture.rs"), "use crate::config::Config;\n").unwrap(); #[cfg(unix)] - std::os::unix::fs::symlink(temp.path(), nested.join("cycle")).unwrap(); + unix::fs::symlink(temp.path(), nested.join("cycle")).unwrap(); assert_eq!(check_domain_sources(temp.path()).unwrap(), 1); #[cfg(unix)] { - let linked = crate::test_temp::tempdir().unwrap(); + let linked = test_temp::tempdir().unwrap(); fs::write( linked.path().join("source.rs"), "use crate::config::Config;\n", ) .unwrap(); let alias = nested.join("alias.rs"); - std::os::unix::fs::symlink(linked.path().join("source.rs"), &alias).unwrap(); + unix::fs::symlink(linked.path().join("source.rs"), &alias).unwrap(); assert_eq!(check_domain_sources(temp.path()).unwrap_err(), alias); } @@ -511,8 +519,8 @@ fn pressure_preferences_preserve_public_paths_and_serialized_names() { (Entry::PressureOnly, "pressure_only"), (Entry::AnyPressure, "any_pressure"), ]); - let legacy_edit: crate::input::state::PressureThicknessEditMode = Edit::Scale; - let legacy_entry: crate::input::state::PressureThicknessEntryMode = Entry::AnyPressure; + let legacy_edit: PressureThicknessEditMode = Edit::Scale; + let legacy_entry: PressureThicknessEntryMode = Entry::AnyPressure; assert_eq!(legacy_edit, Edit::Scale); assert_eq!(legacy_entry, Entry::AnyPressure); } diff --git a/src/domain/tool.rs b/src/domain/tool.rs index 8f43daa26..60ad78f99 100644 --- a/src/domain/tool.rs +++ b/src/domain/tool.rs @@ -131,6 +131,7 @@ pub enum DragBindableTool { } impl DragBindableTool { + #[must_use] pub fn to_drag_tool(self) -> DragTool { match self { Self::Select => DragTool::Select, @@ -153,6 +154,7 @@ impl DragBindableTool { } } + #[must_use] pub fn to_tool(self) -> Tool { match self { Self::Select => Tool::Select, @@ -175,6 +177,7 @@ impl DragBindableTool { } } + #[must_use] pub fn from_tool(tool: Tool) -> Option { match tool { Tool::Select => Some(Self::Select), @@ -200,6 +203,7 @@ impl DragBindableTool { } } + #[must_use] pub fn from_drag_tool(tool: DragTool) -> Option { match tool { DragTool::Default => None, diff --git a/src/draw/canvas_set/pages.rs b/src/draw/canvas_set/pages.rs index 660bcceb4..c3d35871b 100644 --- a/src/draw/canvas_set/pages.rs +++ b/src/draw/canvas_set/pages.rs @@ -1,3 +1,5 @@ +use std::mem; + use super::super::Frame; /// Collection of pages for a single board mode. @@ -25,6 +27,7 @@ impl Default for BoardPages { } impl BoardPages { + #[must_use] pub fn new() -> Self { Self { pages: vec![Frame::new()], @@ -33,6 +36,7 @@ impl BoardPages { } } + #[must_use] pub fn from_pages(mut pages: Vec, active: usize) -> Self { if pages.is_empty() { pages.push(Frame::new()); @@ -45,14 +49,17 @@ impl BoardPages { } } + #[must_use] pub fn page_count(&self) -> usize { self.pages.len() } + #[must_use] pub fn active_index(&self) -> usize { self.active } + #[must_use] pub fn generation(&self) -> u64 { self.generation } @@ -61,6 +68,7 @@ impl BoardPages { self.generation = self.generation.wrapping_add(1); } + #[must_use] pub fn active_frame(&self) -> &Frame { &self.pages[self.active] } @@ -187,7 +195,7 @@ impl BoardPages { return None; } if len == 1 { - let page = std::mem::take(&mut self.pages[0]); + let page = mem::take(&mut self.pages[0]); self.active = 0; self.bump_generation(); return Some(page); @@ -257,10 +265,12 @@ impl BoardPages { } } + #[must_use] pub fn pages(&self) -> &[Frame] { &self.pages } + #[must_use] pub fn page_name(&self, index: usize) -> Option<&str> { self.pages.get(index).and_then(|page| page.page_name()) } diff --git a/src/draw/dirty.rs b/src/draw/dirty.rs index d31526f54..59f41e804 100644 --- a/src/draw/dirty.rs +++ b/src/draw/dirty.rs @@ -2,6 +2,8 @@ //! //! Collects axis-aligned rectangles that need repainting between frames. +use std::mem; + use super::Shape; use crate::draw::TextMeasurer; use crate::util::Rect; @@ -35,6 +37,7 @@ impl Default for DirtyTracker { impl DirtyTracker { /// Creates a new, empty tracker with pre-allocated capacity. + #[must_use] pub fn new() -> Self { Self { regions: Vec::with_capacity(8), // Typical frame has 4-8 dirty regions @@ -118,7 +121,7 @@ impl DirtyTracker { self.force_full_reason = None; self.regions.retain(Rect::is_valid); DirtyRegionReport { - regions: std::mem::take(&mut self.regions), + regions: mem::take(&mut self.regions), full_reason: None, } } diff --git a/src/draw/font.rs b/src/draw/font.rs index ffeae8829..3e314e3a4 100644 --- a/src/draw/font.rs +++ b/src/draw/font.rs @@ -1,5 +1,7 @@ //! Font descriptor for text rendering. +use std::sync::OnceLock; + use serde::{Deserialize, Serialize}; /// Font configuration for text rendering. @@ -22,6 +24,7 @@ pub struct FontDescriptor { impl FontDescriptor { /// Creates a new font descriptor with the specified parameters. + #[must_use] pub fn new(family: String, weight: String, style: String) -> Self { Self { family, @@ -36,6 +39,7 @@ impl FontDescriptor { /// its state from this and `to_pango_string` renders from the same field. /// A numeric weight is not bold — the toggle writes the word, and a config /// that asks for `700` is asking for something the toggle cannot express. + #[must_use] pub fn is_bold(&self) -> bool { self.weight.trim().eq_ignore_ascii_case("bold") } @@ -47,6 +51,7 @@ impl FontDescriptor { /// so a family such as "Roboto Condensed" or "Archivo Black" would come /// back as "Roboto" condensed or "Archivo" black and render in whatever /// fontconfig substitutes. The size keeps its fraction. + #[must_use] pub fn to_pango_description(&self, size: f64) -> pango::FontDescription { let mut description = family_description(&self.family, size); if let Some(weight) = pango_weight(&self.weight) { @@ -66,6 +71,7 @@ impl FontDescriptor { /// `pango::FontDescription::from_string` reads back exactly what /// [`Self::to_pango_description`] built: a family ending in a style word /// is closed with a comma, as in "Roboto Condensed, Bold 17.5". + #[must_use] pub fn to_pango_string(&self, size: f64) -> String { self.to_pango_description(size).to_string() } @@ -75,6 +81,7 @@ impl FontDescriptor { /// /// The family is set as a name, never parsed, for the reason given on /// [`FontDescriptor::to_pango_description`]. +#[must_use] pub fn family_description(family: &str, size: f64) -> pango::FontDescription { let mut description = pango::FontDescription::new(); description.set_family(family.trim()); @@ -279,7 +286,7 @@ struct FontCatalog { monospace: Vec, } -static FONT_CATALOG: std::sync::OnceLock = std::sync::OnceLock::new(); +static FONT_CATALOG: OnceLock = OnceLock::new(); /// The installed families, enumerated once and kept for the life of the process. /// @@ -362,6 +369,7 @@ fn build_font_catalog(mut listed: Vec<(String, bool)>) -> FontCatalog { /// Font families installed on this system, sorted, with duplicates removed. /// /// See [`font_catalog`] for when the enumeration happens and why it is cached. +#[must_use] pub fn system_font_families() -> &'static [String] { &font_catalog().all } @@ -371,6 +379,7 @@ pub fn system_font_families() -> &'static [String] { /// The group people reach for when annotating code, and a small enough slice of /// a typical system — 10 of 269 on the machine this was measured on — to be /// worth offering as its own filter. Free once the full list has been built. +#[must_use] pub fn monospace_font_families() -> &'static [String] { &font_catalog().monospace } @@ -380,6 +389,7 @@ pub fn monospace_font_families() -> &'static [String] { /// Pango resolves an unknown family to whatever fontconfig substitutes, with no /// error and no warning, so a typo in a configured family silently renders in /// something else. This is how a caller can say so. +#[must_use] pub fn family_is_installed(family: &str) -> bool { let wanted = family.trim(); if wanted.is_empty() { @@ -397,6 +407,7 @@ pub fn family_is_installed(family: &str) -> bool { /// names exactly ends up treating them as two, and a step from one to the other /// changes only the spelling. Not `eq_ignore_ascii_case`: family names are not /// all Latin. +#[must_use] pub fn families_match(left: &str, right: &str) -> bool { normalized_family_name(left) == normalized_family_name(right) } @@ -407,6 +418,8 @@ fn normalized_family_name(family: &str) -> String { #[cfg(test)] mod system_font_tests { + use std::ptr; + use super::*; #[test] @@ -431,7 +444,7 @@ mod system_font_tests { let second = system_font_families(); assert!( - std::ptr::eq(first, second), + ptr::eq(first, second), "the second call must not re-enumerate" ); } @@ -444,8 +457,8 @@ mod system_font_tests { let all = system_font_families(); let monospace = monospace_font_families(); - assert!(std::ptr::eq(all, system_font_families())); - assert!(std::ptr::eq(monospace, monospace_font_families())); + assert!(ptr::eq(all, system_font_families())); + assert!(ptr::eq(monospace, monospace_font_families())); // Same catalog: the monospace list is in the full list's own order. let mut positions = monospace .iter() diff --git a/src/draw/frame/core.rs b/src/draw/frame/core.rs index dbaac9678..1ac8691e5 100644 --- a/src/draw/frame/core.rs +++ b/src/draw/frame/core.rs @@ -1,3 +1,6 @@ +#[cfg(test)] +use std::cell::Cell; + use super::frame_storage; use super::types::{DrawnShape, ShapeId, UndoAction}; use crate::draw::shape::Shape; @@ -5,7 +8,7 @@ use serde::Serialize; #[cfg(test)] std::thread_local! { - static LINEAR_ID_LOOKUPS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static LINEAR_ID_LOOKUPS: Cell = const { Cell::new(0) }; } #[derive(Debug, Clone, Serialize)] @@ -43,6 +46,7 @@ impl Default for Frame { impl Frame { /// Creates a new empty frame. + #[must_use] pub fn new() -> Self { Self { shapes: super::FrameShapes::default(), @@ -71,6 +75,7 @@ impl Frame { } /// Clone the frame's shapes while dropping history. + #[must_use] pub fn clone_without_history(&self) -> Self { let mut frame = Frame::new(); frame.shapes = self.shapes.clone(); @@ -82,6 +87,7 @@ impl Frame { /// Copy exactly the newest undo/redo entries retained by clamp_history_depth, /// without first allocating copies of discarded actions or their payloads. + #[must_use] pub fn clone_with_history_limit(&self, limit: usize) -> Self { if limit == usize::MAX { return self.clone(); @@ -94,6 +100,7 @@ impl Frame { frame } + #[must_use] pub fn page_name(&self) -> Option<&str> { self.page_name.as_deref() } @@ -112,17 +119,20 @@ impl Frame { #[allow(dead_code)] /// Returns the number of shapes in the frame. + #[must_use] pub fn len(&self) -> usize { self.shapes.len() } #[allow(dead_code)] /// Returns true if the frame contains no shapes. + #[must_use] pub fn is_empty(&self) -> bool { self.shapes.is_empty() } /// Returns true if the frame contains data worth persisting. + #[must_use] pub fn has_persistable_data(&self) -> bool { !self.shapes.is_empty() || self.page_name.is_some() @@ -139,10 +149,12 @@ impl Frame { } /// Runtime token for the shape content; view/background are separate cache inputs. + #[must_use] pub fn content_revision(&self) -> u64 { self.shapes.revision() } + #[must_use] pub fn view_offset(&self) -> (i32, i32) { self.view_offset } @@ -195,6 +207,7 @@ impl Frame { } /// Finds the index of a shape by id. + #[must_use] pub fn find_index(&self, id: ShapeId) -> Option { #[cfg(test)] LINEAR_ID_LOOKUPS.with(|count| count.set(count.get().saturating_add(1))); @@ -212,10 +225,11 @@ impl Frame { #[cfg(test)] pub(crate) fn linear_id_lookup_count() -> usize { - LINEAR_ID_LOOKUPS.with(std::cell::Cell::get) + LINEAR_ID_LOOKUPS.with(Cell::get) } /// Returns a reference to a shape by id. + #[must_use] pub fn shape(&self, id: ShapeId) -> Option<&DrawnShape> { self.find_index(id).map(|i| &self.shapes[i]) } diff --git a/src/draw/frame/frame_storage.rs b/src/draw/frame/frame_storage.rs index 7b7dfad00..20cffffcb 100644 --- a/src/draw/frame/frame_storage.rs +++ b/src/draw/frame/frame_storage.rs @@ -60,9 +60,9 @@ where } #[derive(Serialize)] -struct SerializedDrawnShape<'a> { +struct SerializedDrawnShape<'shape> { id: ShapeId, - shape: &'a Shape, + shape: &'shape Shape, created_at: u64, locked: bool, } diff --git a/src/draw/frame/history/frame/apply.rs b/src/draw/frame/history/frame/apply.rs index 422bdcaa8..0a0ea99cc 100644 --- a/src/draw/frame/history/frame/apply.rs +++ b/src/draw/frame/history/frame/apply.rs @@ -1,3 +1,5 @@ +use crate::draw::shape::Shape; + use super::super::super::core::Frame; use super::super::super::types::{DrawnShape, ShapeId, UndoAction}; @@ -102,7 +104,7 @@ impl Frame { bounds: super::super::super::types::ImageBoundsSnapshot, ) { if let Some(target) = self.shape_mut(shape_id) - && let crate::draw::shape::Shape::Image { x, y, w, h, .. } = &mut target.shape + && let Shape::Image { x, y, w, h, .. } = &mut target.shape { *x = bounds.x; *y = bounds.y; diff --git a/src/draw/frame/history/frame/mod.rs b/src/draw/frame/history/frame/mod.rs index 0dc40fa97..a7ed4597f 100644 --- a/src/draw/frame/history/frame/mod.rs +++ b/src/draw/frame/history/frame/mod.rs @@ -21,6 +21,7 @@ impl Frame { /// push, undo, redo, and history clear, and trimming the oldest entries /// leaves it alone. Feedback that advertises a particular undo (the /// Shape Pen chip) compares it to know the undo still means that. + #[must_use] pub fn history_revision(&self) -> u64 { self.history_revision } @@ -74,12 +75,14 @@ impl Frame { #[allow(dead_code)] /// Returns a reference to the undo stack (for testing). + #[must_use] pub fn undo_stack_len(&self) -> usize { self.undo_stack.len() } #[allow(dead_code)] /// Returns a reference to the redo stack length (for testing). + #[must_use] pub fn redo_stack_len(&self) -> usize { self.redo_stack.len() } diff --git a/src/draw/frame/shapes.rs b/src/draw/frame/shapes.rs index 5a8bf2c54..34a9dc0f9 100644 --- a/src/draw/frame/shapes.rs +++ b/src/draw/frame/shapes.rs @@ -1,3 +1,6 @@ +use std::slice::{Iter, IterMut}; +use std::vec; + use super::DrawnShape; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicU64, Ordering}; @@ -63,7 +66,7 @@ impl DerefMut for FrameShapes { impl<'a> IntoIterator for &'a FrameShapes { type Item = &'a DrawnShape; - type IntoIter = std::slice::Iter<'a, DrawnShape>; + type IntoIter = Iter<'a, DrawnShape>; fn into_iter(self) -> Self::IntoIter { self.values.iter() } @@ -71,7 +74,7 @@ impl<'a> IntoIterator for &'a FrameShapes { impl<'a> IntoIterator for &'a mut FrameShapes { type Item = &'a mut DrawnShape; - type IntoIter = std::slice::IterMut<'a, DrawnShape>; + type IntoIter = IterMut<'a, DrawnShape>; fn into_iter(self) -> Self::IntoIter { self.deref_mut().iter_mut() } @@ -79,7 +82,7 @@ impl<'a> IntoIterator for &'a mut FrameShapes { impl IntoIterator for FrameShapes { type Item = DrawnShape; - type IntoIter = std::vec::IntoIter; + type IntoIter = vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.values.into_iter() } diff --git a/src/draw/frame/tests/history/basics.rs b/src/draw/frame/tests/history/basics.rs index 4161e5593..9947aecb5 100644 --- a/src/draw/frame/tests/history/basics.rs +++ b/src/draw/frame/tests/history/basics.rs @@ -1,5 +1,6 @@ +use crate::draw::color::BLACK; use crate::draw::frame::{Frame, ImageBoundsSnapshot, UndoAction}; -use crate::draw::{EmbeddedImage, Shape, color::BLACK}; +use crate::draw::{EmbeddedImage, Shape}; fn rect_at(x: i32) -> Shape { Shape::Rect { @@ -26,7 +27,7 @@ fn undo_and_redo_cycle_shapes() { thick: 2.0, }; - let id = frame.add_shape(shape.clone()); + let id = frame.add_shape(shape); frame.push_undo_action( UndoAction::Create { shapes: vec![( diff --git a/src/draw/frame/tests/history/limits.rs b/src/draw/frame/tests/history/limits.rs index 60bb8260e..cb24122e4 100644 --- a/src/draw/frame/tests/history/limits.rs +++ b/src/draw/frame/tests/history/limits.rs @@ -1,5 +1,6 @@ +use crate::draw::Shape; +use crate::draw::color::BLACK; use crate::draw::frame::{DrawnShape, Frame, UndoAction}; -use crate::draw::{Shape, color::BLACK}; #[test] fn undo_stack_respects_limit() { @@ -128,7 +129,8 @@ fn limited_clone_matches_clamp_and_preserves_live_history() { #[test] #[ignore = "release measurement: cargo test --release limited_history_copy_cost -- --ignored --nocapture"] fn limited_history_copy_cost() { - use std::{hint::black_box, time::Instant}; + use std::hint::black_box; + use std::time::Instant; let mut frame = Frame::new(); let shape = Shape::Freehand { points: (0..4096) diff --git a/src/draw/frame/tests/history/prune.rs b/src/draw/frame/tests/history/prune.rs index c50e5c070..b895f95e2 100644 --- a/src/draw/frame/tests/history/prune.rs +++ b/src/draw/frame/tests/history/prune.rs @@ -1,7 +1,8 @@ use std::collections::HashSet; +use crate::draw::Shape; +use crate::draw::color::BLACK; use crate::draw::frame::{DrawnShape, Frame, ShapeSnapshot, UndoAction}; -use crate::draw::{Shape, color::BLACK}; #[test] fn prune_history_for_removed_ids_prunes_shapes_and_actions() { @@ -20,7 +21,7 @@ fn prune_history_for_removed_ids_prunes_shapes_and_actions() { let shape2 = DrawnShape::with_metadata(2, base_shape.clone(), 0, false); let create_both = UndoAction::Create { - shapes: vec![(0, shape1), (1, shape2.clone())], + shapes: vec![(0, shape1), (1, shape2)], }; let modify_second = UndoAction::Modify { diff --git a/src/draw/frame/tests/history/validate.rs b/src/draw/frame/tests/history/validate.rs index 99a8e1843..8b0f2710a 100644 --- a/src/draw/frame/tests/history/validate.rs +++ b/src/draw/frame/tests/history/validate.rs @@ -1,5 +1,6 @@ +use crate::draw::Shape; +use crate::draw::color::BLACK; use crate::draw::frame::{DrawnShape, Frame, UndoAction}; -use crate::draw::{Shape, color::BLACK}; #[test] fn validate_history_drops_actions_exceeding_compound_depth() { diff --git a/src/draw/frame/tests/serialization.rs b/src/draw/frame/tests/serialization.rs index af2b6fa77..0edd795a7 100644 --- a/src/draw/frame/tests/serialization.rs +++ b/src/draw/frame/tests/serialization.rs @@ -1,5 +1,7 @@ +use crate::base64; +use crate::draw::color::BLACK; use crate::draw::frame::{Frame, ImageBoundsSnapshot, UndoAction}; -use crate::draw::{EmbeddedImage, Shape, color::BLACK}; +use crate::draw::{EmbeddedImage, Shape}; #[test] fn frame_serializes_history() { @@ -127,7 +129,7 @@ fn try_add_shape_respects_limit() { #[test] fn image_bounds_history_serializes_without_duplicate_image_payloads() { let bytes = vec![42u8; 256]; - let encoded = crate::base64::encode_standard(&bytes); + let encoded = base64::encode_standard(&bytes); let mut frame = Frame::new(); let id = frame.add_shape(Shape::Image { x: 0, diff --git a/src/draw/frame/types.rs b/src/draw/frame/types.rs index 8d08272b8..abecfd1ec 100644 --- a/src/draw/frame/types.rs +++ b/src/draw/frame/types.rs @@ -110,6 +110,7 @@ pub struct ImageBoundsSnapshot { } impl ImageBoundsSnapshot { + #[must_use] pub fn from_shape(shape: &Shape, locked: bool) -> Option { match shape { Shape::Image { x, y, w, h, .. } => Some(Self { @@ -123,6 +124,7 @@ impl ImageBoundsSnapshot { } } + #[must_use] pub fn bounding_box(&self) -> Option { let min_x = if self.w < 0 { self.x.saturating_add(self.w) @@ -175,6 +177,7 @@ pub enum UndoAction { } impl UndoAction { + #[must_use] pub fn modify_from_snapshots( shape_id: ShapeId, before: ShapeSnapshot, @@ -216,6 +219,7 @@ pub struct HistoryTrimStats { } impl HistoryTrimStats { + #[must_use] pub fn is_empty(&self) -> bool { self.undo_removed == 0 && self.redo_removed == 0 } diff --git a/src/draw/mod.rs b/src/draw/mod.rs index 8d9ad53cb..f94177fdd 100644 --- a/src/draw/mod.rs +++ b/src/draw/mod.rs @@ -31,8 +31,6 @@ pub(crate) use font::{ pub use frame::{DrawnShape, Frame, ShapeId}; #[allow(unused_imports)] pub(crate) use render::render_eraser_stroke; -pub(crate) use render::render_sticky_note_preview_with_measurer; -pub(crate) use render::with_saved_state; #[allow(unused_imports)] pub use render::{ BlurRectParams, BoardPaper, EraserReplayContext, IMMUTABLE_RASTER_SOURCE_TOKEN, RenderCaches, @@ -48,7 +46,9 @@ pub use render::{ render_text_with_halo_with_measurer, render_text_with_measurer, selection_handle_rects, spotlight_regions_for_frame, sticky_note_foreground, text_outline_color, }; -pub(crate) use render::{LaserStyle, render_laser_stroke}; +pub(crate) use render::{ + LaserStyle, render_laser_stroke, render_sticky_note_preview_with_measurer, with_saved_state, +}; #[allow(unused_imports)] pub use shape::{ ArrowLabel, ArrowStyle, BlurStyle, EmbeddedImage, EraserBrush, EraserKind, MAX_PEN_SMOOTHING, diff --git a/src/draw/render/backdrop_probe.rs b/src/draw/render/backdrop_probe.rs index 216ffd548..ec55524a3 100644 --- a/src/draw/render/backdrop_probe.rs +++ b/src/draw/render/backdrop_probe.rs @@ -34,6 +34,7 @@ const MIN_OPAQUE_FRACTION: f64 = 0.5; /// Relative luminance of what is painted under `bounds`, in user-space /// coordinates, or `None` when too little of it is opaque to judge. +#[must_use] pub fn painted_luminance(ctx: &cairo::Context, bounds: (f64, f64, f64, f64)) -> Option { let (x, y, width, height) = bounds; if !(x.is_finite() && y.is_finite() && width.is_finite() && height.is_finite()) @@ -127,6 +128,7 @@ fn average_luminance(probe: &mut cairo::ImageSurface) -> Option { /// and answers a different question for the overlay chrome. The two disagree /// most on green, so a shared name for both would be a bug waiting to be /// written: keep the names apart and pick by what is being contrasted. +#[must_use] pub fn perceived_luminance(red: f64, green: f64, blue: f64) -> f64 { red * 0.299 + green * 0.587 + blue * 0.114 } diff --git a/src/draw/render/background.rs b/src/draw/render/background.rs index db1f588bb..a1dc11ee7 100644 --- a/src/draw/render/background.rs +++ b/src/draw/render/background.rs @@ -44,10 +44,10 @@ mod tests { use super::*; use crate::domain::Color; - fn rendered_bytes(background: BoardBackground) -> Vec { + fn rendered_bytes(background: &BoardBackground) -> Vec { let mut surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 2, 2).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); - render_board_background(&ctx, &background); + render_board_background(&ctx, background); drop(ctx); surface.flush(); surface.data().unwrap().to_vec() @@ -55,8 +55,8 @@ mod tests { #[test] fn transparent_background_does_not_paint_but_solid_background_does() { - let transparent = rendered_bytes(BoardBackground::Transparent); - let solid = rendered_bytes(BoardBackground::Solid(Color { + let transparent = rendered_bytes(&BoardBackground::Transparent); + let solid = rendered_bytes(&BoardBackground::Solid(Color { r: 1.0, g: 0.0, b: 0.0, diff --git a/src/draw/render/board_grid.rs b/src/draw/render/board_grid.rs index 6a19f9635..34b8876ea 100644 --- a/src/draw/render/board_grid.rs +++ b/src/draw/render/board_grid.rs @@ -1,5 +1,8 @@ //! Procedural board paper shared by canvas paint, eraser replay, and exports. +use std::f64::consts; +use std::ops::RangeInclusive; + use cairo::{Context, RecordingSurface, Rectangle, SurfacePattern}; use crate::domain::{BoardGrid, BoardGridKind, Color}; @@ -80,12 +83,14 @@ impl BoardPaper { }) } + #[must_use] pub fn pattern(&self) -> &cairo::Pattern { self.pattern.as_ref() } } /// Smallest target-axis scale, including Cairo surface device scaling. +#[must_use] pub fn board_paper_device_scale(ctx: &Context) -> f64 { let matrix = ctx.matrix(); let (dx, dy) = ctx.target().device_scale(); @@ -156,7 +161,7 @@ fn paint_geometry( result } -fn indices(min: f64, max: f64, spacing: f64) -> std::ops::RangeInclusive { +fn indices(min: f64, max: f64, spacing: f64) -> RangeInclusive { (min / spacing).floor() as i64..=(max / spacing).ceil() as i64 } @@ -200,7 +205,7 @@ fn isometric_dots(ctx: &Context, s: f64, b: Rectangle) { let shift = i.rem_euclid(2) as f64 * s / 2.0; for j in indices(b.y() - shift - 1.25, b.y() + b.height() - shift + 1.25, s) { ctx.new_sub_path(); - ctx.arc(x, j as f64 * s + shift, 1.25, 0.0, std::f64::consts::TAU); + ctx.arc(x, j as f64 * s + shift, 1.25, 0.0, consts::TAU); } } } diff --git a/src/draw/render/board_grid/performance.rs b/src/draw/render/board_grid/performance.rs index 0424dd44e..4d250ed5e 100644 --- a/src/draw/render/board_grid/performance.rs +++ b/src/draw/render/board_grid/performance.rs @@ -1,4 +1,8 @@ //! Opt-in source comparison; no compositor, font stack, or desktop windows. +use std::env; + +use crate::draw; + use super::*; use crate::draw::{EraserBrush, EraserKind, EraserReplayContext}; use std::time::Instant; @@ -36,7 +40,7 @@ fn render_case( let pattern = match source { Source::Solid => None, Source::Tile => Some( - BoardPaper::for_context(crate::draw::WHITE, grid, &ctx) + BoardPaper::for_context(draw::WHITE, grid, &ctx) .unwrap() .pattern, ), @@ -46,7 +50,7 @@ fn render_case( RecordingSurface::create(cairo::Content::ColorAlpha, Some(bounds)).unwrap(); paint_geometry( &Context::new(&record).unwrap(), - crate::draw::WHITE, + draw::WHITE, grid, 1.0, bounds, @@ -74,7 +78,7 @@ fn render_case( pattern: pattern.as_ref().map(|p| p.as_ref()), surface: None, backdrop_cache_key: None, - bg_color: Some(crate::draw::WHITE), + bg_color: Some(draw::WHITE), logical_to_image_scale_x: 1.0, logical_to_image_scale_y: 1.0, logical_image_origin_x: 0.0, @@ -117,7 +121,7 @@ fn render_case( #[test] #[ignore = "opt-in board-paper CPU/PDF comparison; run with --nocapture --test-threads=1"] fn board_grid_backdrop_performance() { - let sources: &[Source] = if std::env::var_os("WAYSCRIBER_GRID_COMPARE_VIEWPORT").is_some() { + let sources: &[Source] = if env::var_os("WAYSCRIBER_GRID_COMPARE_VIEWPORT").is_some() { &[Source::Solid, Source::Viewport, Source::Tile] } else { &[Source::Solid, Source::Tile] diff --git a/src/draw/render/board_grid/tests.rs b/src/draw/render/board_grid/tests.rs index 0830e6169..5b787a40f 100644 --- a/src/draw/render/board_grid/tests.rs +++ b/src/draw/render/board_grid/tests.rs @@ -1,3 +1,5 @@ +use crate::draw; + use super::*; use crate::draw::{EraserBrush, EraserKind, EraserReplayContext}; @@ -64,13 +66,13 @@ fn board_grid_paint_preserves_incoming_path_and_cairo_state() { let grid = BoardGrid::new(BoardGridKind::Isometric, 40); paint_geometry( &ctx, - crate::draw::WHITE, + draw::WHITE, grid, 1.0, Rectangle::new(0.0, 0.0, 80.0, 80.0), ) .unwrap(); - BoardPaper::for_context(crate::draw::WHITE, grid, &ctx) + BoardPaper::for_context(draw::WHITE, grid, &ctx) .unwrap() .paint(&ctx) .unwrap(); @@ -89,8 +91,8 @@ fn board_grid_eraser_replays_paper_instead_of_removing_lines_or_dots() { let mut surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 100, 100).unwrap(); let ctx = Context::new(&surface).unwrap(); ctx.translate(9.0, 7.0); - let paper = BoardPaper::for_context(crate::draw::WHITE, BoardGrid::new(kind, 20), &ctx) - .unwrap(); + let paper = + BoardPaper::for_context(draw::WHITE, BoardGrid::new(kind, 20), &ctx).unwrap(); paper.paint(&ctx).unwrap(); let original = { let mut copy = @@ -108,7 +110,7 @@ fn board_grid_eraser_replays_paper_instead_of_removing_lines_or_dots() { pattern: Some(paper.pattern()), surface: None, backdrop_cache_key: None, - bg_color: Some(crate::draw::WHITE), + bg_color: Some(draw::WHITE), logical_to_image_scale_x: 1.0, logical_to_image_scale_y: 1.0, logical_image_origin_x: 0.0, diff --git a/src/draw/render/context.rs b/src/draw/render/context.rs index 8b400ec3a..cb52828db 100644 --- a/src/draw/render/context.rs +++ b/src/draw/render/context.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::blur::{BlurRenderCache, render_blur_rect_with_cache}; use super::image::ImageSurfaceCache; use super::{BlurRectParams, EraserReplayContext}; @@ -16,37 +18,33 @@ pub struct RenderCaches { } /// A short drawing pass borrowing its target and persistent resources. -pub struct RenderCtx<'c, 'r> { - pub cairo: &'c cairo::Context, - pub caches: &'r mut RenderCaches, +pub struct RenderCtx<'cairo, 'caches> { + pub cairo: &'cairo cairo::Context, + pub caches: &'caches mut RenderCaches, } -impl<'c, 'r> RenderCtx<'c, 'r> { - pub fn new(cairo: &'c cairo::Context, caches: &'r mut RenderCaches) -> Self { +impl<'cairo, 'caches> RenderCtx<'cairo, 'caches> { + pub fn new(cairo: &'cairo cairo::Context, caches: &'caches mut RenderCaches) -> Self { Self { cairo, caches } } pub fn render_shape(&mut self, shape: &Shape) { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); self.render_shape_with_measurer(&measurer, shape); } - pub fn render_shape_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - shape: &Shape, - ) { + pub fn render_shape_with_measurer(&mut self, measurer: &TextMeasurer, shape: &Shape) { self.render_shape_with_halo_with_measurer(measurer, shape, true); } pub fn render_shape_with_halo(&mut self, shape: &Shape, text_halo_enabled: bool) { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); self.render_shape_with_halo_with_measurer(&measurer, shape, text_halo_enabled); } pub fn render_shape_with_halo_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, shape: &Shape, text_halo_enabled: bool, ) { @@ -54,13 +52,13 @@ impl<'c, 'r> RenderCtx<'c, 'r> { } pub fn render_shape_over(&mut self, shape: &Shape, known_background_luminance: Option) { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); self.render_shape_over_with_measurer(&measurer, shape, known_background_luminance); } pub fn render_shape_over_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, shape: &Shape, known_background_luminance: Option, ) { @@ -78,7 +76,7 @@ impl<'c, 'r> RenderCtx<'c, 'r> { known_background_luminance: Option, text_halo_enabled: bool, ) { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); self.render_shape_over_with_halo_with_measurer( &measurer, shape, @@ -89,7 +87,7 @@ impl<'c, 'r> RenderCtx<'c, 'r> { pub fn render_shape_over_with_halo_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, shape: &Shape, known_background_luminance: Option, text_halo_enabled: bool, diff --git a/src/draw/render/context/text_tests.rs b/src/draw/render/context/text_tests.rs index 30257e558..f15e3e905 100644 --- a/src/draw/render/context/text_tests.rs +++ b/src/draw/render/context/text_tests.rs @@ -1,3 +1,5 @@ +use crate::draw; + use super::{RenderCaches, RenderCtx}; use crate::draw::{ ArrowLabel, ArrowStyle, FontDescriptor, Frame, RED, Shape, StepMarkerLabel, TextMeasurer, @@ -78,10 +80,10 @@ fn pixels( for shape in &frame.shapes { if legacy { render.render_shape_with_halo(&shape.shape, halo); - crate::draw::render_selection_halo(&ctx, shape); + draw::render_selection_halo(&ctx, shape); } else { render.render_shape_with_halo_with_measurer(measurer, &shape.shape, halo); - crate::draw::render_selection_halo_with_measurer(measurer, &ctx, shape); + draw::render_selection_halo_with_measurer(measurer, &ctx, shape); } } assert_eq!(ctx.matrix(), before); diff --git a/src/draw/render/highlight.rs b/src/draw/render/highlight.rs index 85b04a31f..66f6f0f11 100644 --- a/src/draw/render/highlight.rs +++ b/src/draw/render/highlight.rs @@ -1,3 +1,5 @@ +use std::f64::consts; + use crate::draw::Color; /// Renders a circular click highlight with configurable fill/outline colors. @@ -27,7 +29,7 @@ pub fn render_click_highlight( fill_color.b, fill_color.a * alpha, ); - ctx.arc(center_x, center_y, radius, 0.0, std::f64::consts::PI * 2.0); + ctx.arc(center_x, center_y, radius, 0.0, consts::PI * 2.0); let _ = ctx.fill(); } @@ -39,7 +41,7 @@ pub fn render_click_highlight( outline_color.a * alpha, ); ctx.set_line_width(outline_thickness); - ctx.arc(center_x, center_y, radius, 0.0, std::f64::consts::PI * 2.0); + ctx.arc(center_x, center_y, radius, 0.0, consts::PI * 2.0); let _ = ctx.stroke(); } diff --git a/src/draw/render/image.rs b/src/draw/render/image.rs index 21a8fe5c9..2f0dc9169 100644 --- a/src/draw/render/image.rs +++ b/src/draw/render/image.rs @@ -124,7 +124,11 @@ impl ImageSurfaceCache { fn evict_if_needed(&mut self) { while self.entries.len() > self.max_entries || self.cached_bytes > self.max_bytes { let Some(oldest) = self.access_order.pop_front() else { - debug_assert!(self.entries.is_empty()); + debug_assert!( + self.entries.is_empty(), + "every cached image has an access-order slot, but {} entries have none", + self.entries.len() + ); self.cached_bytes = 0; break; }; @@ -260,12 +264,16 @@ fn render_missing_image_placeholder(ctx: &cairo::Context, x: i32, y: i32, w: i32 #[cfg(test)] mod tests { + use std::sync::Weak; + + use crate::draw::EmbeddedImage; + use super::{ImageBytesIdentity, ImageCacheKey, ImageSurfaceCache}; use cairo::{Format, ImageSurface}; use std::rc::Rc; use std::sync::Arc; - fn cache_key(marker: u8) -> (ImageCacheKey, std::sync::Weak<[u8]>) { + fn cache_key(marker: u8) -> (ImageCacheKey, Weak<[u8]>) { let bytes: Arc<[u8]> = vec![marker].into(); let weak = Arc::downgrade(&bytes); ( @@ -357,7 +365,7 @@ mod tests { fn decoded_cache_reuses_only_shared_payload_and_matching_metadata() { let mut bytes = Vec::new(); surface().write_to_png(&mut bytes).unwrap(); - let data = crate::draw::EmbeddedImage { + let data = EmbeddedImage { mime_type: "image/png".into(), width: 1, height: 1, @@ -367,13 +375,13 @@ mod tests { let first = super::cached_surface(&mut cache, &data).unwrap(); let shared = super::cached_surface(&mut cache, &data.clone()).unwrap(); assert!(Rc::ptr_eq(&first, &shared)); - let separate_payload = crate::draw::EmbeddedImage { + let separate_payload = EmbeddedImage { bytes: bytes.into(), ..data.clone() }; let distinct = super::cached_surface(&mut cache, &separate_payload).unwrap(); assert!(!Rc::ptr_eq(&first, &distinct)); - let different_metadata = crate::draw::EmbeddedImage { + let different_metadata = EmbeddedImage { mime_type: String::new(), ..data }; @@ -388,7 +396,7 @@ mod tests { .unwrap() .write_to_png(&mut bytes) .unwrap(); - let recorded = crate::draw::EmbeddedImage { + let recorded = EmbeddedImage { mime_type: "image/png".into(), width: 2, height: 2, @@ -396,7 +404,7 @@ mod tests { }; let mut cache = ImageSurfaceCache::default(); - let tampered = crate::draw::EmbeddedImage { + let tampered = EmbeddedImage { width: 1, ..recorded.clone() }; diff --git a/src/draw/render/mod.rs b/src/draw/render/mod.rs index af07637d6..d2c130bb7 100644 --- a/src/draw/render/mod.rs +++ b/src/draw/render/mod.rs @@ -2,8 +2,7 @@ mod backdrop_probe; pub(crate) use backdrop_probe::color_luminance; -pub use backdrop_probe::painted_luminance as painted_background_luminance; -pub use backdrop_probe::perceived_luminance; +pub use backdrop_probe::{painted_luminance as painted_background_luminance, perceived_luminance}; mod background; mod blur; mod board_grid; diff --git a/src/draw/render/pressure_strokes.rs b/src/draw/render/pressure_strokes.rs index 797e7c6d0..3ac4854e0 100644 --- a/src/draw/render/pressure_strokes.rs +++ b/src/draw/render/pressure_strokes.rs @@ -1,3 +1,5 @@ +use std::f64::consts; + use crate::draw::Color; const PRESSURE_STROKE_MAX_SAMPLE_SPACING: f64 = 4.0; @@ -162,13 +164,7 @@ fn fill_pressure_segment(ctx: &cairo::Context, segment: &PressureStrokeSegment) fn fill_pressure_sample(ctx: &cairo::Context, sample: &PressureStrokeSample) { ctx.new_path(); - ctx.arc( - sample.x, - sample.y, - sample.radius, - 0.0, - std::f64::consts::PI * 2.0, - ); + ctx.arc(sample.x, sample.y, sample.radius, 0.0, consts::PI * 2.0); ctx.fill().ok(); } diff --git a/src/draw/render/primitives.rs b/src/draw/render/primitives.rs index 8c2a502b2..acc8b61c3 100644 --- a/src/draw/render/primitives.rs +++ b/src/draw/render/primitives.rs @@ -1,4 +1,6 @@ -use crate::draw::{ArrowStyle, Color}; +use std::f64::consts; + +use crate::draw::{ArrowStyle, Color, shape}; use crate::util; /// Render a straight line @@ -79,7 +81,7 @@ pub(super) fn render_ellipse( ctx.translate(cx as f64, cy as f64); ctx.scale(rx as f64, ry as f64); ctx.new_sub_path(); - ctx.arc(0.0, 0.0, 1.0, 0.0, 2.0 * std::f64::consts::PI); + ctx.arc(0.0, 0.0, 1.0, 0.0, 2.0 * consts::PI); fill_path(ctx, fill); ctx.restore().ok(); @@ -95,7 +97,7 @@ pub(super) fn render_polygon( color: Color, thick: f64, ) { - if !crate::draw::shape::has_minimum_distinct_points(points) { + if !shape::has_minimum_distinct_points(points) { return; } @@ -138,7 +140,7 @@ pub(crate) fn render_polygon_preview( for &(x, y) in &points[1..] { ctx.line_to(x as f64, y as f64); } - if crate::draw::shape::has_minimum_distinct_points(points) { + if shape::has_minimum_distinct_points(points) { ctx.close_path(); if fill { let _ = ctx.fill_preserve(); @@ -226,6 +228,9 @@ where #[cfg(test)] mod tests { + use crate::draw; + use crate::draw::Shape; + use super::*; use cairo::{Context, ImageSurface}; @@ -563,9 +568,9 @@ mod tests { let (mut surface, ctx) = surface_with_context(60, 60); let red = Color::new(1.0, 0.0, 0.0, 1.0); let blue = Color::new(0.0, 0.0, 1.0, 1.0); - crate::draw::render_shape( + draw::render_shape( &ctx, - &crate::draw::Shape::Rect { + &Shape::Rect { x: 10, y: 10, w: 40, diff --git a/src/draw/render/selection.rs b/src/draw/render/selection.rs index 807d8b275..bd81ba3a0 100644 --- a/src/draw/render/selection.rs +++ b/src/draw/render/selection.rs @@ -1,3 +1,6 @@ +use crate::draw; +use crate::draw::TextMeasurer; + use super::highlight::render_click_highlight; use super::primitives::{render_arrow, render_ellipse, render_line, render_polygon, render_rect}; use super::spotlight::{SpotlightRegion, render_spotlight_outline}; @@ -30,12 +33,12 @@ const SELECTION_GLOW: Color = Color { /// Renders a selection halo overlay for a drawn shape. pub fn render_selection_halo(ctx: &cairo::Context, drawn: &DrawnShape) { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); render_selection_halo_with_measurer(&measurer, ctx, drawn); } pub fn render_selection_halo_with_measurer( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ctx: &cairo::Context, drawn: &DrawnShape, ) { @@ -112,7 +115,7 @@ pub fn render_selection_halo_with_measurer( cy: f64::from(*cy), rx: f64::from(*rx), ry: f64::from(*ry), - magnification: crate::draw::DEFAULT_SPOTLIGHT_MAGNIFICATION, + magnification: draw::DEFAULT_SPOTLIGHT_MAGNIFICATION, }, glow, outline_width, @@ -312,6 +315,7 @@ pub fn render_selection_handles(ctx: &cairo::Context, bounds: &Rect) { /// Computes selection handle rectangles for hit testing. /// Returns handles in order: TL, TR, BL, BR, Top, Bottom, Left, Right #[allow(dead_code)] +#[must_use] pub fn selection_handle_rects(bounds: &Rect) -> [Rect; 8] { let x = bounds.x; let y = bounds.y; diff --git a/src/draw/render/shapes.rs b/src/draw/render/shapes.rs index 66ffe536f..23dac5ca6 100644 --- a/src/draw/render/shapes.rs +++ b/src/draw/render/shapes.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::blur::{render_black_out_rect, render_blur_placeholder}; use super::highlight::render_click_highlight; use super::image::render_image_shape; @@ -17,7 +19,7 @@ struct ShapeTextOptions { halo_enabled: bool, } -struct ArrowRenderSpec<'a> { +struct ArrowRenderSpec<'label> { start: (i32, i32), end: (i32, i32), color: Color, @@ -27,13 +29,13 @@ struct ArrowRenderSpec<'a> { head_at_end: bool, style: ArrowStyle, bend: f64, - label: Option<&'a ArrowLabel>, + label: Option<&'label ArrowLabel>, } -struct StepMarkerRenderSpec<'a> { +struct StepMarkerRenderSpec<'label> { center: (i32, i32), color: Color, - label: &'a StepMarkerLabel, + label: &'label StepMarkerLabel, } /// Renders a single shape to a Cairo context. @@ -83,7 +85,7 @@ pub fn render_shape_over_with_halo( } pub(super) fn render_shape_with_cache( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, images: &mut super::image::ImageSurfaceCache, ctx: &cairo::Context, shape: &Shape, @@ -169,7 +171,7 @@ pub(super) fn render_shape_with_cache( render_arrow_shape( measurer, ctx, - ArrowRenderSpec { + &ArrowRenderSpec { start: (*x1, *y1), end: (*x2, *y2), color: *color, @@ -233,7 +235,7 @@ pub(super) fn render_shape_with_cache( render_step_marker_shape( measurer, ctx, - StepMarkerRenderSpec { + &StepMarkerRenderSpec { center: (*x, *y), color: *color, label, @@ -279,9 +281,9 @@ pub(super) fn render_shape_with_cache( } fn render_arrow_shape( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ctx: &cairo::Context, - arrow: ArrowRenderSpec<'_>, + arrow: &ArrowRenderSpec<'_>, text: ShapeTextOptions, ) { // Only the label needs these: `render_arrow` reads `head_at_end` itself. @@ -344,9 +346,9 @@ fn render_arrow_shape( } fn render_step_marker_shape( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ctx: &cairo::Context, - marker: StepMarkerRenderSpec<'_>, + marker: &StepMarkerRenderSpec<'_>, text: ShapeTextOptions, ) { let label_text = marker.label.value.to_string(); diff --git a/src/draw/render/spotlight.rs b/src/draw/render/spotlight.rs index a444b8577..5cc7eb5b8 100644 --- a/src/draw/render/spotlight.rs +++ b/src/draw/render/spotlight.rs @@ -10,6 +10,11 @@ //! last also means the committed canvas dims as a whole, while the live preview, //! selection handles, click highlights, and UI drawn afterwards stay bright. +use std::f64::consts; +use std::time::{Duration, Instant}; + +use crate::draw; +use crate::draw::{Color, Frame, Shape}; use crate::i18n::tr; /// One elliptical opening left bright by the pass, in logical canvas coordinates. @@ -66,6 +71,7 @@ impl SpotlightMagnifierSource { /// A raster backdrop whose provenance has gone stale passes `None` here and /// degrades to the solid/transparent answer, which is what keeps a stale /// capture from being magnified as though it were live. + #[must_use] pub const fn from_backdrop(raster_token: Option, has_solid_color: bool) -> Self { match raster_token { Some(source_token) => Self::CompleteRaster { source_token }, @@ -75,6 +81,7 @@ impl SpotlightMagnifierSource { } /// Availability for a backdrop whose pixels cannot change under it. + #[must_use] pub const fn immutable_raster() -> Self { Self::CompleteRaster { source_token: IMMUTABLE_RASTER_SOURCE_TOKEN, @@ -82,11 +89,13 @@ impl SpotlightMagnifierSource { } /// Whether the loupe has every pixel it needs to magnify faithfully. + #[must_use] pub const fn is_complete(self) -> bool { !matches!(self, Self::IncompleteTransparent) } /// Provenance identity of the raster backdrop, when there is one. + #[must_use] pub const fn raster_token(self) -> Option { match self { Self::CompleteRaster { source_token } => Some(source_token), @@ -95,6 +104,7 @@ impl SpotlightMagnifierSource { } /// Stable, user-facing reason a loupe cannot preview right now. + #[must_use] pub fn unavailable_reason(self) -> Option<&'static str> { match self { Self::IncompleteTransparent => Some(tr("Freeze screen to preview")), @@ -114,8 +124,8 @@ pub struct SpotlightMagnifierMetrics { pub regions: usize, pub copied_pixels: u64, pub strategy: SpotlightSnapshotStrategy, - pub snapshot_time: std::time::Duration, - pub paint_time: std::time::Duration, + pub snapshot_time: Duration, + pub paint_time: Duration, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -195,7 +205,7 @@ pub fn render_spotlight_magnification_pass( let has_magnification = regions .iter() - .any(|region| crate::draw::spotlight_magnification_is_active(region.magnification)); + .any(|region| draw::spotlight_magnification_is_active(region.magnification)); if !has_magnification { return Ok(SpotlightMagnifierOutcome::NotNeeded); } @@ -207,9 +217,8 @@ pub fn render_spotlight_magnification_pass( .iter() .copied() .filter_map(|mut region| { - region.magnification = - crate::draw::normalize_spotlight_magnification(region.magnification); - crate::draw::spotlight_magnification_is_active(region.magnification) + region.magnification = draw::normalize_spotlight_magnification(region.magnification); + draw::spotlight_magnification_is_active(region.magnification) .then(|| device_rect_for_region(ctx, region, target_width, target_height)) .flatten() .map(|rect| (region, rect)) @@ -235,7 +244,7 @@ pub fn render_spotlight_magnification_pass( SpotlightSnapshotStrategy::FullSurface }; - let snapshot_start = std::time::Instant::now(); + let snapshot_start = Instant::now(); target.flush(); let snapshots = match strategy { SpotlightSnapshotStrategy::Regional => { @@ -286,7 +295,7 @@ pub fn render_spotlight_magnification_pass( }; let snapshot_time = snapshot_start.elapsed(); - let paint_start = std::time::Instant::now(); + let paint_start = Instant::now(); for snapshot in &snapshots { paint_snapshot(ctx, snapshot, feather)?; } @@ -438,19 +447,19 @@ fn paint_snapshot( return Ok(()); } - crate::draw::with_saved_state(ctx, || { + draw::with_saved_state(ctx, || { // Establish the ellipse under the caller's transform. Cairo stores the // resulting clip in device space, so sampling can then use an identity CTM. ctx.new_path(); ctx.save()?; ctx.translate(region.cx, region.cy); ctx.scale(rx, ry); - ctx.arc(0.0, 0.0, 1.0, 0.0, std::f64::consts::TAU); + ctx.arc(0.0, 0.0, 1.0, 0.0, consts::TAU); ctx.restore()?; ctx.clip(); ctx.identity_matrix(); - let magnification = crate::draw::normalize_spotlight_magnification(region.magnification); + let magnification = draw::normalize_spotlight_magnification(region.magnification); let pattern = cairo::SurfacePattern::create(&snapshot.surface); pattern.set_extend(cairo::Extend::Pad); pattern.set_filter(cairo::Filter::Bilinear); @@ -495,12 +504,13 @@ fn paint_snapshot( /// /// The pass needs all regions at once, so each surface that renders a frame — /// the live canvas, exports, and board thumbnails — collects them the same way. -pub fn spotlight_regions_for_frame(frame: &crate::draw::Frame) -> Vec { +#[must_use] +pub fn spotlight_regions_for_frame(frame: &Frame) -> Vec { frame .shapes .iter() .filter_map(|drawn| match &drawn.shape { - crate::draw::Shape::Spotlight { + Shape::Spotlight { cx, cy, rx, @@ -511,7 +521,7 @@ pub fn spotlight_regions_for_frame(frame: &crate::draw::Frame) -> Vec None, }) @@ -573,7 +583,7 @@ fn punch_opening(ctx: &cairo::Context, region: SpotlightRegion, feather: f64) { let _ = ctx.set_source(&gradient); ctx.new_sub_path(); - ctx.arc(0.0, 0.0, 1.0, 0.0, std::f64::consts::TAU); + ctx.arc(0.0, 0.0, 1.0, 0.0, consts::TAU); let _ = ctx.fill(); let _ = ctx.restore(); } @@ -582,7 +592,7 @@ fn punch_opening(ctx: &cairo::Context, region: SpotlightRegion, feather: f64) { pub fn render_spotlight_outline( ctx: &cairo::Context, region: SpotlightRegion, - color: crate::draw::Color, + color: Color, thick: f64, ) { let rx = region.rx.max(1.0); @@ -597,7 +607,7 @@ pub fn render_spotlight_outline( ctx.translate(region.cx, region.cy); ctx.scale(rx, ry); ctx.new_sub_path(); - ctx.arc(0.0, 0.0, 1.0, 0.0, std::f64::consts::TAU); + ctx.arc(0.0, 0.0, 1.0, 0.0, consts::TAU); let _ = ctx.restore(); ctx.set_source_rgba(color.r, color.g, color.b, color.a); @@ -887,7 +897,7 @@ mod tests { render_spotlight_outline( &ctx, CENTERED, - crate::draw::Color { + Color { r: 1.0, g: 0.0, b: 0.0, @@ -915,7 +925,7 @@ mod tests { render_spotlight_outline( &ctx, CENTERED, - crate::draw::Color { + Color { r: 1.0, g: 1.0, b: 1.0, diff --git a/src/draw/render/strokes.rs b/src/draw/render/strokes.rs index 053470c07..f5422c59e 100644 --- a/src/draw/render/strokes.rs +++ b/src/draw/render/strokes.rs @@ -1,3 +1,5 @@ +use std::f64::consts; + use super::types::EraserReplayContext; use crate::draw::Color; use crate::draw::shape::{EraserBrush, EraserKind}; @@ -54,7 +56,7 @@ pub(crate) fn render_eraser_stroke( let (x, y) = (points[0].0 as f64, points[0].1 as f64); let half = stroke_width / 2.0; match brush.kind { - EraserKind::Circle => ctx.arc(x, y, half, 0.0, std::f64::consts::PI * 2.0), + EraserKind::Circle => ctx.arc(x, y, half, 0.0, consts::PI * 2.0), EraserKind::Rect => ctx.rectangle(x - half, y - half, stroke_width, stroke_width), } return; diff --git a/src/draw/render/text.rs b/src/draw/render/text.rs index c5da7ab7a..c5a7c1462 100644 --- a/src/draw/render/text.rs +++ b/src/draw/render/text.rs @@ -1,3 +1,7 @@ +use pangocairo::functions; + +use crate::draw::TextMeasurer; + use super::backdrop_probe; use crate::draw::shape::{ TextMeasurement, sticky_note_layout, sticky_note_layout_text, @@ -37,7 +41,7 @@ pub fn render_text( background_enabled: bool, wrap_width: Option, ) { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); render_text_with_measurer( &measurer, ctx, @@ -54,7 +58,7 @@ pub fn render_text( #[allow(clippy::too_many_arguments)] pub fn render_text_with_measurer( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ctx: &cairo::Context, x: i32, y: i32, @@ -94,7 +98,7 @@ pub fn render_text_with_halo( wrap_width: Option, halo_enabled: bool, ) { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); render_text_with_halo_with_measurer( &measurer, ctx, @@ -112,7 +116,7 @@ pub fn render_text_with_halo( #[allow(clippy::too_many_arguments)] pub fn render_text_with_halo_with_measurer( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ctx: &cairo::Context, x: i32, y: i32, @@ -160,7 +164,7 @@ pub fn render_text_over( wrap_width: Option, known_background_luminance: Option, ) { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); render_text_over_with_measurer( &measurer, ctx, @@ -178,7 +182,7 @@ pub fn render_text_over( #[allow(clippy::too_many_arguments)] pub fn render_text_over_with_measurer( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ctx: &cairo::Context, x: i32, y: i32, @@ -221,7 +225,7 @@ pub fn render_text_over_with_halo( known_background_luminance: Option, halo_enabled: bool, ) { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); render_text_over_with_halo_with_measurer( &measurer, ctx, @@ -240,7 +244,7 @@ pub fn render_text_over_with_halo( #[allow(clippy::too_many_arguments)] pub fn render_text_over_with_halo_with_measurer( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ctx: &cairo::Context, x: i32, y: i32, @@ -261,7 +265,7 @@ pub fn render_text_over_with_halo_with_measurer( ctx.set_antialias(cairo::Antialias::Best); // Create Pango layout for text rendering - let layout = pangocairo::functions::create_layout(ctx); + let layout = functions::create_layout(ctx); // Set font description from config; the string form is the cache key. let font_desc = font_descriptor.to_pango_description(size); @@ -319,8 +323,11 @@ pub fn render_text_over_with_halo_with_measurer( }); // First pass: draw semi-transparent background rectangle (if enabled) - if background_enabled && content.width > 0.0 && content.height > 0.0 { - let contrast = contrast.expect("text background requests a contrast color"); + if background_enabled + && content.width > 0.0 + && content.height > 0.0 + && let Some(contrast) = contrast + { let padding = size * 0.15; // Union ink and logical extents: ink preserves italic overhangs while // logical cells retain leading/trailing whitespace advances. @@ -338,13 +345,13 @@ pub fn render_text_over_with_halo_with_measurer( let shadow_offset = size * 0.04; ctx.move_to(x as f64 + shadow_offset, adjusted_y + shadow_offset); ctx.set_source_rgba(0.0, 0.0, 0.0, 0.4); - pangocairo::functions::show_layout(ctx, &layout); + functions::show_layout(ctx, &layout); // Third pass: render text with contrasting stroke outline ctx.move_to(x as f64, adjusted_y); // Create path from layout for stroking - pangocairo::functions::layout_path(ctx, &layout); + functions::layout_path(ctx, &layout); if let Some(outline) = contrast.filter(|_| halo_enabled) { // Fully opaque stroke for maximum contrast and crispness. @@ -363,6 +370,7 @@ pub fn render_text_over_with_halo_with_measurer( } /// Stroke width of the editor's standalone caret line at `size`. +#[must_use] pub fn caret_line_width(size: f64) -> f64 { (size * 0.06).max(1.5) } @@ -372,6 +380,7 @@ pub fn caret_line_width(size: f64) -> f64 { /// to either side. The damage tracker sizes the caret's repaint rectangle from /// this, which is why the value lives here next to the code that paints it — /// duplicating the constant is how the caret used to leave a trail on drags. +#[must_use] pub fn caret_outline_width(size: f64) -> f64 { caret_line_width(size) + 2.0 } @@ -388,6 +397,7 @@ pub fn caret_outline_width(size: f64) -> f64 { /// including on a white page, which is the exact case a halo prevents. It stays /// as the fallback only because a transparent overlay over a live desktop has no /// background pixels to read, and a guess from the text color beats no halo. +#[must_use] pub fn text_outline_color(color: Color, background_luminance: Option) -> Color { let brightness = background_luminance.unwrap_or_else(|| backdrop_probe::color_luminance(color)); if brightness > 0.5 { @@ -409,7 +419,7 @@ pub fn render_sticky_note( font_descriptor: &FontDescriptor, wrap_width: Option, ) { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); render_sticky_note_with_measurer( &measurer, ctx, @@ -425,7 +435,7 @@ pub fn render_sticky_note( #[allow(clippy::too_many_arguments)] pub fn render_sticky_note_with_measurer( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ctx: &cairo::Context, x: i32, y: i32, @@ -456,7 +466,7 @@ pub fn render_sticky_note_with_measurer( /// when its buffer is empty so the background remains visible behind the caret. #[allow(clippy::too_many_arguments)] pub(crate) fn render_sticky_note_preview_with_measurer( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ctx: &cairo::Context, x: i32, y: i32, @@ -482,7 +492,7 @@ pub(crate) fn render_sticky_note_preview_with_measurer( #[allow(clippy::too_many_arguments)] fn render_sticky_note_layout( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ctx: &cairo::Context, x: i32, y: i32, @@ -543,7 +553,7 @@ fn render_sticky_note_layout( let fg = sticky_note_foreground(background); ctx.move_to(base_x, base_y); ctx.set_source_rgba(fg.r, fg.g, fg.b, fg.a); - pangocairo::functions::show_layout(ctx, &text_layout.layout); + functions::show_layout(ctx, &text_layout.layout); } ctx.restore().ok(); @@ -553,6 +563,7 @@ fn render_sticky_note_layout( /// background: near-black on light notes, near-white on dark ones. Shared so the /// live caret matches the committed note text instead of vanishing into the /// background fill. +#[must_use] pub fn sticky_note_foreground(background: Color) -> Color { let brightness = super::color_luminance(background); if brightness > 0.6 { @@ -584,6 +595,8 @@ fn draw_round_rect(ctx: &cairo::Context, x: f64, y: f64, w: f64, h: f64, r: f64) #[cfg(test)] mod tests { + use crate::draw::{TextMeasurer, shape}; + use super::{ Color, FontDescriptor, caret_outline_width, render_sticky_note, render_sticky_note_preview_with_measurer, render_text, render_text_with_halo, @@ -702,7 +715,7 @@ mod tests { #[test] fn disabled_halo_keeps_the_optional_text_background() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let text = "A "; let font = FontDescriptor::default(); let size = 20.0; @@ -777,7 +790,7 @@ mod tests { #[test] fn empty_sticky_note_still_draws_its_preview_background() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 100, 100).unwrap(); { let ctx = cairo::Context::new(&surface).unwrap(); @@ -825,7 +838,7 @@ mod tests { #[test] fn live_plain_text_background_covers_a_trailing_space_caret() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let text = "A "; let font = FontDescriptor::default(); let size = 20.0; @@ -859,7 +872,7 @@ mod tests { #[test] fn live_sticky_note_background_covers_a_trailing_space_caret() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let text = "A "; let font = FontDescriptor::default(); let size = 20.0; @@ -944,8 +957,8 @@ mod tests { } let (min_x, min_y, max_x, max_y) = painted_extents(&mut surface).expect("text paints something"); - let bounds = crate::draw::shape::bounding_box_for_text_with( - &crate::draw::TextMeasurer::default(), + let bounds = shape::bounding_box_for_text_with( + &TextMeasurer::default(), origin.0, origin.1, text, @@ -968,7 +981,7 @@ mod tests { #[test] fn caret_stroke_stays_within_its_advertised_width() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); // The damage tracker sizes the caret's repaint from `caret_outline_width`; // the stroke is centred, so it must not reach further than half of it. let font = FontDescriptor::default(); diff --git a/src/draw/render/types.rs b/src/draw/render/types.rs index 811899942..bceb2b2ed 100644 --- a/src/draw/render/types.rs +++ b/src/draw/render/types.rs @@ -1,11 +1,11 @@ use crate::draw::Color; /// Background replay context for tools that need access to the captured backdrop. -pub struct EraserReplayContext<'a> { +pub struct EraserReplayContext<'backdrop> { /// Optional pattern representing the current background (e.g., frozen image) in device space. - pub pattern: Option<&'a cairo::Pattern>, + pub pattern: Option<&'backdrop cairo::Pattern>, /// Optional surface representing the captured background image. - pub surface: Option<&'a cairo::ImageSurface>, + pub surface: Option<&'backdrop cairo::ImageSurface>, /// Stable cache key for the currently active backdrop image generation. pub backdrop_cache_key: Option, /// Solid background color (board modes) when no pattern is available. diff --git a/src/draw/shape/arrow_label.rs b/src/draw/shape/arrow_label.rs index 147b4f4aa..6446f0b92 100644 --- a/src/draw/shape/arrow_label.rs +++ b/src/draw/shape/arrow_label.rs @@ -1,4 +1,5 @@ use crate::draw::{ArrowStyle, FontDescriptor}; +use crate::util; use crate::util::Rect; use super::text::{text_bounds_from_metrics, text_layout_metrics}; @@ -84,7 +85,7 @@ pub(crate) fn arrow_label_layout_with( let offset = (label_size * LABEL_OFFSET_SCALE).max(LABEL_OFFSET_MIN) + thick * LABEL_THICKNESS_SCALE; - let bend = crate::util::clamp_arrow_bend(bend); + let bend = util::clamp_arrow_bend(bend); // Left normal of the tail-to-tip direction, matching the convention the // arc's control point is offset along. let (left_x, left_y) = (uy, -ux); diff --git a/src/draw/shape/bounds.rs b/src/draw/shape/bounds.rs index 04a6b103a..19a302a9f 100644 --- a/src/draw/shape/bounds.rs +++ b/src/draw/shape/bounds.rs @@ -174,12 +174,12 @@ pub(crate) fn bounding_box_for_arrow_with( bend, ) { let heads = [Some(skeleton.head), skeleton.tail_head]; - for corner in heads + let corners = heads .iter() .flatten() .flat_map(|head| [head.tip, head.left, head.right]) - .chain(skeleton.spine.points().iter().copied()) - { + .chain(skeleton.spine.points().iter().copied()); + for corner in corners { min_x = min_x.min(corner.0); max_x = max_x.max(corner.0); min_y = min_y.min(corner.1); diff --git a/src/draw/shape/polygon.rs b/src/draw/shape/polygon.rs index 51348563a..71528f30a 100644 --- a/src/draw/shape/polygon.rs +++ b/src/draw/shape/polygon.rs @@ -1,3 +1,5 @@ +use std::f64::consts; + use crate::i18n::tr; use crate::util::Rect; use serde::{Deserialize, Serialize}; @@ -20,6 +22,7 @@ pub enum PolygonKind { } impl PolygonKind { + #[must_use] pub fn label(self) -> &'static str { match self { Self::Triangle => tr("Triangle"), @@ -52,6 +55,7 @@ impl PolygonTemplate { } } +#[must_use] pub fn clamp_regular_sides(sides: u8) -> u8 { sides.clamp(REGULAR_POLYGON_MIN_SIDES, REGULAR_POLYGON_MAX_SIDES) } @@ -150,11 +154,11 @@ fn regular_polygon_points(start: (i32, i32), end: (i32, i32), sides: u8) -> Vec< let radius_x = f64::from(end.0.abs_diff(start.0)) / 2.0; let radius_y = f64::from(end.1.abs_diff(start.1)) / 2.0; let radius = radius_x.min(radius_y); - let start_angle = -std::f64::consts::FRAC_PI_2; + let start_angle = -consts::FRAC_PI_2; (0..sides) .map(|index| { - let angle = start_angle + std::f64::consts::TAU * f64::from(index) / f64::from(sides); + let angle = start_angle + consts::TAU * f64::from(index) / f64::from(sides); ( (center_x + angle.cos() * radius).round() as i32, (center_y + angle.sin() * radius).round() as i32, diff --git a/src/draw/shape/smoothing.rs b/src/draw/shape/smoothing.rs index ccebba3fb..17d2bc5a6 100644 --- a/src/draw/shape/smoothing.rs +++ b/src/draw/shape/smoothing.rs @@ -24,6 +24,8 @@ //! started and stopped. A filter that moved them would pull an underline off the //! word it began under. +use std::mem; + /// Highest smoothing level. Past roughly this many passes a stroke stops /// following its own corners and reads as a different line from the one drawn. pub const MAX_PEN_SMOOTHING: u8 = 6; @@ -52,11 +54,13 @@ const MIN_SMOOTHABLE_POINTS: usize = 3; const CENTER_WEIGHT: f64 = 0.5; /// Clamp a configured or stepped level into range. +#[must_use] pub fn clamp_pen_smoothing(level: u8) -> u8 { level.min(MAX_PEN_SMOOTHING) } /// Smooth an `(x, y)` path. Level 0 returns the path unchanged. +#[must_use] pub fn smooth_path(points: &[(f64, f64)], level: u8) -> Vec<(f64, f64)> { let Some(smoothed) = smooth_points(points, level, |&(x, y)| (x, y)) else { return points.to_vec(); @@ -87,6 +91,7 @@ pub(crate) fn smooth_committed_path(points: &[(f64, f64)], level: u8) -> Vec<(f6 /// tablet rather than from the hand's aim, and averaging them would erase real /// detail while fixing nothing. Each smoothed position keeps the thickness that /// was sampled with it. +#[must_use] pub fn smooth_pressure_path(points: &[(f64, f64, f32)], level: u8) -> Vec<(f64, f64, f32)> { let Some(smoothed) = smooth_points(points, level, |&(x, y, _)| (x, y)) else { return points.to_vec(); @@ -135,7 +140,7 @@ fn smooth_points( + following.1 * neighbor_weight, ); } - std::mem::swap(&mut current, &mut next); + mem::swap(&mut current, &mut next); } Some(current) } diff --git a/src/draw/shape/step_marker.rs b/src/draw/shape/step_marker.rs index cc4a5d8d8..03de9e698 100644 --- a/src/draw/shape/step_marker.rs +++ b/src/draw/shape/step_marker.rs @@ -49,6 +49,8 @@ pub(crate) fn step_marker_bounds_with( #[cfg(test)] mod tests { + use crate::draw; + use super::*; #[test] @@ -59,7 +61,7 @@ mod tests { #[test] fn step_marker_radius_grows_with_font_size() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = draw::TextMeasurer::default(); let font = FontDescriptor::default(); assert!( step_marker_radius_with(&measurer, 1, 32.0, &font) @@ -69,7 +71,7 @@ mod tests { #[test] fn step_marker_radius_grows_for_multi_digit_labels() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = draw::TextMeasurer::default(); let font = FontDescriptor::default(); assert!( step_marker_radius_with(&measurer, 88, 18.0, &font) diff --git a/src/draw/shape/stroke_points.rs b/src/draw/shape/stroke_points.rs index 62d08c6b1..0e09da80e 100644 --- a/src/draw/shape/stroke_points.rs +++ b/src/draw/shape/stroke_points.rs @@ -1,6 +1,6 @@ //! Floating stroke coordinates accept legacy JSON integers and validate both directions. -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de, ser}; fn valid(x: f64, y: f64) -> bool { [x, y] @@ -13,7 +13,7 @@ pub(super) fn serialize( serializer: S, ) -> Result { if points.iter().any(|&(x, y)| !valid(x, y)) { - return Err(serde::ser::Error::custom("invalid stroke coordinate")); + return Err(ser::Error::custom("invalid stroke coordinate")); } points.serialize(serializer) } @@ -23,7 +23,7 @@ pub(super) fn deserialize<'de, D: Deserializer<'de>>( ) -> Result, D::Error> { let points = Vec::<(f64, f64)>::deserialize(deserializer)?; if points.iter().any(|&(x, y)| !valid(x, y)) { - return Err(serde::de::Error::custom("invalid stroke coordinate")); + return Err(de::Error::custom("invalid stroke coordinate")); } Ok(points) } @@ -36,7 +36,7 @@ pub(super) fn serialize_pressure( .iter() .any(|&(x, y, width)| !valid(x, y) || !width.is_finite()) { - return Err(serde::ser::Error::custom("invalid pressure stroke sample")); + return Err(ser::Error::custom("invalid pressure stroke sample")); } points.serialize(serializer) } @@ -49,14 +49,15 @@ pub(super) fn deserialize_pressure<'de, D: Deserializer<'de>>( .iter() .any(|&(x, y, width)| !valid(x, y) || !width.is_finite()) { - return Err(serde::de::Error::custom("invalid pressure stroke sample")); + return Err(de::Error::custom("invalid pressure stroke sample")); } Ok(points) } #[cfg(test)] mod tests { - use crate::draw::{Shape, color::BLACK}; + use crate::draw::Shape; + use crate::draw::color::BLACK; #[test] fn fractional_ink_survives_selection_transforms_and_serialization() { diff --git a/src/draw/shape/tests.rs b/src/draw/shape/tests.rs index 8938f05c4..45c57ee9a 100644 --- a/src/draw/shape/tests.rs +++ b/src/draw/shape/tests.rs @@ -1,8 +1,12 @@ +use std::sync::Arc; + +use crate::draw::{Color, TextMeasurer}; + use super::types::Shape; use super::{EmbeddedImage, EraserBrush}; +use crate::draw::color::{BLACK, WHITE}; use crate::draw::{ ArrowLabel, ArrowStyle, EraserKind, FontDescriptor, PolygonKind, StepMarkerLabel, - color::{BLACK, WHITE}, }; use crate::util; @@ -212,7 +216,7 @@ fn double_arrow_bounds_contain_the_second_head() { #[test] fn arrow_label_layout_offsets_from_line() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let font = FontDescriptor::default(); let layout = super::arrow_label_layout_with(&measurer, 100, 0, 0, 0, 2.0, 0.0, "1", 12.0, &font) @@ -502,7 +506,7 @@ fn pressure_and_image_bounds_handle_extreme_coordinates() { #[test] fn curved_arrow_label_follows_the_arc_not_the_chord() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); // Anchored to the chord, the label would sit in the gap the arrow was drawn // to route around - far from the shaft it numbers. let font = FontDescriptor::default(); @@ -577,7 +581,7 @@ fn flipping_the_head_still_moves_a_single_headed_arrow_label() { #[test] fn arrow_label_layout_handles_full_span_endpoints() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let font = FontDescriptor::default(); let layout = super::arrow_label_layout_with( &measurer, @@ -635,7 +639,7 @@ fn embedded_image_clones_share_the_encoded_payload() { let cloned = image.clone(); - assert!(std::sync::Arc::ptr_eq(&image.bytes, &cloned.bytes)); + assert!(Arc::ptr_eq(&image.bytes, &cloned.bytes)); } #[test] @@ -657,7 +661,7 @@ fn a_fill_color_is_written_only_when_set_and_old_files_read_without_one() { "an unset fill color stays out: {json}" ); - let blue = crate::draw::Color { + let blue = Color { r: 0.0, g: 0.0, b: 1.0, diff --git a/src/draw/shape/text.rs b/src/draw/shape/text.rs index 59f9278ed..405b1c344 100644 --- a/src/draw/shape/text.rs +++ b/src/draw/shape/text.rs @@ -1,3 +1,6 @@ +use pangocairo::functions; + +use crate::draw; use crate::draw::font::FontDescriptor; use crate::util::Rect; @@ -155,7 +158,7 @@ pub(crate) fn sticky_note_layout( } pub(crate) fn sticky_note_text_layout_with_measurer( - measurer: &crate::draw::TextMeasurer, + measurer: &draw::TextMeasurer, ctx: &cairo::Context, text: &str, size: f64, @@ -167,7 +170,7 @@ pub(crate) fn sticky_note_text_layout_with_measurer( let font_desc_str = font_desc.to_string(); // Create layout for rendering (required for draw operations) - let layout = pangocairo::functions::create_layout(ctx); + let layout = functions::create_layout(ctx); layout.set_font_description(Some(&font_desc)); layout.set_text(text); if let Some(width) = wrap_width { diff --git a/src/draw/shape/text_cache.rs b/src/draw/shape/text_cache.rs index 6d076d02a..253e9315b 100644 --- a/src/draw/shape/text_cache.rs +++ b/src/draw/shape/text_cache.rs @@ -4,6 +4,7 @@ mod owner; use cache::{TextCacheKey, TextMeasurementCache}; pub use owner::TextMeasurer; +use pangocairo::functions; /// Cached text measurement results from Pango layout. #[derive(Clone, Debug)] @@ -71,7 +72,7 @@ pub(crate) fn configured_layout( font_desc_str: &str, wrap_width: Option, ) -> pango::Layout { - let layout = pangocairo::functions::create_layout(ctx); + let layout = functions::create_layout(ctx); let font_desc = pango::FontDescription::from_string(font_desc_str); layout.set_font_description(Some(&font_desc)); layout.set_text(text); diff --git a/src/draw/shape/text_cache/cursor.rs b/src/draw/shape/text_cache/cursor.rs index 4274a37df..21157b939 100644 --- a/src/draw/shape/text_cache/cursor.rs +++ b/src/draw/shape/text_cache/cursor.rs @@ -1,4 +1,9 @@ -use super::*; +use super::owner::TextMeasurer; +use super::{ + CaretGeometry, TextPreviewGeometry, VisualCaretDirection, VisualLineDirection, VisualLineEdge, + caret_geometry_in, configured_layout, hit_position_to_byte, logical_bounds_in, + snap_char_boundary, +}; impl TextMeasurer { /// Hit-test a point against a rendered text run, returning the caret byte diff --git a/src/draw/shape/text_cache/owner/tests.rs b/src/draw/shape/text_cache/owner/tests.rs index d7bafdfec..29e0ecc4c 100644 --- a/src/draw/shape/text_cache/owner/tests.rs +++ b/src/draw/shape/text_cache/owner/tests.rs @@ -1,5 +1,8 @@ mod bounds; +use crate::draw; +use crate::draw::FontDescriptor; + use super::*; use crate::draw::shape::text_cache::{VisualCaretDirection, VisualLineDirection, VisualLineEdge}; @@ -102,15 +105,15 @@ fn destination_settings_do_not_change_canonical_measurements() { destination.set_font_options(&options); let owner = TextMeasurer::default(); let fresh = TextMeasurer::default(); - crate::draw::render_text_with_measurer( + draw::render_text_with_measurer( &owner, &destination, 0, 5, "Hello 你好\nאבג", - crate::draw::RED, + draw::RED, 16.0, - &crate::draw::FontDescriptor::default(), + &FontDescriptor::default(), false, Some(70), ); diff --git a/src/draw/shape/text_cache/tests.rs b/src/draw/shape/text_cache/tests.rs index 806517a07..990e60d4d 100644 --- a/src/draw/shape/text_cache/tests.rs +++ b/src/draw/shape/text_cache/tests.rs @@ -1,3 +1,5 @@ +use crate::draw; + use super::*; fn measurement(width: f64) -> TextMeasurement { @@ -52,7 +54,7 @@ fn hit_test_result_is_always_a_char_boundary() { #[test] fn caret_geometry_advances_left_to_right_and_has_height() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = draw::TextMeasurer::default(); let start = measurer .caret_geometry_text("hello", "Sans 20", None, 0) .unwrap(); @@ -69,7 +71,7 @@ fn caret_geometry_advances_left_to_right_and_has_height() { #[test] fn caret_geometry_works_on_empty_text() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = draw::TextMeasurer::default(); // An empty buffer still needs a visible caret at the origin. let geom = measurer .caret_geometry_text("", "Sans 20", None, 0) @@ -80,7 +82,7 @@ fn caret_geometry_works_on_empty_text() { #[test] fn caret_geometry_snaps_off_boundary_indices_down() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = draw::TextMeasurer::default(); // Byte 2 is inside the 3-byte '你'; it must resolve like byte 0, not panic. let at_zero = measurer .caret_geometry_text("你a", "Sans 20", None, 0) @@ -93,7 +95,7 @@ fn caret_geometry_snaps_off_boundary_indices_down() { #[test] fn test_cache_returns_same_measurement() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = draw::TextMeasurer::default(); let text = "Hello World"; let font = "Sans 12"; @@ -113,7 +115,7 @@ fn test_cache_returns_same_measurement() { #[test] fn test_different_sizes_use_different_cache_keys() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = draw::TextMeasurer::default(); // Verify that measurements for different sizes are cached with different keys // by checking that both requests succeed (cache doesn't confuse them) let text = "Test"; @@ -190,14 +192,14 @@ fn test_insert_existing_key_updates_cached_measurement() { #[test] fn test_empty_text_returns_none() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = draw::TextMeasurer::default(); let result = measurer.measure("", "Sans 12", 12.0, None); assert!(result.is_none()); } #[test] fn test_wrap_width_affects_cache_key() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = draw::TextMeasurer::default(); let text = "A very long text that would wrap"; let font = "Sans 12"; diff --git a/src/draw/shape/transform.rs b/src/draw/shape/transform.rs index 4b2f65527..0b6a5a315 100644 --- a/src/draw/shape/transform.rs +++ b/src/draw/shape/transform.rs @@ -1,5 +1,7 @@ //! Pure geometry transforms owned by `Shape`. +use crate::util; + use super::Shape; impl Shape { @@ -198,7 +200,7 @@ impl Shape { // carries the arc for free — but a non-uniform one has to // scale the arc itself, or the bulge (the only part of a // flat curved arrow with any height) ignores the drag. - bend: crate::util::scaled_arrow_bend( + bend: util::scaled_arrow_bend( (*x1 as f64, *y1 as f64), (*x2 as f64, *y2 as f64), (nx1 as f64, ny1 as f64), diff --git a/src/draw/shape/transform/tests.rs b/src/draw/shape/transform/tests.rs index 014bcf8b8..c235a36ef 100644 --- a/src/draw/shape/transform/tests.rs +++ b/src/draw/shape/transform/tests.rs @@ -1,3 +1,5 @@ +use crate::util::Rect; + use super::*; use crate::draw::{ ArrowLabel, ArrowStyle, BLACK, BlurStyle, EmbeddedImage, EraserBrush, EraserKind, @@ -157,7 +159,7 @@ fn every_shape_variant_translates_its_bounds() { .unwrap_or_else(|| panic!("{} should keep bounds", shape.kind_name())); assert_eq!( after, - crate::util::Rect::new(before.x + 17, before.y - 9, before.width, before.height) + Rect::new(before.x + 17, before.y - 9, before.width, before.height) .expect("translated bounds"), "{} translated incorrectly", shape.kind_name() diff --git a/src/draw/shape/types.rs b/src/draw/shape/types.rs index 7aded0219..006007b07 100644 --- a/src/draw/shape/types.rs +++ b/src/draw/shape/types.rs @@ -67,6 +67,7 @@ impl BlurStyle { pub const ALL: [Self; 4] = [Self::Gaussian, Self::Pixelate, Self::Secure, Self::BlackOut]; /// Short human-readable name for toolbars, menus, and toasts. + #[must_use] pub fn label(self) -> &'static str { match self { Self::Gaussian => tr("Blur"), @@ -77,6 +78,7 @@ impl BlurStyle { } /// Next style in [`Self::ALL`] order, wrapping at the end. + #[must_use] pub fn next(self) -> Self { match self { Self::Gaussian => Self::Pixelate, @@ -87,6 +89,7 @@ impl BlurStyle { } /// Whether the style needs the captured backdrop to render. + #[must_use] pub fn needs_backdrop(self) -> bool { !matches!(self, Self::BlackOut) } @@ -120,6 +123,7 @@ impl ArrowStyle { /// Stable name for identifiers, the same in every UI language; it matches /// the serde spelling. + #[must_use] pub fn id(self) -> &'static str { match self { Self::Standard => "standard", @@ -131,6 +135,7 @@ impl ArrowStyle { /// Short human-readable name for toolbars, menus, and toasts. Translated; /// use [`Self::id`] wherever a name must stay stable. + #[must_use] pub fn label(self) -> &'static str { match self { Self::Standard => tr("Standard"), @@ -141,6 +146,7 @@ impl ArrowStyle { } /// Next style in [`Self::ALL`] order, wrapping at the end. + #[must_use] pub fn next(self) -> Self { match self { Self::Standard => Self::Pointy, @@ -154,6 +160,7 @@ impl ArrowStyle { /// /// The properties panel steps entries in both directions, so a four-way /// cycle needs a way back that is not three presses forward. + #[must_use] pub fn previous(self) -> Self { match self { Self::Standard => Self::Double, @@ -165,6 +172,7 @@ impl ArrowStyle { /// Whether this style bends its shaft, and so needs the `bend` field and /// the curve sampler rather than the straight-line fast paths. + #[must_use] pub fn is_curved(self) -> bool { matches!(self, Self::Curved) } @@ -175,6 +183,7 @@ impl ArrowStyle { /// lose the arc the user shaped, which means readers have to ask the style /// whether that stored value is on screen before they position anything /// against it. + #[must_use] pub fn effective_bend(self, bend: f64) -> f64 { if self.is_curved() { bend } else { 0.0 } } @@ -471,6 +480,7 @@ impl Shape { /// The returned rectangle is suitable for dirty region tracking and damage hints. /// Returns `None` when the shape has no drawable area or its full bounds cannot be /// represented safely by [`Rect`]. + #[must_use] pub fn bounding_box(&self) -> Option { let measurer = TextMeasurer::default(); self.bounding_box_with(&measurer) @@ -587,6 +597,7 @@ impl Shape { } /// Returns a human-readable label for the shape variant. + #[must_use] pub fn kind_name(&self) -> &'static str { match self { Shape::Freehand { .. } | Shape::FreehandPressure { .. } => tr("Freehand"), @@ -620,13 +631,17 @@ fn normalized_rect(x: i32, y: i32, w: i32, h: i32) -> Option { } mod base64_bytes { - use super::*; + use serde::de; + + use crate::base64; + + use super::{Arc, Deserialize, Deserializer, Serialize, Serializer}; pub fn serialize(bytes: &Arc<[u8]>, serializer: S) -> Result where S: Serializer, { - crate::base64::encode_standard(bytes).serialize(serializer) + base64::encode_standard(bytes).serialize(serializer) } pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> @@ -634,8 +649,8 @@ mod base64_bytes { D: Deserializer<'de>, { let encoded = String::deserialize(deserializer)?; - crate::base64::decode_standard(&encoded) + base64::decode_standard(&encoded) .map(Arc::from) - .map_err(serde::de::Error::custom) + .map_err(de::Error::custom) } } diff --git a/src/draw/spotlight.rs b/src/draw/spotlight.rs index 7c79b9101..4dcd6ab7f 100644 --- a/src/draw/spotlight.rs +++ b/src/draw/spotlight.rs @@ -7,6 +7,7 @@ pub const MAX_SPOTLIGHT_MAGNIFICATION: f64 = 4.0; pub const SPOTLIGHT_MAGNIFICATION_STEP: f64 = 0.25; /// Serde default for Spotlight shapes written before magnification existed. +#[must_use] pub const fn default_spotlight_magnification() -> f64 { DEFAULT_SPOTLIGHT_MAGNIFICATION } @@ -19,6 +20,7 @@ where Ok(normalize_spotlight_magnification(value)) } +#[must_use] pub fn normalize_spotlight_magnification(value: f64) -> f64 { if value.is_finite() { value.clamp(MIN_SPOTLIGHT_MAGNIFICATION, MAX_SPOTLIGHT_MAGNIFICATION) @@ -27,10 +29,12 @@ pub fn normalize_spotlight_magnification(value: f64) -> f64 { } } +#[must_use] pub fn spotlight_magnification_is_active(value: f64) -> bool { normalize_spotlight_magnification(value) > MIN_SPOTLIGHT_MAGNIFICATION + f64::EPSILON } +#[must_use] pub fn format_spotlight_magnification(value: f64) -> String { let value = normalize_spotlight_magnification(value); let mut number = format!("{value:.2}"); diff --git a/src/durable_io.rs b/src/durable_io.rs index 39f5f19b7..c6c6a5c77 100644 --- a/src/durable_io.rs +++ b/src/durable_io.rs @@ -8,8 +8,10 @@ use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; +use std::ffi::{CString, OsStr, OsString}; #[cfg(unix)] use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; +use std::process; mod model; @@ -332,10 +334,7 @@ fn finalize_overwrite_mode( } } -fn create_temp_file( - parent: &Path, - file_name: &std::ffi::OsStr, -) -> Result<(PathBuf, File), DurableIoError> { +fn create_temp_file(parent: &Path, file_name: &OsStr) -> Result<(PathBuf, File), DurableIoError> { create_temp_file_from_candidates((0..64).map(|_| next_temp_path(parent, file_name))) } @@ -371,6 +370,7 @@ where /// that audits its directory has to recognise those leftovers as its own /// rather than as foreign entries, and this is the one place that knows how /// [`next_temp_path`] spells them: `.{target}.{pid}.{stamp}.{sequence}.tmp`. +#[must_use] pub fn temp_file_target(name: &str) -> Option<&str> { let mut rest = name.strip_prefix('.')?.strip_suffix(".tmp")?; for _ in 0..3 { @@ -384,24 +384,20 @@ pub fn temp_file_target(name: &str) -> Option<&str> { } /// Whether `name` is a temporary an atomic write of `target` left behind. +#[must_use] pub fn is_temp_file_for(name: &str, target: &str) -> bool { temp_file_target(name) == Some(target) } -fn next_temp_path(parent: &Path, file_name: &std::ffi::OsStr) -> PathBuf { +fn next_temp_path(parent: &Path, file_name: &OsStr) -> PathBuf { let stamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_nanos(); let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed); - let mut name = std::ffi::OsString::from("."); + let mut name = OsString::from("."); name.push(file_name); - name.push(format!( - ".{}.{}.{}.tmp", - std::process::id(), - stamp, - sequence - )); + name.push(format!(".{}.{}.{}.tmp", process::id(), stamp, sequence)); parent.join(name) } @@ -618,7 +614,7 @@ pub fn rename_no_replace(source: &Path, target: &Path) -> io::Result<()> { } #[cfg(target_os = "linux")] -fn path_to_cstring(path: &Path) -> io::Result { +fn path_to_cstring(path: &Path) -> io::Result { use std::ffi::CString; use std::os::unix::ffi::OsStrExt; diff --git a/src/durable_io/model.rs b/src/durable_io/model.rs index 33670ed93..bea6be67c 100644 --- a/src/durable_io/model.rs +++ b/src/durable_io/model.rs @@ -1,8 +1,7 @@ -use std::fmt; -use std::fs; -use std::io; use std::path::PathBuf; +use std::{fmt, fs, io}; +use std::error; #[cfg(unix)] use std::os::unix::fs::MetadataExt; @@ -79,8 +78,8 @@ impl fmt::Display for DurableIoError { } } -impl std::error::Error for DurableIoError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { +impl error::Error for DurableIoError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { match self { Self::Io { source, .. } => Some(source), _ => None, @@ -116,6 +115,7 @@ pub struct FileIdentity; impl FileIdentity { #[cfg(unix)] + #[must_use] pub fn of(metadata: &fs::Metadata) -> Self { Self { device: metadata.dev(), @@ -139,14 +139,14 @@ impl FileIdentity { /// catch an editor that truncates and rewrites the same file in place. Carrying /// both makes the rename conditional on the complete revision the caller read. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DestinationExpectation<'a> { +pub enum DestinationExpectation<'contents> { /// The path named nothing when the caller looked. Anything there now was /// put there by somebody else, and creating over it would discard it. Absent, /// It named this regular file with exactly these contents. Present { identity: FileIdentity, - contents: &'a [u8], + contents: &'contents [u8], }, } @@ -172,6 +172,7 @@ pub struct AtomicWriteOptions { } impl AtomicWriteOptions { + #[must_use] pub const fn private_runtime_file() -> Self { Self { overwrite: OverwriteMode::Replace, @@ -182,6 +183,7 @@ impl AtomicWriteOptions { } } + #[must_use] pub const fn user_config_file() -> Self { Self { overwrite: OverwriteMode::Replace, diff --git a/src/durable_io/tests.rs b/src/durable_io/tests.rs index ff0d1cb4d..86ac82576 100644 --- a/src/durable_io/tests.rs +++ b/src/durable_io/tests.rs @@ -1,3 +1,5 @@ +use crate::test_temp; + use super::*; use std::fs; use std::io::Write; @@ -28,7 +30,7 @@ fn create_new_reject_options() -> AtomicWriteOptions { #[cfg(unix)] #[test] fn replacing_existing_file_preserves_mode() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("state.toml"); fs::write(&path, "old").unwrap(); fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).unwrap(); @@ -45,7 +47,7 @@ fn replacing_existing_file_preserves_mode() { #[cfg(unix)] #[test] fn creating_new_file_uses_fixed_mode() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("created.toml"); write_text_atomic(&path, "created", create_new_reject_options()).unwrap(); @@ -59,7 +61,7 @@ fn creating_new_file_uses_fixed_mode() { #[test] fn create_new_reports_existing_destination() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("existing.txt"); fs::write(&path, "old").unwrap(); @@ -71,7 +73,7 @@ fn create_new_reports_existing_destination() { #[test] fn temporary_file_creation_skips_colliding_paths() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let first = temp.path().join(".first.tmp"); let second = temp.path().join(".second.tmp"); fs::write(&first, "existing").unwrap(); @@ -88,8 +90,8 @@ fn temporary_file_creation_skips_colliding_paths() { #[test] fn temporary_names_are_recognised_by_the_file_they_were_meant_for() { - let temp = crate::test_temp::tempdir().unwrap(); - let generated = next_temp_path(temp.path(), std::ffi::OsStr::new("admission.json")); + let temp = test_temp::tempdir().unwrap(); + let generated = next_temp_path(temp.path(), OsStr::new("admission.json")); let generated = generated.file_name().unwrap().to_str().unwrap(); assert_eq!(temp_file_target(generated), Some("admission.json")); @@ -112,7 +114,7 @@ fn temporary_names_are_recognised_by_the_file_they_were_meant_for() { #[cfg(unix)] #[test] fn temporary_file_creation_skips_symlink_collisions() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let target = temp.path().join("target"); let link = temp.path().join(".link.tmp"); let second = temp.path().join(".second.tmp"); @@ -128,7 +130,7 @@ fn temporary_file_creation_skips_symlink_collisions() { #[cfg(unix)] #[test] fn follows_existing_symlink_and_preserves_link() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let target = temp.path().join("target.toml"); let link = temp.path().join("config.toml"); fs::write(&target, "old").unwrap(); @@ -143,16 +145,13 @@ fn follows_existing_symlink_and_preserves_link() { .file_type() .is_symlink() ); - assert_eq!( - fs::read_link(&link).unwrap(), - std::path::PathBuf::from("target.toml") - ); + assert_eq!(fs::read_link(&link).unwrap(), PathBuf::from("target.toml")); } #[cfg(unix)] #[test] fn followed_symlink_change_is_rejected_before_finalization() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let first = temp.path().join("first.toml"); let second = temp.path().join("second.toml"); let link = temp.path().join("config.toml"); @@ -174,7 +173,7 @@ fn followed_symlink_change_is_rejected_before_finalization() { #[cfg(unix)] #[test] fn followed_symlink_target_replacement_is_rejected_before_finalization() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let target = temp.path().join("target.toml"); let link = temp.path().join("config.toml"); fs::write(&target, "old").unwrap(); @@ -193,7 +192,7 @@ fn followed_symlink_target_replacement_is_rejected_before_finalization() { #[cfg(unix)] #[test] fn follow_policy_detects_replaced_regular_file() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("config.toml"); fs::write(&path, "old").unwrap(); let options = AtomicWriteOptions::user_config_file(); @@ -209,7 +208,7 @@ fn follow_policy_detects_replaced_regular_file() { #[test] fn caller_expectation_detects_an_in_place_content_change() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("config.toml"); fs::write(&path, "old").unwrap(); let identity = FileIdentity::of(&fs::metadata(&path).unwrap()); @@ -281,7 +280,7 @@ fn a_conditional_create_that_loses_the_final_race_reports_a_changed_destination( #[cfg(unix)] #[test] fn reject_policy_rejects_destination_symlink() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let target = temp.path().join("target"); let link = temp.path().join("state"); fs::write(&target, "target").unwrap(); @@ -296,7 +295,7 @@ fn reject_policy_rejects_destination_symlink() { #[cfg(unix)] #[test] fn reject_policy_rejects_final_path_changed_to_symlink() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("state"); let target = temp.path().join("target"); fs::write(&path, "old").unwrap(); @@ -314,7 +313,7 @@ fn reject_policy_rejects_final_path_changed_to_symlink() { #[cfg(unix)] #[test] fn reject_policy_detects_replaced_regular_file() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("state"); fs::write(&path, "old").unwrap(); let options = replace_reject_options(); @@ -330,7 +329,7 @@ fn reject_policy_detects_replaced_regular_file() { #[test] fn reject_policy_detects_created_regular_file_after_missing_inspect() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("state"); let options = replace_reject_options(); let destination = inspect_destination(&path, options).unwrap(); @@ -351,7 +350,7 @@ fn reject_policy_detects_created_regular_file_after_missing_inspect() { #[cfg(unix)] #[test] fn dangling_followed_symlink_creates_target_and_preserves_link() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let link = temp.path().join("config.toml"); let target = temp.path().join("missing-target.toml"); symlink("missing-target.toml", &link).unwrap(); @@ -370,7 +369,7 @@ fn dangling_followed_symlink_creates_target_and_preserves_link() { #[cfg(unix)] #[test] fn follows_multi_level_symlink_chain() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let target = temp.path().join("target.toml"); let middle = temp.path().join("middle.toml"); let link = temp.path().join("config.toml"); @@ -398,7 +397,7 @@ fn follows_multi_level_symlink_chain() { #[cfg(unix)] #[test] fn changed_intermediate_symlink_is_rejected_before_finalization() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let first = temp.path().join("first.toml"); let second = temp.path().join("second.toml"); let middle = temp.path().join("middle.toml"); @@ -422,7 +421,7 @@ fn changed_intermediate_symlink_is_rejected_before_finalization() { #[cfg(unix)] #[test] fn parent_directory_sync_succeeds() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("state"); fs::write(&path, "state").unwrap(); diff --git a/src/help_overlay_interaction.rs b/src/help_overlay_interaction.rs index 2e832f089..a4dd8c590 100644 --- a/src/help_overlay_interaction.rs +++ b/src/help_overlay_interaction.rs @@ -66,12 +66,14 @@ impl HelpHitMap { } /// Register the painted unbound-actions toggle, as `(x, y, width, height)`. + #[must_use] pub fn with_unbound_toggle(mut self, rect: (f64, f64, f64, f64)) -> Self { self.unbound_toggle = Some(HitRect::from_tuple(rect)); self } /// Return the most specific target at a point, including rectangle edges. + #[must_use] pub fn region_at(&self, x: f64, y: f64) -> Option { if !self.box_rect?.contains(x, y) { return None; diff --git a/src/i18n.rs b/src/i18n.rs index 506431a2e..3cf213ea4 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -17,13 +17,19 @@ //! notes, board names — is user data and is never translated. use std::collections::BTreeMap; -use std::fmt::{Display, Write}; +use std::fmt::{Display, Formatter, Write}; +use std::fs::OpenOptions; +use std::io::ErrorKind; use std::path::{Path, PathBuf}; use std::sync::OnceLock; use std::sync::atomic::{AtomicU8, Ordering}; +use std::{env, fmt, io}; use crate::domain::Language; +#[cfg(not(test))] +use crate::env_vars; use crate::env_vars::{LANGUAGE_OVERRIDE_ENV, LOCALE_DIR_ENV}; +use crate::paths; /// Translation catalogs shipped in the binary. Adding one means adding a /// `locales/.toml`, a `Language` variant, and an entry here. @@ -78,6 +84,7 @@ pub enum LanguageSource { /// The language the deciding setting asks for, possibly [`Language::Auto`], /// and which setting that is, given the configured language. +#[must_use] pub fn requested_language(configured: Language) -> (Language, LanguageSource) { select( configured, @@ -140,6 +147,7 @@ pub(crate) fn restore_language(language: Language) { /// Explicit languages resolve to themselves (English is the fallback, so /// asking for it is always satisfiable). `Auto` follows /// [`detect_from_env`], and a locale without a catalog falls back to English. +#[must_use] pub fn resolve(language: Language) -> Language { if !language.is_auto() { return language; @@ -153,6 +161,7 @@ pub fn resolve(language: Language) -> Language { /// `LANGUAGE` is a colon-separated priority list (`zh_CN:en`), so its first /// translated entry wins. Locales without a catalog leave the slot empty and /// the search continues; English is the final fallback. +#[must_use] pub fn detect_from_env() -> Language { // The test suite asserts source-language strings, so the locale of whoever // runs it must not leak in: a Chinese desktop would otherwise translate the @@ -166,7 +175,7 @@ pub fn detect_from_env() -> Language { #[cfg(not(test))] { - for name in crate::env_vars::LOCALE_ENV_KEYS { + for name in env_vars::LOCALE_ENV_KEYS { let Some(value) = env_var(name) else { continue; }; @@ -191,6 +200,7 @@ pub fn language_from_locale_chain(value: &str) -> Option { /// caller's cue to keep looking or fall back to English. `zh_TW`/`zh_HK` are /// Traditional Chinese and stay untranslated until they have their own /// catalog. +#[must_use] pub fn language_from_locale(locale: &str) -> Option { let normalized = locale .trim() @@ -212,6 +222,7 @@ pub fn language_from_locale(locale: &str) -> Option { } /// Parses the spellings `WAYSCRIBER_LANGUAGE` and configuration accept. +#[must_use] pub fn language_from_code(code: &str) -> Option { match code.trim().to_ascii_lowercase().as_str() { "auto" => Some(Language::Auto), @@ -225,6 +236,7 @@ pub fn language_from_code(code: &str) -> Option { /// /// The argument must be a literal so that the guard test can find every /// translatable string in the sources. Unknown strings pass through. +#[must_use] pub fn tr(source: &'static str) -> &'static str { lookup(language(), source) } @@ -397,11 +409,11 @@ enum OverrideRejection { NotARegularFile, TooLarge, InvalidUtf8, - Unreadable(std::io::Error), + Unreadable(io::Error), } -impl std::fmt::Display for OverrideRejection { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for OverrideRejection { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { match self { Self::Missing => formatter.write_str("no such file"), Self::SymbolicLink => formatter.write_str("it is a symbolic link"), @@ -422,12 +434,12 @@ fn read_override(path: &Path) -> Result { use std::io::Read; use std::os::unix::fs::OpenOptionsExt; - let file = std::fs::OpenOptions::new() + let file = OpenOptions::new() .read(true) .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK) .open(path) .map_err(|err| match err.raw_os_error() { - _ if err.kind() == std::io::ErrorKind::NotFound => OverrideRejection::Missing, + _ if err.kind() == ErrorKind::NotFound => OverrideRejection::Missing, Some(libc::ELOOP) => OverrideRejection::SymbolicLink, _ => OverrideRejection::Unreadable(err), })?; @@ -452,11 +464,11 @@ fn locale_dir() -> Option { return Some(PathBuf::from(dir)); } - crate::paths::data_dir().map(|dir| dir.join("locales")) + paths::data_dir().map(|dir| dir.join("locales")) } fn env_var(name: &str) -> Option { - match std::env::var(name) { + match env::var(name) { Ok(value) if !value.trim().is_empty() => Some(value), _ => None, } diff --git a/src/i18n/tests.rs b/src/i18n/tests.rs index 00a025788..3b11ce0ae 100644 --- a/src/i18n/tests.rs +++ b/src/i18n/tests.rs @@ -1,4 +1,11 @@ use std::collections::BTreeMap; +use std::ffi::CString; +use std::os::unix; +use std::sync::mpsc; +use std::time::Duration; +use std::{fs, thread}; + +use crate::test_env; use super::*; @@ -21,7 +28,7 @@ fn english_source_strings_are_the_identity_catalog() { fn simplified_chinese_serves_translations() { // Catalogs load once per process, with the runner's override directory // if any; the child's is empty, so lookup serves the embedded entries. - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!(module_path!(), "::simplified_chinese_serves_translations"), || { let catalog = catalog_pairs(); @@ -181,7 +188,7 @@ fn catalog_placeholders_survive_translation() { "{source:?} changes the placeholder count" ); - let arguments: Vec<&dyn std::fmt::Display> = vec![&"x"; placeholders]; + let arguments: Vec<&dyn Display> = vec![&"x"; placeholders]; let rendered = substitute(&translation, &arguments); assert!( !rendered.contains("{}"), @@ -211,7 +218,7 @@ fn catalog_entries_are_translated_and_non_empty() { /// The embedded catalog with whatever `contents` an override file holds. fn with_override(contents: &[u8]) -> Catalog { let directory = tempfile::tempdir().expect("override directory"); - std::fs::write(directory.path().join("zh-CN.toml"), contents).expect("write override"); + fs::write(directory.path().join("zh-CN.toml"), contents).expect("write override"); load_catalog("zh-CN", SIMPLIFIED_CHINESE_CATALOG, Some(directory.path())) } @@ -275,14 +282,14 @@ fn an_override_entry_that_changes_its_placeholders_is_dropped() { fn an_override_that_is_too_large_is_refused_after_a_bounded_read() { let directory = tempfile::tempdir().expect("override directory"); let path = directory.path().join("zh-CN.toml"); - std::fs::write(&path, vec![b'#'; MAX_OVERRIDE_BYTES as usize + 1]).expect("write"); + fs::write(&path, vec![b'#'; MAX_OVERRIDE_BYTES as usize + 1]).expect("write"); assert!(matches!( read_override(&path), Err(OverrideRejection::TooLarge) )); - std::fs::write(&path, vec![b'#'; MAX_OVERRIDE_BYTES as usize]).expect("write"); + fs::write(&path, vec![b'#'; MAX_OVERRIDE_BYTES as usize]).expect("write"); assert!(read_override(&path).is_ok(), "the limit itself is allowed"); } @@ -290,8 +297,8 @@ fn an_override_that_is_too_large_is_refused_after_a_bounded_read() { fn a_symlinked_override_is_refused() { let directory = tempfile::tempdir().expect("override directory"); let target = directory.path().join("real.toml"); - std::fs::write(&target, "\"Boards\" = \"白板\"\n").expect("write target"); - std::os::unix::fs::symlink(&target, directory.path().join("zh-CN.toml")).expect("symlink"); + fs::write(&target, "\"Boards\" = \"白板\"\n").expect("write target"); + unix::fs::symlink(&target, directory.path().join("zh-CN.toml")).expect("symlink"); assert!(matches!( read_override(&directory.path().join("zh-CN.toml")), @@ -307,12 +314,12 @@ fn a_symlinked_override_is_refused() { fn a_fifo_override_is_refused_without_waiting_for_a_writer() { let directory = tempfile::tempdir().expect("override directory"); let path = directory.path().join("zh-CN.toml"); - let raw_path = std::ffi::CString::new(path.as_os_str().as_encoded_bytes()).expect("path"); + let raw_path = CString::new(path.as_os_str().as_encoded_bytes()).expect("path"); // SAFETY: `raw_path` is a valid NUL-terminated path for the call. assert_eq!(unsafe { libc::mkfifo(raw_path.as_ptr(), 0o600) }, 0); - let (sender, receiver) = std::sync::mpsc::channel(); - std::thread::spawn(move || { + let (sender, receiver) = mpsc::channel(); + thread::spawn(move || { let _ = sender.send(matches!( read_override(&path), Err(OverrideRejection::NotARegularFile) @@ -321,7 +328,7 @@ fn a_fifo_override_is_refused_without_waiting_for_a_writer() { // No process ever opens the write end, so a blocking open never returns. let refused = receiver - .recv_timeout(std::time::Duration::from_secs(5)) + .recv_timeout(Duration::from_secs(5)) .expect("reading a FIFO override must not block"); assert!(refused); } @@ -329,13 +336,13 @@ fn a_fifo_override_is_refused_without_waiting_for_a_writer() { #[test] fn the_first_lookup_merges_the_override_directory() { let directory = tempfile::tempdir().expect("override directory"); - std::fs::write( + fs::write( directory.path().join("zh-CN.toml"), "\"Boards\" = \"白板\"\n", ) .expect("write override"); - crate::test_env::in_language_isolated_child_with_overrides( + test_env::in_language_isolated_child_with_overrides( concat!( module_path!(), "::the_first_lookup_merges_the_override_directory" @@ -352,14 +359,14 @@ fn the_first_lookup_merges_the_override_directory() { #[test] fn the_command_line_language_holds_for_the_whole_process() { - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::the_command_line_language_holds_for_the_whole_process" ), || { // SAFETY: this child process runs only this test. - unsafe { std::env::set_var(LANGUAGE_OVERRIDE_ENV, "en") }; + unsafe { env::set_var(LANGUAGE_OVERRIDE_ENV, "en") }; set_command_line_language(Language::SimplifiedChinese); set_language(Language::English); diff --git a/src/image_decode.rs b/src/image_decode.rs index a4bd64872..97bdd4ff0 100644 --- a/src/image_decode.rs +++ b/src/image_decode.rs @@ -1,5 +1,7 @@ use std::io::Cursor; +use zune_jpeg::zune_core::bytestream::ZCursor; + use crate::screen_pixels::EmbeddedImageLimits; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -189,8 +191,7 @@ fn normalize_png_rgba( #[allow(dead_code)] fn jpeg_dimensions(bytes: &[u8]) -> Result<(u32, u32), String> { - let mut decoder = - zune_jpeg::JpegDecoder::new(zune_jpeg::zune_core::bytestream::ZCursor::new(bytes)); + let mut decoder = zune_jpeg::JpegDecoder::new(ZCursor::new(bytes)); decoder.decode_headers().map_err(|err| err.to_string())?; let info = decoder .info() @@ -245,6 +246,8 @@ fn pixel_count(width: u32, height: u32) -> Result { #[cfg(test)] mod tests { + use crate::base64; + use super::{DecodeLimits, EncodedImageFormat, decode_rgba}; use crate::screen_pixels::EmbeddedImageLimits; @@ -347,7 +350,7 @@ mod tests { #[test] fn decode_jpeg_rgba_preserves_cmyk_jpeg_colors() { - let bytes = crate::base64::decode_standard(CMYK_RED_JPEG).unwrap(); + let bytes = base64::decode_standard(CMYK_RED_JPEG).unwrap(); let image = decode_rgba( EncodedImageFormat::Jpeg, @@ -401,7 +404,7 @@ mod tests { #[test] fn jpeg_header_over_the_budget_is_rejected_before_decoding() { - let bytes = crate::base64::decode_standard(CMYK_RED_JPEG).unwrap(); + let bytes = base64::decode_standard(CMYK_RED_JPEG).unwrap(); let err = decode_rgba( EncodedImageFormat::Jpeg, diff --git a/src/input/boards.rs b/src/input/boards.rs index 69af7df2a..7472f6e56 100644 --- a/src/input/boards.rs +++ b/src/input/boards.rs @@ -44,6 +44,7 @@ pub struct BoardState { } impl BoardState { + #[must_use] pub fn new(spec: BoardSpec) -> Self { Self { configured_appearance: BoardAppearance::from_spec(&spec), diff --git a/src/input/boards/appearance.rs b/src/input/boards/appearance.rs index 4d1c96d38..e38c3857e 100644 --- a/src/input/boards/appearance.rs +++ b/src/input/boards/appearance.rs @@ -20,6 +20,7 @@ pub struct BoardAppearance { } impl BoardAppearance { + #[must_use] pub fn from_spec(spec: &BoardSpec) -> Self { Self { background: spec.background.clone(), diff --git a/src/input/boards/color.rs b/src/input/boards/color.rs index 2c1a12b26..b1a14fa87 100644 --- a/src/input/boards/color.rs +++ b/src/input/boards/color.rs @@ -2,6 +2,7 @@ use crate::config::BoardColorConfig; use crate::domain::Color; use crate::domain::color::{PALETTE_BLACK, PALETTE_WHITE}; +#[must_use] pub fn runtime_contrast_pen_color(background: Color) -> Color { let [r, g, b] = runtime_contrast_pen_rgb([background.r, background.g, background.b]); Color { r, g, b, a: 1.0 } @@ -10,6 +11,7 @@ pub fn runtime_contrast_pen_color(background: Color) -> Color { /// Picks the auto-adjust pen color for a solid board background. The tuned /// palette constants are used so the assigned pen bit-matches the default /// black/white quick color slots and keeps swatch selection intact. +#[must_use] pub fn runtime_contrast_pen_rgb(background: [f64; 3]) -> [f64; 3] { let luminance = 0.2126 * background[0] + 0.7152 * background[1] + 0.0722 * background[2]; if luminance > 0.5 { @@ -19,6 +21,7 @@ pub fn runtime_contrast_pen_rgb(background: [f64; 3]) -> [f64; 3] { } } +#[must_use] pub fn board_color_from_config(config: &BoardColorConfig) -> Color { let rgb = config.rgb(); Color { @@ -29,6 +32,7 @@ pub fn board_color_from_config(config: &BoardColorConfig) -> Color { } } +#[must_use] pub fn board_color_to_config(color: Color) -> BoardColorConfig { BoardColorConfig::Rgb([color.r, color.g, color.b]) } @@ -37,6 +41,8 @@ pub use crate::domain::clamp_board_rgb; #[cfg(test)] mod tests { + use crate::config::QuickColorPalette; + use super::*; #[test] @@ -53,7 +59,7 @@ mod tests { #[test] fn auto_adjust_pen_colors_bit_match_default_quick_color_slots() { - let palette = crate::config::QuickColorPalette::default(); + let palette = QuickColorPalette::default(); let light_board_pen = runtime_contrast_pen_color(Color { r: 1.0, diff --git a/src/input/boards/core.rs b/src/input/boards/core.rs index 28a6f0717..666ca8a6b 100644 --- a/src/input/boards/core.rs +++ b/src/input/boards/core.rs @@ -6,14 +6,17 @@ use crate::draw::{BoardPages, Frame, PageDeleteOutcome as CanvasPageDeleteOutcom use std::collections::HashSet; impl BoardManager { + #[must_use] pub fn board_count(&self) -> usize { self.boards.len() } + #[must_use] pub fn active_index(&self) -> usize { self.active_index } + #[must_use] pub fn board_identity_generation(&self) -> BoardIdentityGeneration { self.identity_generation } @@ -23,6 +26,7 @@ impl BoardManager { self.identity_generation } + #[must_use] pub fn active_board(&self) -> &BoardState { &self.boards[self.active_index] } @@ -31,10 +35,12 @@ impl BoardManager { &mut self.boards[self.active_index] } + #[must_use] pub fn active_board_id(&self) -> &str { &self.active_board().spec.id } + #[must_use] pub fn has_board(&self, id: &str) -> bool { self.boards.iter().any(|board| board.spec.id == id) } @@ -66,14 +72,17 @@ impl BoardManager { true } + #[must_use] pub fn active_board_name(&self) -> &str { &self.active_board().spec.name } + #[must_use] pub fn active_background(&self) -> &BoardBackground { &self.active_board().spec.background } + #[must_use] pub fn active_pages(&self) -> &BoardPages { &self.active_board().pages } @@ -82,6 +91,7 @@ impl BoardManager { &mut self.active_board_mut().pages } + #[must_use] pub fn active_frame(&self) -> &Frame { self.active_pages().active_frame() } @@ -90,35 +100,43 @@ impl BoardManager { self.active_pages_mut().active_frame_mut() } + #[must_use] pub fn show_badge(&self) -> bool { self.show_badge } + #[must_use] pub fn pan_enabled(&self) -> bool { self.pan_enabled } + #[must_use] pub fn show_pan_badge(&self) -> bool { self.show_pan_badge } + #[must_use] pub fn max_count(&self) -> usize { self.max_count } #[allow(dead_code)] + #[must_use] pub fn default_board_id(&self) -> &str { &self.default_board_id } + #[must_use] pub fn page_count(&self) -> usize { self.active_pages().page_count() } + #[must_use] pub fn active_page_index(&self) -> usize { self.active_pages().active_index() } + #[must_use] pub fn active_page_generation(&self) -> u64 { self.active_pages().generation() } @@ -180,6 +198,7 @@ impl BoardManager { &mut self.boards } + #[must_use] pub fn board_states(&self) -> &[BoardState] { &self.boards } diff --git a/src/input/boards/mapping.rs b/src/input/boards/mapping.rs index 5be88c2fd..0a1b4964e 100644 --- a/src/input/boards/mapping.rs +++ b/src/input/boards/mapping.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use super::{ BOARD_ID_TRANSPARENT, BOARD_ID_WHITEBOARD, BoardBackground, BoardIdentityGeneration, BoardManager, BoardSpec, BoardState, board_color_from_config, board_color_to_config, @@ -24,6 +26,7 @@ pub(crate) enum PendingBoardRuntimeUiAction { }, } +use crate::domain::BoardGrid; use crate::domain::color::PALETTE_BLACK; impl BoardSpec { @@ -33,7 +36,7 @@ impl BoardSpec { name: item.name.clone(), background: board_background_from_config(&item.background), grid: if item.background.is_transparent() { - crate::domain::BoardGrid::from(item.grid).disabled() + BoardGrid::from(item.grid).disabled() } else { item.grid.into() }, @@ -44,6 +47,7 @@ impl BoardSpec { } } + #[must_use] pub fn effective_pen_color(&self) -> Option { if let Some(color) = self.default_pen_color { return Some(color); @@ -77,7 +81,7 @@ impl BoardManager { if boards.is_empty() { boards.push(default_overlay_board()); } - let mut pin_seeds: std::collections::BTreeMap = pin_seeds; + let mut pin_seeds: BTreeMap = pin_seeds; for board in &boards { pin_seeds .entry(board.spec.id.clone()) @@ -107,6 +111,7 @@ impl BoardManager { } } + #[must_use] pub fn to_config(&self) -> BoardsConfig { BoardsConfig { max_count: self.max_count, @@ -142,7 +147,7 @@ impl BoardManager { .items .iter() .map(|item| (item.id.as_str(), item.pinned)) - .collect::>(); + .collect::>(); let live_ids = self .boards .iter() diff --git a/src/input/boards/naming.rs b/src/input/boards/naming.rs index 92efc76d5..2fe0300a3 100644 --- a/src/input/boards/naming.rs +++ b/src/input/boards/naming.rs @@ -1,6 +1,7 @@ use super::{BOARD_ID_TRANSPARENT, BoardBackground, BoardManager, BoardSpec, BoardState}; impl BoardManager { + #[must_use] pub fn can_switch_to_id(&self, id: &str) -> bool { self.switch_to_id_target_index(id) .is_some_and(|index| index != self.active_index) @@ -22,6 +23,7 @@ impl BoardManager { self.switch_to_slot(desired_slot) } + #[must_use] pub fn can_switch_to_slot(&self, slot: usize) -> bool { self.switch_to_slot_target_index(slot) .is_some_and(|index| index != self.active_index) @@ -193,7 +195,7 @@ fn name_from_id(id: &str) -> String { let normalized = id.replace('-', " "); let mut chars = normalized.chars(); match chars.next() { - Some(first) => first.to_uppercase().collect::() + chars.as_str(), + Some(first) => format!("{}{}", first.to_uppercase(), chars.as_str()), None => "Board".to_string(), } } diff --git a/src/input/boards/operations.rs b/src/input/boards/operations.rs index 287416837..9fcdba459 100644 --- a/src/input/boards/operations.rs +++ b/src/input/boards/operations.rs @@ -23,6 +23,7 @@ pub struct BoardDeleteConfirmation { } impl BoardDeleteConfirmation { + #[must_use] pub fn matches_identity(&self, board_id: &str, generation: BoardIdentityGeneration) -> bool { self.board_id == board_id && self.board_identity_generation == generation } @@ -50,7 +51,7 @@ pub enum BoardDeleteOutcome { Rejected(BoardDeleteRejection), } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum BoardDeleteRejection { MissingBoard, StaleConfirmation, @@ -90,6 +91,7 @@ pub struct PageDeleteConfirmation { } impl PageDeleteConfirmation { + #[must_use] pub fn matches_identity( &self, board_id: &str, @@ -132,7 +134,7 @@ pub enum PageDeleteOutcome { Rejected(PageOperationRejection), } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PageOperationRejection { MissingBoard, MissingPage, @@ -204,14 +206,14 @@ impl BoardManager { pub fn delete_board(&mut self, request: BoardDeleteRequest) -> BoardDeleteOutcome { match request { BoardDeleteRequest::Request(target) => self.request_board_delete(target), - BoardDeleteRequest::Confirm(confirmation) => self.confirm_board_delete(confirmation), + BoardDeleteRequest::Confirm(confirmation) => self.confirm_board_delete(&confirmation), } } pub fn delete_page(&mut self, request: PageDeleteRequest) -> PageDeleteOutcome { match request { - PageDeleteRequest::Request(target) => self.request_page_delete(target), - PageDeleteRequest::Confirm(confirmation) => self.confirm_page_delete(confirmation), + PageDeleteRequest::Request(target) => self.request_page_delete(&target), + PageDeleteRequest::Confirm(confirmation) => self.confirm_page_delete(&confirmation), } } @@ -299,7 +301,7 @@ impl BoardManager { fn confirm_board_delete( &mut self, - confirmation: BoardDeleteConfirmation, + confirmation: &BoardDeleteConfirmation, ) -> BoardDeleteOutcome { if confirmation.board_identity_generation != self.identity_generation { return BoardDeleteOutcome::Rejected(BoardDeleteRejection::StaleConfirmation); @@ -346,7 +348,7 @@ impl BoardManager { } } - fn request_page_delete(&mut self, target: PageDeleteTarget) -> PageDeleteOutcome { + fn request_page_delete(&mut self, target: &PageDeleteTarget) -> PageDeleteOutcome { let Some(board_index) = self.board_index_for_page_delete_target(&target.board) else { return PageDeleteOutcome::Rejected(PageOperationRejection::MissingBoard); }; @@ -384,7 +386,7 @@ impl BoardManager { } } - fn confirm_page_delete(&mut self, confirmation: PageDeleteConfirmation) -> PageDeleteOutcome { + fn confirm_page_delete(&mut self, confirmation: &PageDeleteConfirmation) -> PageDeleteOutcome { if confirmation.board_identity_generation != self.identity_generation { return PageDeleteOutcome::Rejected(PageOperationRejection::StaleConfirmation); } diff --git a/src/input/hit_test/mod.rs b/src/input/hit_test/mod.rs index abf37b13d..4085570a4 100644 --- a/src/input/hit_test/mod.rs +++ b/src/input/hit_test/mod.rs @@ -11,6 +11,7 @@ use crate::draw::shape::{ step_marker_radius_with, }; use crate::draw::{DrawnShape, Shape, TextMeasurer}; +use crate::util; use crate::util::Rect; const MAX_HIT_TEST_TOLERANCE: f64 = i32::MAX as f64; @@ -160,7 +161,7 @@ pub(crate) fn hit_test_with_tolerance( // Shaft and heads both come from the shared skeleton, so a curved // arrow is grabbed along its arc and a double-ended one is grabbed // by either head. - let skeleton = crate::util::calculate_arrow_skeleton( + let skeleton = util::calculate_arrow_skeleton( tip_x, tip_y, tail_x, diff --git a/src/input/hit_test/shapes.rs b/src/input/hit_test/shapes.rs index 1025cb797..9ae5bde00 100644 --- a/src/input/hit_test/shapes.rs +++ b/src/input/hit_test/shapes.rs @@ -1,3 +1,4 @@ +use crate::draw::shape; use crate::util::{self, ArrowheadTriangle}; use super::geometry::{ @@ -131,7 +132,7 @@ pub(super) fn polygon_outline_hit( point: (i32, i32), tolerance: f64, ) -> bool { - if !crate::draw::shape::has_minimum_distinct_points(points) { + if !shape::has_minimum_distinct_points(points) { return false; } @@ -145,7 +146,7 @@ pub(super) fn polygon_outline_hit( } pub(super) fn polygon_fill_hit(points: &[(i32, i32)], point: (i32, i32)) -> bool { - if !crate::draw::shape::has_minimum_distinct_points(points) { + if !shape::has_minimum_distinct_points(points) { return false; } diff --git a/src/input/hit_test/tests.rs b/src/input/hit_test/tests.rs index 1d5135051..cc5287c9e 100644 --- a/src/input/hit_test/tests.rs +++ b/src/input/hit_test/tests.rs @@ -1,3 +1,5 @@ +use crate::draw::shape; + use super::*; use crate::draw::{ ArrowLabel, ArrowStyle, BLACK, DrawnShape, EmbeddedImage, EraserBrush, EraserKind, @@ -251,7 +253,7 @@ fn arrowhead_hit_detects_point_near_tip_and_rejects_distant_point() { // Arrow pointing upwards from tail at (0, -20) to tip at (0, 0). let tip = (0, 0); let tail = (0, -20); - let skeleton = crate::util::calculate_arrow_skeleton( + let skeleton = util::calculate_arrow_skeleton( tip.0, tip.1, tail.0, @@ -338,7 +340,7 @@ fn double_arrow_is_grabbed_by_either_head() { #[test] fn arrow_label_hit_detects_label_bounds() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let font = FontDescriptor::default(); let label = ArrowLabel { @@ -367,7 +369,7 @@ fn arrow_label_hit_detects_label_bounds() { ); let label_text = "12"; - let layout = crate::draw::shape::arrow_label_layout_with( + let layout = shape::arrow_label_layout_with( &test_text_measurer, 100, 0, @@ -397,7 +399,7 @@ fn arrow_label_hit_detects_label_bounds() { #[test] fn step_marker_hit_detects_center_and_rejects_outside_point() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let font = FontDescriptor::default(); let label = StepMarkerLabel { @@ -425,13 +427,13 @@ fn step_marker_hit_detects_center_and_rejects_outside_point() { let Shape::StepMarker { label, .. } = &drawn.shape else { panic!("expected step marker shape"); }; - let radius = crate::draw::shape::step_marker_radius_with( + let radius = shape::step_marker_radius_with( &test_text_measurer, label.value, label.size, &label.font_descriptor, ); - let outline = crate::draw::shape::step_marker_outline_thickness(label.size); + let outline = shape::step_marker_outline_thickness(label.size); let outside_x = 50 + (radius + outline / 2.0).ceil() as i32 + 2; assert!( !hit_test(&drawn, (outside_x, 60), 0.1), @@ -563,14 +565,14 @@ fn labelled_arrow_shape(style: ArrowStyle, head_at_end: bool) -> DrawnShape { #[test] fn a_double_arrow_label_is_grabbable_from_the_same_place_either_way() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); // head_at_end has no effect on a Double arrow — the outline is the same // polygon either way, and docs/CONFIG.md says so. The hit area has to // follow, or the number is grabbable where it is not painted on exactly // one of the two readings. let font = FontDescriptor::default(); - let layout = crate::draw::shape::arrow_label_layout_with( + let layout = shape::arrow_label_layout_with( &test_text_measurer, 400, 100, diff --git a/src/input/modifiers.rs b/src/input/modifiers.rs index 8f7b3a9fd..f19c9352d 100644 --- a/src/input/modifiers.rs +++ b/src/input/modifiers.rs @@ -1,10 +1,11 @@ //! Keyboard modifier state tracking. -use crate::config::{DragButtonConfig, MouseDragToolsConfig, enums::ColorSpec}; +use crate::config::enums::ColorSpec; +use crate::config::{ChordModifiers, DragButtonConfig, MouseDragToolsConfig}; use crate::domain::Color; -use super::tool::DragTool; -use super::{events::MouseButton, tool::Tool}; +use super::events::MouseButton; +use super::tool::{DragTool, Tool}; /// Active drag modifier combination used for tool mapping. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -17,6 +18,7 @@ pub enum DragModifier { } impl DragModifier { + #[must_use] pub fn is_active(self) -> bool { !matches!(self, Self::None) } @@ -30,10 +32,12 @@ pub struct DragBinding { } impl DragBinding { + #[must_use] pub fn new(tool: DragTool, color: Option) -> Self { Self { tool, color } } + #[must_use] pub fn from_tool(tool: Tool) -> Self { Self { tool: DragTool::from_tool(tool).unwrap_or(DragTool::Default), @@ -41,6 +45,7 @@ impl DragBinding { } } + #[must_use] pub fn button_default() -> Self { Self { tool: DragTool::Default, @@ -60,6 +65,7 @@ pub struct DragButtonBindings { } impl DragButtonBindings { + #[must_use] pub fn legacy_left() -> Self { Self { drag: DragBinding::from_tool(Tool::Pen), @@ -70,6 +76,7 @@ impl DragButtonBindings { } } + #[must_use] pub fn button_default() -> Self { Self { drag: DragBinding::button_default(), @@ -80,6 +87,7 @@ impl DragButtonBindings { } } + #[must_use] pub fn binding_for_modifier(self, modifier: DragModifier) -> DragBinding { match modifier { DragModifier::None => self.drag, @@ -90,6 +98,7 @@ impl DragButtonBindings { } } + #[must_use] pub fn from_config(config: &DragButtonConfig) -> Self { Self { drag: DragBinding::new( @@ -150,6 +159,7 @@ impl Default for DragToolBindings { } impl DragToolBindings { + #[must_use] pub fn tool_for_modifier(self, modifier: DragModifier) -> Tool { self.binding_for_button_modifier(MouseButton::Left, modifier) .tool @@ -157,6 +167,7 @@ impl DragToolBindings { .unwrap_or(Tool::Select) } + #[must_use] pub fn binding_for_button_modifier( self, button: MouseButton, @@ -170,6 +181,7 @@ impl DragToolBindings { .binding_for_modifier(modifier) } + #[must_use] pub fn from_config(config: &MouseDragToolsConfig) -> Self { Self { left: DragButtonBindings::from_config(&config.left), @@ -178,6 +190,7 @@ impl DragToolBindings { } } + #[must_use] pub fn to_config(self) -> MouseDragToolsConfig { MouseDragToolsConfig::from_buttons( self.left.to_config(), @@ -214,6 +227,7 @@ impl Default for Modifiers { impl Modifiers { /// Creates a new Modifiers instance with all keys released. + #[must_use] pub fn new() -> Self { Self { shift: false, @@ -232,6 +246,7 @@ impl Modifiers { /// 3. Shift → Line /// 4. Tab → Ellipse /// 5. None + #[must_use] pub fn active_drag_modifier(&self) -> DragModifier { if self.ctrl && self.shift { DragModifier::CtrlShift @@ -246,13 +261,27 @@ impl Modifiers { } } + /// The held modifiers a keybinding can carry. Tab is a drag-tool + /// modifier only, so it is left out. + #[must_use] + pub fn chord_modifiers(self) -> ChordModifiers { + ChordModifiers { + ctrl: self.ctrl, + shift: self.shift, + alt: self.alt, + logo: self.logo, + } + } + /// Ctrl, Shift, Alt, and Super — the modifiers a keybinding can carry. /// Tab is a drag-tool modifier only. + #[must_use] pub fn has_shortcut_modifier(self) -> bool { self.ctrl || self.shift || self.alt || self.logo } /// Determines which drawing tool is active based on current modifier state and drag mapping. + #[must_use] pub fn current_tool_with_bindings(&self, bindings: DragToolBindings) -> Tool { bindings.tool_for_modifier(self.active_drag_modifier()) } diff --git a/src/input/state/actions/action_board_pages.rs b/src/input/state/actions/action_board_pages.rs index 35a8b537f..f4c1ad2ac 100644 --- a/src/input/state/actions/action_board_pages.rs +++ b/src/input/state/actions/action_board_pages.rs @@ -1,5 +1,5 @@ use crate::domain::Action; -use crate::draw::PageDeleteOutcome; +use crate::draw::{PageDeleteOutcome, TextMeasurer}; use crate::i18n::tr; use crate::input::state::{Toast, ToastPriority}; use crate::input::{BOARD_ID_BLACKBOARD, BOARD_ID_TRANSPARENT, BOARD_ID_WHITEBOARD}; @@ -10,7 +10,7 @@ use super::super::InputState; impl InputState { pub(in crate::input::state) fn handle_board_pages_action_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, action: Action, ) -> bool { match action { diff --git a/src/input/state/actions/action_capture_zoom.rs b/src/input/state/actions/action_capture_zoom.rs index 8ee3423e4..43adcc65b 100644 --- a/src/input/state/actions/action_capture_zoom.rs +++ b/src/input/state/actions/action_capture_zoom.rs @@ -169,6 +169,8 @@ impl InputState { #[cfg(test)] mod tests { + use crate::draw::TextMeasurer; + use super::*; use crate::input::state::test_support::make_test_input_state; use crate::input::state::{EyedropperCaptureSource, RegionPurposeTag, ScreenCaptureSource}; @@ -193,7 +195,7 @@ mod tests { ); } else { state.set_eyedropper_pending_capture(EyedropperCaptureSource::Frozen); - state.activate_eyedropper_with(&crate::draw::TextMeasurer::default(), None); + state.activate_eyedropper_with(&TextMeasurer::default(), None); } assert!(state.handle_capture_zoom_action(action)); diff --git a/src/input/state/actions/action_colors.rs b/src/input/state/actions/action_colors.rs index bba38901c..5448ef7e7 100644 --- a/src/input/state/actions/action_colors.rs +++ b/src/input/state/actions/action_colors.rs @@ -1,6 +1,6 @@ use crate::config::QuickColorPalette; use crate::domain::Action; -use crate::draw::Color; +use crate::draw::{Color, TextMeasurer}; use crate::input::Tool; use super::super::{HexPasteTarget, InputEffect, InputState}; @@ -12,7 +12,7 @@ impl InputState { pub(in crate::input::state) fn handle_color_action_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, action: Action, ) -> bool { if action == Action::PickScreenColor { @@ -28,7 +28,7 @@ impl InputState { pub(crate) fn apply_color_from_ui_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, color: Color, ) -> bool { self.note_recent_color(color); @@ -57,8 +57,8 @@ impl InputState { /// Restores recents from a session snapshot, re-applying the dedupe and cap /// so a hand-edited session file cannot grow the list past its bound. - pub fn restore_recent_colors(&mut self, colors: Vec) { - self.style.restore_recent_colors(&colors); + pub fn restore_recent_colors(&mut self, colors: &[Color]) { + self.style.restore_recent_colors(colors); } /// Request a hex-color copy to the clipboard. The color is captured now so diff --git a/src/input/state/actions/action_core.rs b/src/input/state/actions/action_core.rs index 685a2239e..6fda76031 100644 --- a/src/input/state/actions/action_core.rs +++ b/src/input/state/actions/action_core.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::super::{DrawingState, InputState, TextInputMode}; use crate::domain::Action; use crate::i18n::tr; @@ -7,7 +9,7 @@ use log::info; impl InputState { pub(in crate::input::state) fn handle_core_action_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, action: Action, ) -> bool { match action { @@ -79,7 +81,7 @@ impl InputState { /// with the same numbers. fn start_text_draft_at_view_centre_with( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, mode: TextInputMode, ) { if !matches!(self.state, DrawingState::Idle) { diff --git a/src/input/state/actions/action_dispatch.rs b/src/input/state/actions/action_dispatch.rs index c0f5b5b78..20283318e 100644 --- a/src/input/state/actions/action_dispatch.rs +++ b/src/input/state/actions/action_dispatch.rs @@ -1,4 +1,5 @@ use crate::domain::Action; +use crate::input::state::{InputTextResources, ZoomAnchor}; use super::super::{InputState, interaction}; @@ -9,7 +10,7 @@ impl InputState { /// gesture preflights live at that shared boundary rather than here. pub(crate) fn handle_action_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, action: Action, ) { self.note_session_interaction_activity(); @@ -22,9 +23,9 @@ impl InputState { /// `anchor` instead of on the control under the pointer. pub(crate) fn handle_action_anchored( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, action: Action, - anchor: crate::input::state::ZoomAnchor, + anchor: ZoomAnchor, ) { let previous = self.zoom_action_anchor.replace(anchor); self.handle_action_with_resources(resources, action); diff --git a/src/input/state/actions/action_history.rs b/src/input/state/actions/action_history.rs index 9c0f66198..81cbc25c9 100644 --- a/src/input/state/actions/action_history.rs +++ b/src/input/state/actions/action_history.rs @@ -1,12 +1,12 @@ use crate::domain::Action; -use crate::draw::Frame; +use crate::draw::{Frame, TextMeasurer}; use super::super::InputState; impl InputState { pub(in crate::input::state) fn handle_history_action_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, action: Action, ) -> bool { match action { @@ -46,17 +46,11 @@ impl InputState { } } - pub(crate) fn undo_all_immediate_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - ) { + pub(crate) fn undo_all_immediate_with_measurer(&mut self, measurer: &TextMeasurer) { while self.step_history_with(measurer, Frame::undo_last) {} } - pub(crate) fn redo_all_immediate_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - ) { + pub(crate) fn redo_all_immediate_with_measurer(&mut self, measurer: &TextMeasurer) { while self.step_history_with(measurer, Frame::redo_last) {} } } diff --git a/src/input/state/actions/action_presets.rs b/src/input/state/actions/action_presets.rs index 80b4ccb43..35044a14d 100644 --- a/src/input/state/actions/action_presets.rs +++ b/src/input/state/actions/action_presets.rs @@ -1,11 +1,12 @@ use crate::domain::Action; +use crate::draw::TextMeasurer; use super::super::InputState; impl InputState { pub(in crate::input::state) fn handle_preset_action_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, action: Action, ) -> bool { match action { diff --git a/src/input/state/actions/action_selection.rs b/src/input/state/actions/action_selection.rs index 241775cd8..ee0d33790 100644 --- a/src/input/state/actions/action_selection.rs +++ b/src/input/state/actions/action_selection.rs @@ -1,4 +1,5 @@ use crate::domain::Action; +use crate::draw::TextMeasurer; use crate::i18n::tr; use crate::input::state::{Toast, ToastPriority}; use log::info; @@ -11,7 +12,7 @@ const KEYBOARD_NUDGE_LARGE: i32 = 32; impl InputState { pub(in crate::input::state) fn handle_selection_action_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, action: Action, ) -> bool { match action { @@ -85,7 +86,7 @@ impl InputState { fn handle_selection_content_action_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, action: Action, ) -> bool { match action { @@ -115,7 +116,7 @@ impl InputState { true } - fn select_all_shapes_with(&mut self, measurer: &crate::draw::TextMeasurer) { + fn select_all_shapes_with(&mut self, measurer: &TextMeasurer) { let ids: Vec<_> = self .boards .active_frame() @@ -136,7 +137,7 @@ impl InputState { fn handle_selection_nudge_action_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, action: Action, ) -> bool { let shifted_step = if self.modifiers.shift { diff --git a/src/input/state/actions/action_tools.rs b/src/input/state/actions/action_tools.rs index 66ec24457..126b0d803 100644 --- a/src/input/state/actions/action_tools.rs +++ b/src/input/state/actions/action_tools.rs @@ -1,4 +1,6 @@ +use crate::config; use crate::domain::Action; +use crate::draw::{TextMeasurer, shape}; use crate::i18n::{tr, trf}; use crate::input::state::{Toast, ToastPriority}; use crate::input::tool::Tool; @@ -16,7 +18,7 @@ impl InputState { return; } let level = self.style.pen_smoothing; - let max = crate::draw::shape::MAX_PEN_SMOOTHING; + let max = shape::MAX_PEN_SMOOTHING; info!("Pen smoothing set to {level}/{max}"); let message = if level == 0 { tr("Pen smoothing off").to_string() @@ -33,7 +35,7 @@ impl InputState { return; } let level = self.style.shape_recognition_sensitivity; - let max = crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY; + let max = config::MAX_SHAPE_RECOGNITION_SENSITIVITY; info!("Shape Pen sensitivity set to {level}/{max}"); self.push_toast( ToastPriority::Info, @@ -44,7 +46,7 @@ impl InputState { pub(in crate::input::state) fn handle_tool_action_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, action: Action, ) -> bool { if let Some(tool) = Tool::from_select_action(action) { diff --git a/src/input/state/actions/action_ui.rs b/src/input/state/actions/action_ui.rs index 9970743ac..ee2fff2fc 100644 --- a/src/input/state/actions/action_ui.rs +++ b/src/input/state/actions/action_ui.rs @@ -1,10 +1,12 @@ +use crate::config::TopDisplayMode; use crate::configurator_destination::{ ConfiguratorDestination, ConfiguratorScreen, language_destination, onboarding_hints_destination, quick_colors_destination, }; use crate::domain::Action; +use crate::draw::TextMeasurer; use crate::i18n::{tr, trf}; -use crate::input::state::{Toast, ToastPriority}; +use crate::input::state::{InputTextResources, Toast, ToastPriority}; use log::info; use super::super::{DrawingState, InputState, PendingBackendAction, PendingToolbarPersistence}; @@ -12,7 +14,7 @@ use super::super::{DrawingState, InputState, PendingBackendAction, PendingToolba impl InputState { pub(in crate::input::state) fn handle_ui_action_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, action: Action, ) -> bool { match action { @@ -181,10 +183,7 @@ impl InputState { } } - fn handle_toggle_focus_mode_with_resources( - &mut self, - resources: crate::input::state::InputTextResources<'_>, - ) { + fn handle_toggle_focus_mode_with_resources(&mut self, resources: InputTextResources<'_>) { // Presenter mode already owns chrome visibility and restores it on // exit; a second snapshot layer would fight it. if self.presenter_mode_active() { @@ -287,10 +286,7 @@ impl InputState { } } - fn handle_toggle_toolbar_with_resources( - &mut self, - resources: crate::input::state::InputTextResources<'_>, - ) { + fn handle_toggle_toolbar_with_resources(&mut self, resources: InputTextResources<'_>) { if self.presenter_hides_toolbars() { return; } @@ -320,10 +316,7 @@ impl InputState { } } - fn handle_cycle_toolbar_display_with_resources( - &mut self, - resources: crate::input::state::InputTextResources<'_>, - ) { + fn handle_cycle_toolbar_display_with_resources(&mut self, resources: InputTextResources<'_>) { if self.presenter_hides_toolbars() { return; } @@ -334,7 +327,7 @@ impl InputState { self.pending_onboarding_usage.used_toolbar_toggle = true; let toast = self.toolbar_display_toast(mode); self.push_toast(ToastPriority::Info, "ui", toast); - if mode == crate::config::TopDisplayMode::Hidden { + if mode == TopDisplayMode::Hidden { self.warn_if_all_chrome_hidden(); } self.queue_toolbar_persistence(PendingToolbarPersistence::DisplayMode { @@ -345,11 +338,11 @@ impl InputState { info!("Toolbar display mode cycled to {mode:?}"); } - fn toolbar_display_toast(&self, mode: crate::config::TopDisplayMode) -> Toast { + fn toolbar_display_toast(&self, mode: TopDisplayMode) -> Toast { match mode { - crate::config::TopDisplayMode::Full => Toast::info(tr("Toolbar: full")), - crate::config::TopDisplayMode::Micro => Toast::info(tr("Toolbar: micro")), - crate::config::TopDisplayMode::Hidden => { + TopDisplayMode::Full => Toast::info(tr("Toolbar: full")), + TopDisplayMode::Micro => Toast::info(tr("Toolbar: micro")), + TopDisplayMode::Hidden => { let label = match self.action_binding_primary_label(Action::CycleToolbarDisplay) { Some(binding) => trf("Show ({})", &[&binding]), None => tr("Show").to_string(), @@ -368,10 +361,7 @@ impl InputState { } } - fn handle_toggle_selection_properties_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - ) { + fn handle_toggle_selection_properties_with_measurer(&mut self, measurer: &TextMeasurer) { if !matches!(self.state, DrawingState::Idle) { return; } diff --git a/src/input/state/actions/key_press/caret_edit.rs b/src/input/state/actions/key_press/caret_edit.rs index e23e96b5f..b42003cf2 100644 --- a/src/input/state/actions/key_press/caret_edit.rs +++ b/src/input/state/actions/key_press/caret_edit.rs @@ -19,9 +19,11 @@ use std::ops::Range; use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; +use crate::draw::TextMeasurer; use crate::draw::shape::{VisualCaretDirection, VisualLineDirection, VisualLineEdge}; use crate::input::events::Key; use crate::input::state::DrawingState; +use crate::input::state::core::TextEditing; use super::text_input::move_horizontal_caret; @@ -29,9 +31,9 @@ use super::text_input::move_horizontal_caret; /// The owner is shared with damage measurement; no destination Cairo context /// or runtime resource is retained by the editor state. #[derive(Clone, Copy)] -pub(in crate::input::state) struct TextNavigation<'a> { - pub(in crate::input::state) measurer: &'a crate::draw::TextMeasurer, - pub(in crate::input::state) font: &'a str, +pub(in crate::input::state) struct TextNavigation<'text> { + pub(in crate::input::state) measurer: &'text TextMeasurer, + pub(in crate::input::state) font: &'text str, pub(in crate::input::state) wrap_width: Option, } @@ -490,7 +492,7 @@ pub(in crate::input::state) fn select_all( true } -impl crate::input::state::core::TextEditing { +impl TextEditing { /// Apply one editor-owned key. `None` means the key belongs to the action /// layer; `Some` reports whether the visible editor state changed. pub(in crate::input::state) fn apply_key_edit( @@ -641,8 +643,8 @@ mod tests { #[test] fn owner_key_edits_advance_revision_only_for_buffer_mutations() { - let measurer = crate::draw::TextMeasurer::default(); - let mut editing = crate::input::state::core::TextEditing::default(); + let measurer = TextMeasurer::default(); + let mut editing = TextEditing::default(); let mut state = DrawingState::text_input(0, 0, "ab".to_string()); editing.begin_session(); diff --git a/src/input/state/actions/key_press/mod.rs b/src/input/state/actions/key_press/mod.rs index baa4cda4f..bb9b000c1 100644 --- a/src/input/state/actions/key_press/mod.rs +++ b/src/input/state/actions/key_press/mod.rs @@ -3,17 +3,22 @@ pub(in crate::input::state) mod caret_edit; mod panels; mod text_input; +use std::time::Instant; + +use crate::draw::TextMeasurer; use crate::input::Modifiers; use crate::input::events::Key; +use crate::input::state::InputTextResources; +use crate::ui_text::UiTextEngine; use super::super::{InputState, interaction}; impl InputState { - pub(crate) fn note_escape_dismissal(&mut self, now: std::time::Instant) { + pub(crate) fn note_escape_dismissal(&mut self, now: Instant) { self.keymap.note_escape_dismissal(now); } - pub(crate) fn suppress_escape_after_dismissal(&self, now: std::time::Instant) -> bool { + pub(crate) fn suppress_escape_after_dismissal(&self, now: Instant) -> bool { self.keymap.suppress_escape_after_dismissal(now) } @@ -41,8 +46,8 @@ impl InputState { /// - Help toggle (configurable) /// - Modifier key tracking pub fn on_key_press(&mut self, key: Key) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.on_key_press_with_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -54,7 +59,7 @@ impl InputState { pub(crate) fn on_key_press_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, ) { self.note_session_interaction_activity(); @@ -63,8 +68,8 @@ impl InputState { } pub fn on_key_repeat(&mut self, key: Key) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.on_key_repeat_with_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -76,7 +81,7 @@ impl InputState { pub(crate) fn on_key_repeat_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, ) { self.note_session_interaction_activity(); diff --git a/src/input/state/actions/key_press/panels.rs b/src/input/state/actions/key_press/panels.rs index 78ef65aeb..1b96c7e52 100644 --- a/src/input/state/actions/key_press/panels.rs +++ b/src/input/state/actions/key_press/panels.rs @@ -1,7 +1,6 @@ -use crate::draw::PageDeleteOutcome; +use crate::draw::{PageDeleteOutcome, TextMeasurer}; use crate::input::events::Key; -use crate::input::state::BoardPickerFocus; -use crate::input::state::InputState; +use crate::input::state::{BoardPickerFocus, InputState, InputTextResources}; const PROPERTIES_PANEL_COARSE_STEP: i32 = 5; @@ -66,7 +65,7 @@ impl InputState { pub(in crate::input::state) fn handle_board_picker_key_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, ) -> bool { if !self.is_board_picker_open() { @@ -144,7 +143,7 @@ impl InputState { fn handle_board_picker_board_list_key_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, ) -> bool { if self.board_picker_is_quick() { @@ -253,7 +252,7 @@ impl InputState { fn handle_board_picker_page_panel_key_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, ) -> bool { let (page_count, current) = self.board_picker_page_panel_position(); @@ -410,7 +409,7 @@ impl InputState { pub(in crate::input::state) fn handle_properties_panel_key_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, ) -> bool { // A key pressed mid-drag ends the drag first, so what it changes is @@ -447,7 +446,7 @@ impl InputState { pub(in crate::input::state) fn handle_context_menu_key_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, ) -> bool { match key { diff --git a/src/input/state/actions/key_press/text_input.rs b/src/input/state/actions/key_press/text_input.rs index fb06a3b5b..ff5a64a12 100644 --- a/src/input/state/actions/key_press/text_input.rs +++ b/src/input/state/actions/key_press/text_input.rs @@ -1,3 +1,6 @@ +use std::cmp::Ordering; +use std::ops::Range; + use crate::draw::TextMeasurer; use log::warn; @@ -6,8 +9,8 @@ use crate::draw::shape::VisualCaretDirection; use crate::input::events::Key; use crate::input::state::core::TextEditing; use crate::input::state::{ - DrawingState, InputEffect, InputState, TextClipboardRequest, TextCutTarget, TextInputMode, - TextPasteEdit, TextPasteTarget, + DrawingState, InputEffect, InputState, InputTextResources, TextClipboardRequest, TextCutTarget, + TextInputMode, TextPasteEdit, TextPasteTarget, }; use super::bindings::{fallback_unshifted_label, key_to_action_label}; @@ -16,7 +19,7 @@ use super::caret_edit::{self, MAX_TEXT_LENGTH, TextNavigation}; impl InputState { pub(in crate::input::state) fn handle_text_input_key_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, ) { // Editing and caret navigation own their keys in text mode, ahead of @@ -293,7 +296,7 @@ impl InputState { } impl TextEditing { - fn selected_text(&self, state: &DrawingState) -> Option<(std::ops::Range, String)> { + fn selected_text(&self, state: &DrawingState) -> Option<(Range, String)> { let DrawingState::TextInput { buffer, caret, @@ -519,9 +522,9 @@ pub(super) fn move_horizontal_caret( } match adjacent.cmp(caret) { - std::cmp::Ordering::Less => caret_edit::move_word_left(buffer, caret, anchor, extend), - std::cmp::Ordering::Greater => caret_edit::move_word_right(buffer, caret, anchor, extend), - std::cmp::Ordering::Equal => caret_edit::move_to_offset(caret, anchor, extend, adjacent), + Ordering::Less => caret_edit::move_word_left(buffer, caret, anchor, extend), + Ordering::Greater => caret_edit::move_word_right(buffer, caret, anchor, extend), + Ordering::Equal => caret_edit::move_to_offset(caret, anchor, extend, adjacent), } } diff --git a/src/input/state/actions/key_press/text_input/measurement_tests.rs b/src/input/state/actions/key_press/text_input/measurement_tests.rs index de0a8e857..cd6194465 100644 --- a/src/input/state/actions/key_press/text_input/measurement_tests.rs +++ b/src/input/state/actions/key_press/text_input/measurement_tests.rs @@ -1,7 +1,9 @@ +use crate::input::state::test_support; + use super::*; fn text_state(buffer: &str) -> InputState { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.state = DrawingState::text_input(0, 0, buffer.to_string()); state } diff --git a/src/input/state/core/base/input_effect_outbox.rs b/src/input/state/core/base/input_effect_outbox.rs index e6930fd53..923c6c747 100644 --- a/src/input/state/core/base/input_effect_outbox.rs +++ b/src/input/state/core/base/input_effect_outbox.rs @@ -1,3 +1,8 @@ +use std::mem; +use std::sync::Arc; + +use crate::input::state::ClipboardFallbackSaveRequest; + use super::{ ClipboardPasteRequest, KeybindingEditRequest, OutputFocusAction, PendingBackendAction, PendingSelectionClipboardPublish, PendingToolbarPersistence, PresetAction, QuickColorEdit, @@ -17,7 +22,7 @@ use std::collections::VecDeque; pub(crate) enum InputEffect { Backend(PendingBackendAction), /// An accepted file save owns its bytes even if the clipboard fallback changes. - ClipboardFallbackSave(std::sync::Arc), + ClipboardFallbackSave(Arc), SpotlightMagnifierFeedback, ToolbarPersistence(PendingToolbarPersistence), KeybindingEdit(KeybindingEditRequest), @@ -369,7 +374,7 @@ fn policy(effect: &InputEffect) -> EffectPolicy { fn same_slot(left: &InputEffect, right: &InputEffect) -> bool { match (left, right) { (InputEffect::ToolbarPersistence(left), InputEffect::ToolbarPersistence(right)) => { - std::mem::discriminant(left) == std::mem::discriminant(right) + mem::discriminant(left) == mem::discriminant(right) } // Each preset slot and each quick color is a config write of its own, // so a later edit replaces only the pending write for the same one. diff --git a/src/input/state/core/base/mod.rs b/src/input/state/core/base/mod.rs index ce774b5f7..f981d6fda 100644 --- a/src/input/state/core/base/mod.rs +++ b/src/input/state/core/base/mod.rs @@ -21,10 +21,9 @@ pub use types::{ }; pub(crate) use types::{ BoardPickerClickState, ClipboardFingerprint, ClipboardPasteRequest, HelperLaunchRequest, - PasteAnchor, PendingBackendAction, PendingOnboardingUsage, PendingSelectionClipboardPublish, - PendingToolbarPersistence, PresetFeedbackState, TextClipboardRequest, TextCutTarget, - TextPasteEdit, TextPasteTarget, ToastCommand, ToastPress, UiToastState, - WayscriberClipboardSelection, + KeybindingEditOperation, KeybindingEditRequest, PasteAnchor, PendingBackendAction, + PendingOnboardingUsage, PendingSelectionClipboardPublish, PendingToolbarPersistence, + PresetFeedbackState, TextClipboardRequest, TextCutTarget, TextPasteEdit, TextPasteTarget, + ToastCommand, ToastPress, UiToastState, WayscriberClipboardSelection, }; -pub(crate) use types::{KeybindingEditOperation, KeybindingEditRequest}; pub use ui_visibility::UiVisibility; diff --git a/src/input/state/core/base/state/init.rs b/src/input/state/core/base/state/init.rs index e3662506b..0a9ce65c3 100644 --- a/src/input/state/core/base/state/init.rs +++ b/src/input/state/core/base/state/init.rs @@ -1,14 +1,23 @@ +use crate::config::PresenterModeConfig; +use crate::input::state::UiVisibility; +use crate::input::state::core::{ + DrawingStyle, EyedropperUiState, HistoryLimits, RegionSelectUiState, ToolbarInteraction, +}; +use crate::input::state::laser::LaserInk; +use crate::render_profiles::RenderProfileSet; + +use super::super::super::selection::{SelectionClipboard, SelectionInteraction}; use super::super::super::{ BoardTransitions, CanvasIndex, ChromeModes, Keymap, PointerTracking, SessionFlags, ViewState, - selection::{SelectionClipboard, SelectionInteraction}, }; use super::super::types::{CompositorCapabilities, DrawingState, PendingOnboardingUsage}; use super::structs::InputState; use crate::config::BoardsConfig; use crate::draw::DirtyTracker; +use crate::input::BoardManager; +use crate::input::modifiers::Modifiers; use crate::input::state::highlight::{ClickHighlightSettings, ClickHighlightState}; use crate::input::state::input_hud::{InputHudSettings, InputHudState}; -use crate::input::{BoardManager, modifiers::Modifiers}; /// Runtime values needed to construct an [`InputState`]. /// @@ -17,14 +26,14 @@ use crate::input::{BoardManager, modifiers::Modifiers}; /// cannot accidentally transpose same-typed positional arguments. #[derive(Clone)] pub(in crate::input::state) struct InputStateSeed { - pub(in crate::input::state) style: crate::input::state::core::DrawingStyle, - pub(in crate::input::state) ui_visibility: crate::input::state::UiVisibility, + pub(in crate::input::state) style: DrawingStyle, + pub(in crate::input::state) ui_visibility: UiVisibility, pub(in crate::input::state) boards_config: BoardsConfig, pub(in crate::input::state) keymap: Keymap, pub(in crate::input::state) canvas_index: CanvasIndex, pub(in crate::input::state) click_highlight_settings: ClickHighlightSettings, - pub(in crate::input::state) history_limits: crate::input::state::core::HistoryLimits, - pub(in crate::input::state) presenter_mode_config: crate::config::PresenterModeConfig, + pub(in crate::input::state) history_limits: HistoryLimits, + pub(in crate::input::state) presenter_mode_config: PresenterModeConfig, } impl InputState { @@ -66,9 +75,9 @@ impl InputState { ui_visibility, zoom_chip: Default::default(), modes: ChromeModes::new(presenter_mode_config), - render_profiles: crate::render_profiles::RenderProfileSet::default(), + render_profiles: RenderProfileSet::default(), status_hud: Default::default(), - toolbar: crate::input::state::core::ToolbarInteraction::default(), + toolbar: ToolbarInteraction::default(), precision_entry: None, board_transitions: BoardTransitions::default(), dirty_tracker: DirtyTracker::new(), @@ -76,7 +85,7 @@ impl InputState { pending_onboarding_usage: PendingOnboardingUsage::default(), zoom_action_anchor: None, click_highlight: ClickHighlightState::new(click_highlight_settings), - laser: crate::input::state::laser::LaserInk::new(Default::default()), + laser: LaserInk::new(Default::default()), input_hud: InputHudState::new(InputHudSettings::default()), selection_interaction: SelectionInteraction::default(), context_menu: Default::default(), @@ -87,8 +96,8 @@ impl InputState { ocr_scan: None, selection_clipboard: SelectionClipboard::default(), properties: Default::default(), - eyedropper_ui_state: crate::input::state::core::EyedropperUiState::Inactive, - region_select_ui_state: crate::input::state::core::RegionSelectUiState::Inactive, + eyedropper_ui_state: EyedropperUiState::Inactive, + region_select_ui_state: RegionSelectUiState::Inactive, preset_slots: Default::default(), compositor_capabilities: CompositorCapabilities::default(), recognition_feedback: Default::default(), diff --git a/src/input/state/core/base/state/modifiers.rs b/src/input/state/core/base/state/modifiers.rs index 2d788a078..379cb46c7 100644 --- a/src/input/state/core/base/state/modifiers.rs +++ b/src/input/state/core/base/state/modifiers.rs @@ -1,5 +1,6 @@ use super::super::types::DrawingState; use super::structs::InputState; +use crate::config::ChordModifiers; use crate::input::Modifiers; impl InputState { @@ -30,12 +31,12 @@ impl InputState { /// /// This lets us correct cases where a key release event was missed but the compositor's /// authoritative modifier state is still accurate. Returns whether any modifier changed. - pub fn sync_modifiers(&mut self, shift: bool, ctrl: bool, alt: bool, logo: bool) -> bool { + pub fn sync_modifiers(&mut self, reported: ChordModifiers) -> bool { self.update_modifiers(|modifiers| { - modifiers.shift = shift; - modifiers.ctrl = ctrl; - modifiers.alt = alt; - modifiers.logo = logo; + modifiers.shift = reported.shift; + modifiers.ctrl = reported.ctrl; + modifiers.alt = reported.alt; + modifiers.logo = reported.logo; // Tab has no direct compositor flag; leave it unchanged. }) } @@ -69,8 +70,11 @@ impl InputState { mod tests { use std::time::Instant; + use crate::config::ChordModifiers; + use crate::draw::TextMeasurer; use crate::input::state::test_support::make_test_input_state; use crate::input::{Key, Tool}; + use crate::ui_text::UiTextEngine; #[test] fn a_modifier_only_sync_redraws_the_tool_chrome_without_dirtying_the_canvas() { @@ -80,7 +84,11 @@ mod tests { let _ = state.take_dirty_regions(); state.needs_redraw = false; - assert!(state.sync_modifiers(false, true, false, false)); + let ctrl_held = ChordModifiers { + ctrl: true, + ..ChordModifiers::default() + }; + assert!(state.sync_modifiers(ctrl_held)); assert!( state.needs_redraw, "the Ctrl drag tool must reach the status bar" @@ -88,7 +96,7 @@ mod tests { assert_eq!(state.active_tool(), Tool::Rect); state.needs_redraw = false; - assert!(state.sync_modifiers(false, false, false, false)); + assert!(state.sync_modifiers(ChordModifiers::default())); assert!( state.needs_redraw, "releasing Ctrl must restore the status bar" @@ -100,10 +108,14 @@ mod tests { #[test] fn an_unchanged_modifier_sync_requests_no_redraw() { let mut state = make_test_input_state(); - state.sync_modifiers(true, false, false, false); + let shift_held = ChordModifiers { + shift: true, + ..ChordModifiers::default() + }; + state.sync_modifiers(shift_held); state.needs_redraw = false; - assert!(!state.sync_modifiers(true, false, false, false)); + assert!(!state.sync_modifiers(shift_held)); assert!(!state.needs_redraw); } @@ -125,8 +137,8 @@ mod tests { #[test] fn focus_loss_clears_modal_repeats_and_modifiers() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -134,7 +146,12 @@ mod tests { let mut state = make_test_input_state(); state.toggle_command_palette(); assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Down)); - state.sync_modifiers(true, true, true, true); + state.sync_modifiers(ChordModifiers { + ctrl: true, + shift: true, + alt: true, + logo: true, + }); state.modifiers.tab = true; assert!( state diff --git a/src/input/state/core/base/state/structs.rs b/src/input/state/core/base/state/structs.rs index ee363f3fa..b8fc08afb 100644 --- a/src/input/state/core/base/state/structs.rs +++ b/src/input/state/core/base/state/structs.rs @@ -1,7 +1,23 @@ +use crate::input::state::core::command_palette::CommandPaletteState; +use crate::input::state::core::font_picker::FontPickerState; +use crate::input::state::core::menus::ContextMenuPanel; +use crate::input::state::core::properties::PropertiesPanelState; +use crate::input::state::core::status_hud::StatusHudState; +use crate::input::state::core::utility::RestoreNoticeContext; +use crate::input::state::core::utility::ocr_scan::OcrScan; +use crate::input::state::core::zoom_chip::ZoomChipState; +use crate::input::state::core::{ + BoardPickerPanel, ColorPickerPopupPanel, DrawingStyle, EyedropperUiState, HelpOverlayState, + HistoryLimits, PresetSlots, RadialMenuPanel, RecognitionFeedback, RegionSelectUiState, + TextEditing, +}; +use crate::input::state::laser::LaserInk; +use crate::input::state::{PrecisionEntryState, SpotlightWheelGesture, UiVisibility, ZoomAnchor}; + +use super::super::super::selection::{SelectionClipboard, SelectionInteraction}; use super::super::super::{ BoardTransitions, CanvasIndex, ChromeModes, Feedback, Keymap, PointerTracking, SessionFlags, ToolbarInteraction, ViewState, - selection::{SelectionClipboard, SelectionInteraction}, }; use super::super::InputEffectOutbox; use super::super::types::{CompositorCapabilities, DrawingState, PendingOnboardingUsage}; @@ -20,11 +36,11 @@ pub struct InputState { /// Multi-board canvas management pub boards: BoardManager, /// Runtime drawing defaults and per-tool appearance settings. - pub(crate) style: crate::input::state::core::DrawingStyle, + pub(crate) style: DrawingStyle, /// State owned by the system font-picker modal. - pub(crate) font_picker: crate::input::state::core::font_picker::FontPickerState, + pub(crate) font_picker: FontPickerState, /// Text-editor mode, asynchronous identity, composition, and pointer state. - pub(crate) text_editing: crate::input::state::core::TextEditing, + pub(crate) text_editing: TextEditing, /// Current modifier key state pub modifiers: Modifiers, /// Configured shortcuts plus transient keyboard and pointer-dispatch state. @@ -47,77 +63,73 @@ pub struct InputState { /// Dirty, preflight, save-as, and last-capture session state. pub(in crate::input::state) session_flags: SessionFlags, /// Visibility, navigation, and pointer bookkeeping for the help overlay. - pub(crate) help_overlay: crate::input::state::core::HelpOverlayState, + pub(crate) help_overlay: HelpOverlayState, /// Modal, layout, search, edit, and drag state for the board picker. - pub(crate) board_picker: crate::input::state::core::BoardPickerPanel, + pub(crate) board_picker: BoardPickerPanel, /// State owned by the command palette modal. - pub command_palette: crate::input::state::core::command_palette::CommandPaletteState, + pub command_palette: CommandPaletteState, /// Toast, geometry, blocked-action, and capability feedback state. pub(in crate::input::state) feedback: Feedback, /// Runtime visibility preferences for overlay chrome and toolbar sections. - pub ui_visibility: crate::input::state::UiVisibility, + pub ui_visibility: UiVisibility, /// Display policy, cached geometry, and pointer interaction state for the zoom chip. - pub(crate) zoom_chip: crate::input::state::core::zoom_chip::ZoomChipState, + pub(crate) zoom_chip: ZoomChipState, /// Presenter, focus, and light mode flags, configuration, and restore snapshots. pub(in crate::input::state) modes: ChromeModes, /// Configured render color profiles and active preview state. pub(crate) render_profiles: RenderProfileSet, /// Cached geometry and pointer interaction state for the status HUD. - pub(crate) status_hud: crate::input::state::core::status_hud::StatusHudState, + pub(crate) status_hud: StatusHudState, /// Toolbar visibility, preferences, resolved layout, and interaction state. pub(in crate::input::state) toolbar: ToolbarInteraction, /// Precise numeric entry popup opened from a pill numeral, when open. - pub(crate) precision_entry: Option, + pub(crate) precision_entry: Option, /// Board switch color, recents, delete confirmations, and restore queues. pub(in crate::input::state) board_transitions: BoardTransitions, /// Tracks dirty regions between renders pub(crate) dirty_tracker: DirtyTracker, /// In-flight Spotlight magnification undo gesture and wheel remainder. - pub(in crate::input::state) spotlight_wheel: crate::input::state::SpotlightWheelGesture, + pub(in crate::input::state) spotlight_wheel: SpotlightWheelGesture, /// Pending first-run onboarding usage markers to persist in onboarding store pub(crate) pending_onboarding_usage: PendingOnboardingUsage, /// Where zoom actions centre while an action runs for a control away from /// the pointer; `None` centres them on the pointer. - pub(crate) zoom_action_anchor: Option, + pub(crate) zoom_action_anchor: Option, /// Click highlight animation state pub(crate) click_highlight: ClickHighlightState, /// Finished laser strokes and their fade clock; never part of a frame. - pub(in crate::input::state) laser: crate::input::state::laser::LaserInk, + pub(in crate::input::state) laser: LaserInk, /// On-screen input HUD (keystroke/click chips) state pub(crate) input_hud: InputHudState, /// Selection membership, nudge direction, and polygon click timing. pub(in crate::input::state) selection_interaction: SelectionInteraction, /// Lifecycle, target, and cached layout for the context menu. - pub(crate) context_menu: crate::input::state::core::menus::ContextMenuPanel, + pub(crate) context_menu: ContextMenuPanel, /// Modal state, cached geometry, and press identity for the color picker popup. - pub(crate) color_picker_popup: crate::input::state::core::ColorPickerPopupPanel, + pub(crate) color_picker_popup: ColorPickerPopupPanel, /// Lifecycle, layout, and configured pointer trigger for the radial menu. - pub(crate) radial_menu: crate::input::state::core::RadialMenuPanel, + pub(crate) radial_menu: RadialMenuPanel, /// Undo retention, delayed playback settings, and active playback state. - pub(crate) history_limits: crate::input::state::core::HistoryLimits, + pub(crate) history_limits: HistoryLimits, /// The scan-band overlay shown while screen text recognition runs, and the /// outcome card that follows it. - pub(crate) ocr_scan: Option, + pub(crate) ocr_scan: Option, /// Local selection clipboard, publication, paste request, and image fallback state. pub(in crate::input::state::core) selection_clipboard: SelectionClipboard, /// Lifecycle, cached geometry, and deferred refresh state for the properties panel. - pub(crate) properties: crate::input::state::core::properties::PropertiesPanelState, + pub(crate) properties: PropertiesPanelState, /// Screen-color eyedropper UI lifecycle. - pub(in crate::input::state::core) eyedropper_ui_state: - crate::input::state::core::EyedropperUiState, + pub(in crate::input::state::core) eyedropper_ui_state: EyedropperUiState, /// Generalized screen-region selector lifecycle. - pub(in crate::input::state::core) region_select_ui_state: - crate::input::state::core::RegionSelectUiState, + pub(in crate::input::state::core) region_select_ui_state: RegionSelectUiState, /// Runtime preset values, active selection, and transient feedback. - pub(crate) preset_slots: crate::input::state::core::PresetSlots, + pub(crate) preset_slots: PresetSlots, /// Compositor capabilities (layer-shell, screencopy, etc.) pub compositor_capabilities: CompositorCapabilities, /// The transient chip naming what Shape Pen just recognized. - pub(in crate::input::state) recognition_feedback: - crate::input::state::core::RecognitionFeedback, + pub(in crate::input::state) recognition_feedback: RecognitionFeedback, /// The board and page the launch restore notice counted, while it may /// still be showing. - pub(in crate::input::state) restore_notice: - Option, + pub(in crate::input::state) restore_notice: Option, } diff --git a/src/input/state/core/base/toast_queue.rs b/src/input/state/core/base/toast_queue.rs index 67646cfbc..dc1b7d8c2 100644 --- a/src/input/state/core/base/toast_queue.rs +++ b/src/input/state/core/base/toast_queue.rs @@ -76,11 +76,13 @@ impl Toast { Self::new(UiToastKind::Error, message) } + #[must_use] pub fn duration_ms(mut self, duration_ms: u64) -> Self { self.duration_ms = duration_ms; self } + #[must_use] pub fn action(mut self, label: impl Into, action: Action) -> Self { self.action = Some(ToastAction { label: label.into(), @@ -89,6 +91,7 @@ impl Toast { self } + #[must_use] pub(crate) fn command(mut self, label: impl Into, command: ToastCommand) -> Self { self.action = Some(ToastAction { label: label.into(), @@ -99,6 +102,7 @@ impl Toast { /// Add a second explicit action chip. Two-action toasts dispatch only from /// their chips; the remaining toast body becomes dismiss-only. + #[must_use] pub fn secondary_action(mut self, label: impl Into, action: Action) -> Self { self.secondary_action = Some(ToastAction { label: label.into(), @@ -121,6 +125,7 @@ impl Toast { /// Rate-limit this key: suppress the push when the same key already /// showed the same message this session. Content changes show again. + #[must_use] pub fn once_per_content(mut self) -> Self { self.once_per_content = true; self @@ -146,11 +151,13 @@ pub enum ToastPushOutcome { impl ToastPushOutcome { /// Whether the push will (eventually) be visible. + #[must_use] pub fn accepted(self) -> bool { !matches!(self, Self::RateLimited | Self::HintYielded) } /// Whether the push changed the currently visible toast. + #[must_use] pub fn changed_active(self) -> bool { matches!(self, Self::Displayed | Self::UpdatedActive) } @@ -235,7 +242,10 @@ impl ToastQueue { if let Some(current) = active.take_if(|current| priority > current.priority) { self.requeue_preempted(current, now); self.activate_next(active, now); - debug_assert!(active.as_ref().is_some_and(|current| current.key == key)); + debug_assert!( + active.as_ref().is_some_and(|current| current.key == key), + "the promoted toast {key:?} must preempt the active toast" + ); return ToastPushOutcome::Displayed; } return ToastPushOutcome::UpdatedQueued; @@ -262,7 +272,10 @@ impl ToastQueue { self.requeue_preempted(current, now); self.enqueue_fifo(priority, key, toast); self.activate_next(active, now); - debug_assert!(active.as_ref().is_some_and(|current| current.key == key)); + debug_assert!( + active.as_ref().is_some_and(|current| current.key == key), + "the preempting toast {key:?} must become the active toast" + ); return ToastPushOutcome::Displayed; } @@ -375,7 +388,10 @@ impl ToastQueue { } fn activate_next(&mut self, active: &mut Option, now: Instant) { - debug_assert!(active.is_none()); + debug_assert!( + active.is_none(), + "retire or requeue the active toast before activating the next one" + ); if self.pending.is_empty() { return; } @@ -490,8 +506,8 @@ mod tests { const T0_DURATION: u64 = UI_TOAST_DURATION_MS; - fn active_key(active: &Option) -> Option<&'static str> { - active.as_ref().map(|toast| toast.key) + fn active_key(active: Option<&UiToastState>) -> Option<&'static str> { + active.map(|toast| toast.key) } #[test] @@ -510,7 +526,7 @@ mod tests { assert_eq!(outcome, ToastPushOutcome::Displayed); assert!(outcome.accepted()); - assert_eq!(active_key(&active), Some("a")); + assert_eq!(active_key(active.as_ref()), Some("a")); assert!(queue.is_empty()); } @@ -533,10 +549,10 @@ mod tests { // Expire "a" -> "b" activates, then "c". let later = now + Duration::from_millis(T0_DURATION); assert_eq!(queue.advance(&mut active, later), (true, true)); - assert_eq!(active_key(&active), Some("b")); + assert_eq!(active_key(active.as_ref()), Some("b")); let later = later + Duration::from_millis(T0_DURATION); assert_eq!(queue.advance(&mut active, later), (true, true)); - assert_eq!(active_key(&active), Some("c")); + assert_eq!(active_key(active.as_ref()), Some("c")); let later = later + Duration::from_millis(T0_DURATION); assert_eq!(queue.advance(&mut active, later), (false, false)); assert!(active.is_none()); @@ -564,13 +580,13 @@ mod tests { ); assert_eq!(outcome, ToastPushOutcome::Displayed); - assert_eq!(active_key(&active), Some("crit")); + assert_eq!(active_key(active.as_ref()), Some("crit")); assert_eq!(queue.pending_len(), 1, "preempted info toast re-queued"); // When the critical toast expires the info toast resumes. let later = now + Duration::from_millis(T0_DURATION); assert_eq!(queue.advance(&mut active, later), (true, true)); - assert_eq!(active_key(&active), Some("info")); + assert_eq!(active_key(active.as_ref()), Some("info")); } #[test] @@ -670,7 +686,7 @@ mod tests { preempt_at, ); - assert_eq!(active_key(&active), Some("crit")); + assert_eq!(active_key(active.as_ref()), Some("crit")); assert!(queue.is_empty(), "stale info toast dropped, not re-queued"); } @@ -705,7 +721,7 @@ mod tests { let later = now + Duration::from_millis(T0_DURATION); queue.advance(&mut active, later); assert_eq!( - active_key(&active), + active_key(active.as_ref()), Some("showing"), "preempted toast keeps seniority over queued peers" ); @@ -744,7 +760,7 @@ mod tests { while active.is_some() { later += Duration::from_millis(T0_DURATION); queue.advance(&mut active, later); - shown.extend(active_key(&active)); + shown.extend(active_key(active.as_ref())); } assert_eq!(shown, ["d", "a", "b"]); } @@ -776,7 +792,7 @@ mod tests { queue.push(&mut active, ToastPriority::Info, "c", Toast::info("c"), now), ToastPushOutcome::Queued ); - assert_eq!(active_key(&active), Some("action")); + assert_eq!(active_key(active.as_ref()), Some("action")); } #[test] @@ -868,7 +884,7 @@ mod tests { ); assert_eq!(outcome, ToastPushOutcome::Displayed); - assert_eq!(active_key(&active), Some("capture")); + assert_eq!(active_key(active.as_ref()), Some("capture")); assert_eq!( active.as_ref().map(|toast| toast.priority), Some(ToastPriority::Critical) @@ -877,7 +893,7 @@ mod tests { let later = now + Duration::from_millis(T0_DURATION); queue.advance(&mut active, later); assert_eq!( - active_key(&active), + active_key(active.as_ref()), Some("action"), "the preempted action toast keeps its remaining display time" ); @@ -962,7 +978,7 @@ mod tests { ); assert_eq!(outcome, ToastPushOutcome::HintYielded); assert!(queue.is_empty()); - assert_eq!(active_key(&active), Some("busy")); + assert_eq!(active_key(active.as_ref()), Some("busy")); } #[test] @@ -999,7 +1015,7 @@ mod tests { Toast::info("hint"), now, ); - assert_eq!(active_key(&active), Some("hint")); + assert_eq!(active_key(active.as_ref()), Some("hint")); queue.push( &mut active, @@ -1008,7 +1024,7 @@ mod tests { Toast::info("i"), now, ); - assert_eq!(active_key(&active), Some("info")); + assert_eq!(active_key(active.as_ref()), Some("info")); assert!(queue.is_empty(), "hint dropped, never re-queued"); } @@ -1022,7 +1038,7 @@ mod tests { queue.push(&mut active, ToastPriority::Info, "b", Toast::info("b"), now); assert!(queue.on_dismissed(&mut active, now)); - assert_eq!(active_key(&active), Some("b")); + assert_eq!(active_key(active.as_ref()), Some("b")); assert!(!queue.on_dismissed(&mut active, now)); assert!(active.is_none()); } @@ -1056,7 +1072,7 @@ mod tests { // The action toast survived; the oldest info entries were dropped. let later = now + Duration::from_millis(T0_DURATION); queue.advance(&mut active, later); - assert_eq!(active_key(&active), Some("act")); + assert_eq!(active_key(active.as_ref()), Some("act")); } #[test] @@ -1077,7 +1093,7 @@ mod tests { for key in keys { queue.push(&mut active, ToastPriority::Info, key, Toast::info(key), now); } - assert_eq!(active_key(&active), Some("i0")); + assert_eq!(active_key(active.as_ref()), Some("i0")); assert_eq!(queue.pending_len(), MAX_PENDING); // A critical push preempts the fresh active i0. i0 is re-queued with @@ -1090,13 +1106,13 @@ mod tests { Toast::error("e"), now, ); - assert_eq!(active_key(&active), Some("crit")); + assert_eq!(active_key(active.as_ref()), Some("crit")); // When the critical toast expires the preempted i0 resumes first. let later = now + Duration::from_millis(T0_DURATION); queue.advance(&mut active, later); assert_eq!( - active_key(&active), + active_key(active.as_ref()), Some("i0"), "the freshly preempted senior toast survives capacity eviction" ); @@ -1123,7 +1139,7 @@ mod tests { Toast::info("Board deleted").action("Undo", Action::BoardRestoreDeleted), now, ); - assert_eq!(active_key(&active), Some("crit")); + assert_eq!(active_key(active.as_ref()), Some("crit")); assert_eq!( queue.pending_len(), 1, @@ -1135,7 +1151,7 @@ mod tests { key == "board.delete" && matches!(action, Some(Action::BoardRestoreDeleted)) }); assert!(!changed, "active slot did not change"); - assert_eq!(active_key(&active), Some("crit")); + assert_eq!(active_key(active.as_ref()), Some("crit")); assert!(queue.is_empty(), "stale queued action removed"); // When the critical toast expires nothing stale surfaces. @@ -1165,7 +1181,7 @@ mod tests { Toast::info("i"), now, ); - assert_eq!(active_key(&active), Some("board.delete")); + assert_eq!(active_key(active.as_ref()), Some("board.delete")); // Removing the active toast vacates the slot but does NOT eagerly // surface the unrelated survivor — that would pop an unrelated toast a @@ -1177,7 +1193,7 @@ mod tests { // The normal advance cycle then promotes the surviving valid entry. assert_eq!(queue.advance(&mut active, now), (true, true)); - assert_eq!(active_key(&active), Some("info")); + assert_eq!(active_key(active.as_ref()), Some("info")); } #[test] diff --git a/src/input/state/core/base/types.rs b/src/input/state/core/base/types.rs index 114034194..facb169b0 100644 --- a/src/input/state/core/base/types.rs +++ b/src/input/state/core/base/types.rs @@ -20,8 +20,9 @@ use crate::draw::{Color, Shape, ShapeId}; use crate::input::tool::Tool; use crate::util::Rect; use serde::{Deserialize, Serialize}; +use std::ops::Range; +use std::sync::Arc; use std::time::Instant; -use std::{ops::Range, sync::Arc}; /// The press that started a selection move. /// @@ -42,6 +43,7 @@ pub struct SelectionGrab { } impl SelectionGrab { + #[must_use] pub fn new(shape_id: ShapeId, x: i32, y: i32) -> Self { Self { shape_id, @@ -54,7 +56,7 @@ impl SelectionGrab { /// Records the pointer at `(x, y)` and returns whether the gesture is now /// a drag. pub(crate) fn track(&mut self, x: i32, y: i32) -> bool { - let threshold = crate::input::state::mouse::SELECTION_DRAG_THRESHOLD; + let threshold = mouse::SELECTION_DRAG_THRESHOLD; self.dragged |= (x - self.x).abs() >= threshold || (y - self.y).abs() >= threshold; self.dragged } @@ -176,7 +178,7 @@ pub enum DrawingState { /// Which handle is being dragged handle: SelectionHandle, /// Original bounding box of selection - original_bounds: crate::util::Rect, + original_bounds: Rect, /// Starting mouse position start_x: i32, start_y: i32, @@ -189,6 +191,7 @@ impl DrawingState { /// Build a [`DrawingState::TextInput`] with the caret at the end of /// `buffer` and no selection — the common entry point for both new text and /// resuming an edit of existing text. + #[must_use] pub fn text_input(x: i32, y: i32, buffer: String) -> Self { let caret = buffer.len(); DrawingState::TextInput { @@ -259,7 +262,11 @@ pub(crate) struct TextPasteEdit { pub(crate) inserted_len: usize, } +use crate::config::TopDisplayMode; +use crate::configurator_destination::ConfiguratorDestination; +use crate::desktop_open::DesktopOpenRequest; pub use crate::domain::{PressureThicknessEditMode, PressureThicknessEntryMode}; +use crate::input::state::mouse; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SelectionAxis { @@ -315,6 +322,7 @@ pub enum PresetAction { impl PresetAction { /// The preset slot this action writes. + #[must_use] pub fn slot(&self) -> usize { match self { Self::Save { slot, .. } | Self::Clear { slot } => *slot, @@ -483,14 +491,17 @@ pub enum ShellMode { } impl CompositorCapabilities { + #[must_use] pub fn direct_capture_available(&self) -> bool { self.screencopy || self.image_copy_capture } + #[must_use] pub fn all_available(&self) -> bool { self.layer_shell && self.direct_capture_available() && self.pointer_constraints } + #[must_use] pub fn limitations_summary(&self) -> Option { let mut issues = Vec::new(); if !self.layer_shell { @@ -515,7 +526,7 @@ impl CompositorCapabilities { #[derive(Debug, Clone, PartialEq, Eq)] pub enum HelperLaunchRequest { About, - Configurator(Option), + Configurator(Option), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -524,7 +535,7 @@ pub enum PendingBackendAction { MeasureMode, CanvasExport(Action), BoardPdfExport(Action), - DesktopOpen(crate::desktop_open::DesktopOpenRequest), + DesktopOpen(DesktopOpenRequest), HelperLaunch(HelperLaunchRequest), ClearSavedToolState, /// Replay Tour: run the first-run cards again from the first step. @@ -544,7 +555,7 @@ pub enum PendingToolbarPersistence { /// the runtime-UI preview's rollback; toolbar-event paths persist via /// their exact event-policy target instead. DisplayMode { - previous: crate::config::TopDisplayMode, + previous: TopDisplayMode, }, /// The pin flag driven by the keyboard visibility toggle (F9). The /// toggle already applied, so the payload carries the pre-change pin for diff --git a/src/input/state/core/board/appearance.rs b/src/input/state/core/board/appearance.rs index 59a140915..d1f6c1f93 100644 --- a/src/input/state/core/board/appearance.rs +++ b/src/input/state/core/board/appearance.rs @@ -1,5 +1,6 @@ use crate::domain::BoardBackground; use crate::input::InputState; +use crate::input::boards; use crate::input::boards::{BoardAppearance, BoardPenOrigin}; impl InputState { @@ -7,14 +8,14 @@ impl InputState { pub(in crate::input::state::core) fn apply_board_appearance_value( &mut self, index: usize, - appearance: BoardAppearance, + appearance: &BoardAppearance, ) -> bool { let active = index == self.boards.active_index(); let Some(board) = self.boards.board_state_mut(index) else { return false; }; let current = BoardAppearance::from_spec(&board.spec); - if current == appearance + if current == *appearance || current.background.is_transparent() || appearance.background.is_transparent() { @@ -26,7 +27,7 @@ impl InputState { let BoardBackground::Solid(color) = board.spec.background else { unreachable!() }; - let pen = crate::input::runtime_contrast_pen_color(color); + let pen = boards::runtime_contrast_pen_color(color); board.spec.default_pen_color = Some(pen); board.pen_origin = BoardPenOrigin::RuntimeContrast; active.then_some(pen) diff --git a/src/input/state/core/board/delete_restore.rs b/src/input/state/core/board/delete_restore.rs index 7c02c79d1..443ffd70f 100644 --- a/src/input/state/core/board/delete_restore.rs +++ b/src/input/state/core/board/delete_restore.rs @@ -200,7 +200,7 @@ impl InputState { ); if self.boards.active_board_id() != active_id_before { - self.finish_board_transition_from(current_spec, &active_id_before, false); + self.finish_board_transition_from(¤t_spec, &active_id_before, false); } else { self.mark_board_surface_changed(); } @@ -274,7 +274,7 @@ impl InputState { } => { self.clear_pending_deletes_after_board_generation_change(generation_before); self.queue_board_identity_available(&restored_id); - self.finish_board_transition_from(current_spec, ¤t_id, false); + self.finish_board_transition_from(¤t_spec, ¤t_id, false); self.push_toast( ToastPriority::Info, "board.delete", diff --git a/src/input/state/core/board/delete_restore/page.rs b/src/input/state/core/board/delete_restore/page.rs index ba7c17b18..0a832ae98 100644 --- a/src/input/state/core/board/delete_restore/page.rs +++ b/src/input/state/core/board/delete_restore/page.rs @@ -1,7 +1,8 @@ +use crate::input::boards::PageDeleteConfirmation; + use super::super::super::base::{InputState, PAGE_DELETE_CONFIRM_MS}; use crate::domain::Action; -use crate::draw::PageDeleteOutcome as CanvasPageDeleteOutcome; -use crate::draw::TextMeasurer; +use crate::draw::{PageDeleteOutcome as CanvasPageDeleteOutcome, TextMeasurer}; use crate::i18n::{tr, trf}; use crate::input::boards::{ PageDeleteBoardTarget, PageDeleteOutcome, PageDeleteRequest, PageDeleteTarget, @@ -229,10 +230,7 @@ impl InputState { } } - fn page_delete_confirmation_is_current( - &self, - confirmation: &crate::input::boards::PageDeleteConfirmation, - ) -> bool { + fn page_delete_confirmation_is_current(&self, confirmation: &PageDeleteConfirmation) -> bool { if confirmation.board_identity_generation != self.boards.board_identity_generation() { return false; } diff --git a/src/input/state/core/board/delete_restore/tests.rs b/src/input/state/core/board/delete_restore/tests.rs index 051deba5b..32afb6fc8 100644 --- a/src/input/state/core/board/delete_restore/tests.rs +++ b/src/input/state/core/board/delete_restore/tests.rs @@ -1,5 +1,5 @@ use crate::domain::Action; -use crate::draw::Frame; +use crate::draw::{Frame, PageDeleteOutcome, TextMeasurer}; use crate::input::BOARD_ID_BLACKBOARD; use crate::input::boards::PendingBoardRuntimeUiAction; use crate::input::state::core::base::{ @@ -29,7 +29,7 @@ fn set_page_count(state: &mut InputState, board_index: usize, count: usize) { #[test] fn confirmed_board_delete_uses_supplied_now_for_undo_timestamp() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.switch_board(BOARD_ID_BLACKBOARD); let requested_at = Instant::now(); @@ -43,7 +43,7 @@ fn confirmed_board_delete_uses_supplied_now_for_undo_timestamp() { #[test] fn expired_board_delete_confirmation_is_replaced_with_supplied_now() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.switch_board(BOARD_ID_BLACKBOARD); let requested_at = Instant::now(); @@ -62,7 +62,7 @@ fn expired_board_delete_confirmation_is_replaced_with_supplied_now() { #[test] fn restore_deleted_board_expires_old_entries_with_supplied_now() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.switch_board(BOARD_ID_BLACKBOARD); let requested_at = Instant::now(); @@ -91,7 +91,7 @@ fn restore_deleted_board_expires_old_entries_with_supplied_now() { #[test] fn confirmed_active_page_delete_uses_supplied_now_for_undo_timestamp() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let board = board_index(&state, BOARD_ID_BLACKBOARD); state.switch_board(BOARD_ID_BLACKBOARD); @@ -101,11 +101,11 @@ fn confirmed_active_page_delete_uses_supplied_now_for_undo_timestamp() { assert_eq!( state.delete_active_page_at_with_measurer(&measurer, requested_at), - crate::draw::PageDeleteOutcome::Pending + PageDeleteOutcome::Pending ); assert_eq!( state.delete_active_page_at_with_measurer(&measurer, confirmed_at), - crate::draw::PageDeleteOutcome::Removed + PageDeleteOutcome::Removed ); assert_eq!(state.latest_deleted_page_at_for_test(), Some(confirmed_at)); @@ -113,7 +113,7 @@ fn confirmed_active_page_delete_uses_supplied_now_for_undo_timestamp() { #[test] fn expired_active_page_delete_confirmation_is_replaced_with_supplied_now() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let board = board_index(&state, BOARD_ID_BLACKBOARD); state.switch_board(BOARD_ID_BLACKBOARD); @@ -124,11 +124,11 @@ fn expired_active_page_delete_confirmation_is_replaced_with_supplied_now() { assert_eq!( state.delete_active_page_at_with_measurer(&measurer, requested_at), - crate::draw::PageDeleteOutcome::Pending + PageDeleteOutcome::Pending ); assert_eq!( state.delete_active_page_at_with_measurer(&measurer, expired_at), - crate::draw::PageDeleteOutcome::Pending + PageDeleteOutcome::Pending ); assert_eq!(state.boards.page_count(), page_count); @@ -140,7 +140,7 @@ fn expired_active_page_delete_confirmation_is_replaced_with_supplied_now() { #[test] fn expired_page_in_board_delete_confirmation_is_replaced_with_supplied_now() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let board = board_index(&state, BOARD_ID_BLACKBOARD); set_page_count(&mut state, board, 2); @@ -150,11 +150,11 @@ fn expired_page_in_board_delete_confirmation_is_replaced_with_supplied_now() { assert_eq!( state.delete_page_in_board_at_with_measurer(&measurer, board, 1, requested_at), - crate::draw::PageDeleteOutcome::Pending + PageDeleteOutcome::Pending ); assert_eq!( state.delete_page_in_board_at_with_measurer(&measurer, board, 1, expired_at), - crate::draw::PageDeleteOutcome::Pending + PageDeleteOutcome::Pending ); assert_eq!( @@ -172,7 +172,7 @@ fn expired_page_in_board_delete_confirmation_is_replaced_with_supplied_now() { /// one, so it cannot surface later against a session that no longer backs it. #[test] fn session_replacement_drops_queued_delete_undo_toast() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.switch_board(BOARD_ID_BLACKBOARD); @@ -233,7 +233,7 @@ fn session_replacement_drops_queued_delete_undo_toast() { #[test] fn restore_deleted_page_expires_old_entries_with_supplied_now() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let board = board_index(&state, BOARD_ID_BLACKBOARD); state.switch_board(BOARD_ID_BLACKBOARD); @@ -243,11 +243,11 @@ fn restore_deleted_page_expires_old_entries_with_supplied_now() { assert_eq!( state.delete_active_page_at_with_measurer(&measurer, requested_at), - crate::draw::PageDeleteOutcome::Pending + PageDeleteOutcome::Pending ); assert_eq!( state.delete_active_page_at_with_measurer(&measurer, confirmed_at), - crate::draw::PageDeleteOutcome::Removed + PageDeleteOutcome::Removed ); let page_count_after_delete = state.boards.page_count(); @@ -266,7 +266,7 @@ fn restore_deleted_page_expires_old_entries_with_supplied_now() { #[test] fn delete_then_create_reused_id_tracks_distinct_identity_before_drain() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); assert!(state.create_board()); let reused_id = state.board_id().to_string(); @@ -290,7 +290,7 @@ fn delete_then_create_reused_id_tracks_distinct_identity_before_drain() { #[test] fn delete_then_restore_same_board_cancels_pending_identity_deletion() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); assert!(state.create_board()); let restored_id = state.board_id().to_string(); diff --git a/src/input/state/core/board/pages.rs b/src/input/state/core/board/pages.rs index fe61fbdc9..d0d2d8955 100644 --- a/src/input/state/core/board/pages.rs +++ b/src/input/state/core/board/pages.rs @@ -1,6 +1,7 @@ +use crate::input::boards::BoardAppearance; + use super::super::base::InputState; -use crate::draw::Color; -use crate::draw::TextMeasurer; +use crate::draw::{Color, TextMeasurer}; use crate::i18n::{tr, trf}; use crate::input::BoardBackground; use crate::input::boards::PendingBoardRuntimeUiAction; @@ -24,7 +25,7 @@ impl InputState { true } - pub(crate) fn set_board_name(&mut self, index: usize, name: String) -> bool { + pub(crate) fn set_board_name(&mut self, index: usize, name: &str) -> bool { let Some(board) = self.boards.board_state_mut(index) else { return false; }; @@ -57,9 +58,9 @@ impl InputState { ); return false; } - let mut appearance = crate::input::boards::BoardAppearance::from_spec(&board.spec); + let mut appearance = BoardAppearance::from_spec(&board.spec); appearance.background = BoardBackground::Solid(color); - self.apply_board_appearance_value(index, appearance) + self.apply_board_appearance_value(index, &appearance) } pub(crate) fn request_board_pin_toggle(&mut self, index: usize) -> bool { diff --git a/src/input/state/core/board/switch.rs b/src/input/state/core/board/switch.rs index 28e806c3c..cb525f44d 100644 --- a/src/input/state/core/board/switch.rs +++ b/src/input/state/core/board/switch.rs @@ -1,3 +1,8 @@ +use std::collections::BTreeSet; + +use crate::draw::Color; +use crate::input::{BoardManager, Tool}; + use super::super::base::InputState; use crate::draw::TextMeasurer; use crate::i18n::{tr, trf}; @@ -5,11 +10,11 @@ use crate::input::state::{Toast, ToastPriority}; use crate::input::{BOARD_ID_TRANSPARENT, BoardSpec}; impl InputState { - pub(crate) fn board_previous_color(&self) -> Option { + pub(crate) fn board_previous_color(&self) -> Option { self.board_transitions.previous_color() } - pub(crate) fn set_board_previous_color(&mut self, color: Option) { + pub(crate) fn set_board_previous_color(&mut self, color: Option) { self.board_transitions.set_previous_color(color); } @@ -26,8 +31,8 @@ impl InputState { pub(crate) fn restore_board_pen_after_snapshot( &mut self, previous_auto: bool, - previous_pen: crate::draw::Color, - previous_color: Option, + previous_pen: Color, + previous_color: Option, ) { let spec = &self.boards.active_board().spec; let target_auto = spec.auto_adjust_pen && !spec.background.is_transparent(); @@ -281,8 +286,8 @@ impl InputState { pub(super) fn switch_board_with( &mut self, measurer: &TextMeasurer, - can_switch: impl FnOnce(&crate::input::BoardManager) -> bool, - switch: impl FnOnce(&mut crate::input::BoardManager) -> bool, + can_switch: impl FnOnce(&BoardManager) -> bool, + switch: impl FnOnce(&mut BoardManager) -> bool, current_id: &str, ) -> bool { if !can_switch(&self.boards) { @@ -295,7 +300,7 @@ impl InputState { .board_states() .iter() .map(|board| board.spec.id.clone()) - .collect::>(); + .collect::>(); let generation_before = self.boards.board_identity_generation(); self.cancel_active_interaction_with(measurer); let switched = switch(&mut self.boards); @@ -305,7 +310,10 @@ impl InputState { } let target_spec = self.boards.active_board().spec.clone(); - debug_assert_ne!(target_spec.id, current_id); + debug_assert_ne!( + target_spec.id, current_id, + "a successful board switch leaves the current board" + ); if target_spec.id == current_id { return false; } @@ -320,13 +328,13 @@ impl InputState { self.queue_board_identity_available(id); } self.clear_pending_deletes_after_board_generation_change(generation_before); - self.finish_board_transition_from(current_spec, current_id, true); + self.finish_board_transition_from(¤t_spec, current_id, true); true } pub(super) fn finish_board_transition_from( &mut self, - current_spec: BoardSpec, + current_spec: &BoardSpec, current_id: &str, log_switch: bool, ) { @@ -340,7 +348,7 @@ impl InputState { match (current_auto, target_auto) { (false, true) => { self.board_transitions - .remember_previous_color(self.color_for_tool(crate::input::Tool::Pen)); + .remember_previous_color(self.color_for_tool(Tool::Pen)); if let Some(default_color) = target_spec.effective_pen_color() { self.set_pen_color_from_board(default_color); } diff --git a/src/input/state/core/board_picker/appearance.rs b/src/input/state/core/board_picker/appearance.rs index c2c1c6a17..2684e6d54 100644 --- a/src/input/state/core/board_picker/appearance.rs +++ b/src/input/state/core/board_picker/appearance.rs @@ -1,3 +1,6 @@ +use crate::draw; +use crate::util::Rect; + use super::{color_to_hex, parse_hex_color}; use crate::domain::{ BOARD_GRID_MAX_SPACING, BOARD_GRID_MIN_SPACING, BoardBackground, BoardGrid, BoardGridKind, @@ -233,7 +236,7 @@ impl BoardAppearanceEdit { pub(crate) fn preview(&self) -> (Color, BoardGrid) { let base = match self.original.background { BoardBackground::Solid(color) => color, - _ => crate::draw::WHITE, + _ => draw::WHITE, }; ( if self.color_is_dirty() { @@ -330,21 +333,20 @@ impl InputState { pub(crate) fn apply_board_appearance(&mut self) -> bool { self.mark_board_appearance_region(); - let Some(edit) = &self.board_picker.appearance else { + let Some(edit) = self.board_picker.appearance.as_mut() else { return false; }; + let identity_current = self.boards.board_identity_generation() == edit.generation; let index = self .boards .board_states() .iter() .position(|b| b.spec.id == edit.id); - if self.boards.board_identity_generation() != edit.generation || index.is_none() { - self.board_picker.appearance.as_mut().unwrap().error = - Some(tr("Board identity changed. Cancel and reopen to edit it.").into()); + let Some(index) = index.filter(|_| identity_current) else { + edit.error = Some(tr("Board identity changed. Cancel and reopen to edit it.").into()); self.needs_redraw = true; return false; - } - let index = index.unwrap(); + }; let current = BoardAppearance::from_spec(&self.boards.board_states()[index].spec); let proposed = if current.background.is_transparent() { Err(tr("Paper patterns require a solid board.")) @@ -354,12 +356,12 @@ impl InputState { let desired = match proposed { Ok(desired) => desired, Err(error) => { - self.board_picker.appearance.as_mut().unwrap().error = Some(error.into()); + edit.error = Some(error.into()); self.needs_redraw = true; return false; } }; - self.apply_board_appearance_value(index, desired); + self.apply_board_appearance_value(index, &desired); self.board_picker_clear_edit(); self.needs_redraw = true; true @@ -597,7 +599,7 @@ impl InputState { let (left, top, width, height) = frame.bounds(); let bottom = top + height + SHEET_SHADOW * frame.scale; let (x, y) = ((left - 4.0).floor(), (top - 4.0).floor()); - crate::util::Rect::new( + Rect::new( x as i32, y as i32, ((left + width + 4.0).ceil() - x) as i32, @@ -731,7 +733,11 @@ impl InputState { let in_color_field = header.is_some_and(|header| rect_contains(header.color_field, point)); let ((x, y), width) = (point, frame.width); - let edit = self.board_picker.appearance.as_mut().unwrap(); + let edit = self + .board_picker + .appearance + .as_mut() + .expect("the appearance frame exists only while an edit is open"); match y.floor() as i32 { -65..=-35 => { if in_color_field { diff --git a/src/input/state/core/board_picker/appearance/tests.rs b/src/input/state/core/board_picker/appearance/tests.rs index e5e9c5013..80e21cd16 100644 --- a/src/input/state/core/board_picker/appearance/tests.rs +++ b/src/input/state/core/board_picker/appearance/tests.rs @@ -1,3 +1,6 @@ +use crate::input::{MouseButton, Tool}; +use crate::ui_text::UiTextEngine; + use super::*; use crate::draw::{BLUE, RED}; use crate::input::state::test_support::TestInputStateBuilder; @@ -5,8 +8,8 @@ use crate::input::state::test_support::TestInputStateBuilder; fn editing() -> InputState { let mut input = TestInputStateBuilder::default().build(); input.switch_board_force("whiteboard"); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); - input.board_picker_edit_color_selected_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); + input.board_picker_edit_color_selected_with_measurer(&TextMeasurer::default()); input.clear_session_dirty(); input } @@ -15,19 +18,19 @@ fn editing() -> InputState { fn appearance_preview_cancel_and_noop_never_change_board_or_session() { let mut input = editing(); let original = BoardAppearance::from_spec(&input.boards.active_board().spec); - let pen = input.color_for_tool(crate::input::Tool::Pen); + let pen = input.color_for_tool(Tool::Pen); input.board_appearance_palette(RED); input.board_picker.appearance.as_mut().unwrap().preview(); assert_eq!( BoardAppearance::from_spec(&input.boards.active_board().spec), original ); - assert_eq!(input.color_for_tool(crate::input::Tool::Pen), pen); + assert_eq!(input.color_for_tool(Tool::Pen), pen); assert!(!input.is_session_dirty()); input.board_appearance_key(Key::Escape); assert!(input.board_appearance_edit().is_none()); assert!(!input.boards.active_board().appearance_explicit); - input.board_picker_edit_color_selected_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_edit_color_selected_with_measurer(&TextMeasurer::default()); assert!(input.apply_board_appearance()); assert!(!input.is_session_dirty()); } @@ -62,7 +65,7 @@ fn reverting_draft_fields_preserves_later_external_changes() { #[test] fn appearance_apply_merges_untouched_fields_and_preserves_history_and_pen_for_grid_only() { let mut input = editing(); - let pen = input.color_for_tool(crate::input::Tool::Pen); + let pen = input.color_for_tool(Tool::Pen); let frame = input.boards.active_frame(); let history = ( frame.shapes.len(), @@ -78,7 +81,7 @@ fn appearance_apply_merges_untouched_fields_and_preserves_history_and_pen_for_gr input.boards.active_board().spec.grid, BoardGrid::new(BoardGridKind::IsometricDots, 63) ); - assert_eq!(input.color_for_tool(crate::input::Tool::Pen), pen); + assert_eq!(input.color_for_tool(Tool::Pen), pen); assert!(input.is_session_dirty()); assert!(input.boards.active_board().appearance_explicit); let frame = input.boards.active_frame(); @@ -128,7 +131,7 @@ fn appearance_keyboard_edits_spacing_and_pattern_and_selection_cancels() { input.boards.active_board().spec.grid, BoardGrid::new(BoardGridKind::Cartesian, 20) ); - input.board_picker_edit_color_selected_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_edit_color_selected_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected(0); assert!(input.board_appearance_edit().is_none()); } @@ -146,7 +149,7 @@ fn appearance_preview_and_cancel_damage_the_sheet_without_changing_the_board() { // Unrelated UI damage must not suppress the sheet through a nonempty clip. input .dirty_tracker - .mark_rect(crate::util::Rect::new(0, 0, 10, 10).unwrap()); + .mark_rect(Rect::new(0, 0, 10, 10).unwrap()); assert!(input.board_appearance_key(key)); let regions = input.dirty_tracker.take_regions(900, 700); assert!(regions.iter().any(|rect| { @@ -226,8 +229,8 @@ fn dragging_the_size_slider_updates_the_draft_until_release() { use crate::input::state::interaction::{ CanvasPoint, PointerPoints, PointerPress, ScreenPoint, route_pointer_press, }; - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); let start = (track_x as i32 + 1, middle_y); let slider_press = route_pointer_press( &mut input, @@ -236,7 +239,7 @@ fn dragging_the_size_slider_updates_the_draft_until_release() { ui_engine: &ui_engine, }, PointerPress::new( - crate::input::MouseButton::Left, + MouseButton::Left, PointerPoints::new( ScreenPoint::new(start.0, start.1), CanvasPoint::new(start.0, start.1), @@ -317,7 +320,7 @@ fn cancel_sits_before_apply_and_both_buttons_work() { BoardGridKind::None ); - input.board_picker_edit_color_selected_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_edit_color_selected_with_measurer(&TextMeasurer::default()); input.board_picker.appearance.as_mut().unwrap().kind = BoardGridKind::Cartesian; let (x, y) = center(&input, buttons.apply); assert!(input.board_appearance_click(x, y)); @@ -415,7 +418,7 @@ fn sheet_header_close_cancels_and_color_field_takes_focus() { #[test] fn opening_and_closing_the_sheet_damage_the_whole_surface() { let mut input = laid_out(900, 700); - let full = |regions: &[crate::util::Rect]| { + let full = |regions: &[Rect]| { regions .iter() .any(|rect| rect.x <= 0 && rect.y <= 0 && rect.width >= 900 && rect.height >= 700) @@ -425,7 +428,7 @@ fn opening_and_closing_the_sheet_damage_the_whole_surface() { input.board_appearance_key(Key::Escape); assert!(full(&input.dirty_tracker.take_regions(900, 700))); - input.board_picker_edit_color_selected_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_edit_color_selected_with_measurer(&TextMeasurer::default()); assert!(input.board_appearance_edit().is_some()); assert!(full(&input.dirty_tracker.take_regions(900, 700))); } @@ -434,8 +437,8 @@ fn opening_and_closing_the_sheet_damage_the_whole_surface() { fn command_palette_edit_board_paper_opens_the_sheet_on_the_active_board() { use crate::domain::Action; - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, @@ -536,7 +539,7 @@ fn the_wheel_does_not_reach_the_size_control_under_the_paper_picker() { fn picker_edits_preview_on_the_draft_and_cancel_restores_it() { let mut input = editing(); let original = input.board_appearance_edit().unwrap().color.clone(); - let pen = input.color_for_tool(crate::input::Tool::Pen); + let pen = input.color_for_tool(Tool::Pen); open_paper_picker(&mut input); input.color_picker_popup_set_color(RED); @@ -550,7 +553,7 @@ fn picker_edits_preview_on_the_draft_and_cancel_restores_it() { input.boards.active_board().spec.background, BoardBackground::Solid(RED) ); - assert_eq!(input.color_for_tool(crate::input::Tool::Pen), pen); + assert_eq!(input.color_for_tool(Tool::Pen), pen); assert!(!input.is_session_dirty()); input.close_color_picker_popup(true); @@ -562,7 +565,7 @@ fn picker_edits_preview_on_the_draft_and_cancel_restores_it() { #[test] fn picker_ok_keeps_the_draft_color_and_only_apply_writes_the_board() { let mut input = editing(); - let pen = input.color_for_tool(crate::input::Tool::Pen); + let pen = input.color_for_tool(Tool::Pen); let recents = input.recent_colors().len(); open_paper_picker(&mut input); @@ -574,7 +577,7 @@ fn picker_ok_keeps_the_draft_color_and_only_apply_writes_the_board() { input.board_appearance_edit().unwrap().color, color_to_hex(BLUE) ); - assert_eq!(input.color_for_tool(crate::input::Tool::Pen), pen); + assert_eq!(input.color_for_tool(Tool::Pen), pen); assert_eq!( input.recent_colors().len(), recents, @@ -594,7 +597,7 @@ fn picker_ok_keeps_the_draft_color_and_only_apply_writes_the_board() { fn ok_with_the_opening_color_typed_back_undoes_an_earlier_preview() { let mut input = editing(); // A color with a three-digit spelling, so it is only parsed on OK. - assert!(input.board_appearance_palette(crate::draw::WHITE)); + assert!(input.board_appearance_palette(draw::WHITE)); open_paper_picker(&mut input); input.color_picker_popup_set_color(RED); assert_eq!( @@ -653,7 +656,7 @@ fn closing_the_sheet_or_picker_takes_the_paper_picker_with_it() { assert!(!input.is_color_picker_popup_open()); assert!(input.is_board_picker_open()); - input.board_picker_edit_color_selected_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_edit_color_selected_with_measurer(&TextMeasurer::default()); open_paper_picker(&mut input); input.close_board_picker(); assert!(!input.is_color_picker_popup_open()); diff --git a/src/input/state/core/board_picker/layout/compute/content_metrics.rs b/src/input/state/core/board_picker/layout/compute/content_metrics.rs index 375a3a7e6..ec2ab1426 100644 --- a/src/input/state/core/board_picker/layout/compute/content_metrics.rs +++ b/src/input/state/core/board_picker/layout/compute/content_metrics.rs @@ -1,10 +1,10 @@ use cairo::Context as CairoContext; use crate::domain::Action; +use crate::label_format; use super::super::super::super::base::InputState; -use super::super::super::BoardPickerEditMode; -use super::super::super::COLUMN_GAP; +use super::super::super::{BoardPickerEditMode, COLUMN_GAP}; use super::{BoardPickerContentMetrics, BoardPickerLayoutConfig}; impl InputState { @@ -213,7 +213,7 @@ fn board_slot_hint(state: &InputState, index: usize) -> Option { let label = state.action_binding_label(action); // The unbound sentinel stays in the source language: it is compared, never // drawn, so it is not translated and needs no catalog entry. - if label == crate::label_format::NOT_BOUND_LABEL { + if label == label_format::NOT_BOUND_LABEL { None } else { Some(label) diff --git a/src/input/state/core/board_picker/layout/hit_test.rs b/src/input/state/core/board_picker/layout/hit_test.rs index 5e69a9df6..238c82fb9 100644 --- a/src/input/state/core/board_picker/layout/hit_test.rs +++ b/src/input/state/core/board_picker/layout/hit_test.rs @@ -15,8 +15,8 @@ struct FloatRect { } #[derive(Debug, Clone, Copy)] -struct BoardPickerPagePanelHitContext<'a> { - layout: &'a super::super::BoardPickerLayout, +struct BoardPickerPagePanelHitContext<'layout> { + layout: &'layout super::super::BoardPickerLayout, board_index: usize, info: PagePanelInfo, } diff --git a/src/input/state/core/board_picker/search.rs b/src/input/state/core/board_picker/search.rs index d20badbdf..fafe25843 100644 --- a/src/input/state/core/board_picker/search.rs +++ b/src/input/state/core/board_picker/search.rs @@ -155,7 +155,9 @@ mod tests { use super::{fuzzy_score, fuzzy_score_relaxed}; use crate::config::KeybindingsConfig; + use crate::draw::TextMeasurer; use crate::input::InputState; + use crate::input::state::test_support; use std::time::{Duration, Instant}; fn make_state() -> InputState { @@ -164,7 +166,7 @@ mod tests { .build_action_bindings() .expect("default keybindings bindings"); - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.set_action_bindings(action_bindings); state } @@ -183,7 +185,7 @@ mod tests { #[test] fn board_picker_match_index_accepts_numeric_board_selection() { let mut state = make_state(); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); assert_eq!( state.board_picker_match_index("3"), @@ -194,7 +196,7 @@ mod tests { #[test] fn board_picker_match_index_trims_surrounding_whitespace() { let mut state = make_state(); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); let blueprint_index = state .boards .board_states() @@ -211,7 +213,7 @@ mod tests { #[test] fn board_picker_match_index_rejects_whitespace_only_queries() { let mut state = make_state(); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); assert_eq!(state.board_picker_match_index(" \t "), None); } @@ -219,7 +221,7 @@ mod tests { #[test] fn board_picker_append_search_returns_focus_to_board_list() { let mut state = make_state(); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); state.board_picker_set_focus(BoardPickerFocus::PagePanel); assert_eq!(state.board_picker_focus(), BoardPickerFocus::PagePanel); @@ -232,7 +234,7 @@ mod tests { #[test] fn board_picker_append_search_resets_stale_query_before_appending() { let mut state = make_state(); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); state.board_picker.search = "old".to_string(); state.board_picker.search_last_input = Some(Instant::now() - BOARD_PICKER_SEARCH_TIMEOUT - Duration::from_millis(1)); diff --git a/src/input/state/core/board_picker/state/actions.rs b/src/input/state/core/board_picker/state/actions.rs index 601be67d4..246a649f6 100644 --- a/src/input/state/core/board_picker/state/actions.rs +++ b/src/input/state/core/board_picker/state/actions.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::super::super::base::InputState; use super::super::BoardPickerEditMode; use crate::draw::PageDeleteOutcome; @@ -20,7 +22,7 @@ impl InputState { pub(crate) fn board_picker_activate_row_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, index: usize, ) { let board_count = self.boards.board_count(); @@ -36,7 +38,7 @@ impl InputState { pub(crate) fn board_picker_activate_page_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, page_index: usize, ) { let Some(board_index) = self.board_picker_page_panel_board_index() else { @@ -57,10 +59,7 @@ impl InputState { self.close_board_picker(); } - pub(crate) fn board_picker_add_page_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - ) { + pub(crate) fn board_picker_add_page_with_measurer(&mut self, measurer: &TextMeasurer) { let Some(board_index) = self.board_picker_page_panel_board_index() else { return; }; @@ -79,7 +78,7 @@ impl InputState { pub(crate) fn board_picker_delete_page_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, page_index: usize, ) -> PageDeleteOutcome { let Some(board_index) = self.board_picker_page_panel_board_index() else { @@ -94,7 +93,7 @@ impl InputState { pub(crate) fn board_picker_duplicate_page_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, page_index: usize, ) { let Some(board_index) = self.board_picker_page_panel_board_index() else { @@ -113,10 +112,7 @@ impl InputState { } } - pub(crate) fn board_picker_create_new_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - ) { + pub(crate) fn board_picker_create_new_with_measurer(&mut self, measurer: &TextMeasurer) { if self.board_picker_is_quick() { self.board_picker_promote_to_full(); } @@ -137,10 +133,7 @@ impl InputState { self.needs_redraw = true; } - pub(crate) fn board_picker_delete_selected_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - ) { + pub(crate) fn board_picker_delete_selected_with_measurer(&mut self, measurer: &TextMeasurer) { let Some(index) = self.board_picker_selected_index() else { return; }; diff --git a/src/input/state/core/board_picker/state/drag.rs b/src/input/state/core/board_picker/state/drag.rs index 1cccae9d4..d91b8a3d2 100644 --- a/src/input/state/core/board_picker/state/drag.rs +++ b/src/input/state/core/board_picker/state/drag.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::super::super::base::InputState; use super::super::{BoardPickerDrag, BoardPickerPageDrag, BoardPickerState}; @@ -161,7 +163,7 @@ impl InputState { pub(crate) fn board_picker_finish_page_drag_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ) -> bool { let Some(drag) = self.board_picker.page_drag.take() else { return false; diff --git a/src/input/state/core/board_picker/state/edit.rs b/src/input/state/core/board_picker/state/edit.rs index 340bc2795..dc25e14b2 100644 --- a/src/input/state/core/board_picker/state/edit.rs +++ b/src/input/state/core/board_picker/state/edit.rs @@ -1,6 +1,7 @@ -use crate::draw::Color; +use crate::draw::{Color, TextMeasurer}; use crate::i18n::{tr, trf}; use crate::input::BoardBackground; +use crate::input::events::Key; use crate::input::state::{Toast, ToastPriority}; use super::super::super::base::InputState; @@ -108,7 +109,7 @@ impl InputState { pub(crate) fn board_picker_commit_page_edit_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ) -> bool { let Some(edit) = self.board_picker.page_edit.take() else { return false; @@ -301,10 +302,7 @@ impl InputState { self.needs_redraw = true; } - pub(crate) fn board_picker_rename_selected_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - ) { + pub(crate) fn board_picker_rename_selected_with_measurer(&mut self, measurer: &TextMeasurer) { let Some(index) = self.board_picker_selected_index() else { return; }; @@ -326,7 +324,7 @@ impl InputState { /// Opens the paper sheet for a board, opening the full picker when needed. pub(crate) fn board_picker_edit_board_paper_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, board_index: usize, ) { if !self.is_board_picker_open() || self.board_picker_is_quick() { @@ -341,7 +339,7 @@ impl InputState { pub(crate) fn board_picker_edit_color_selected_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ) { let Some(index) = self.board_picker_selected_index() else { return; @@ -395,7 +393,7 @@ impl InputState { let trimmed = buffer.trim(); match mode { BoardPickerEditMode::Name => { - if !self.set_board_name(board_index, trimmed.to_string()) { + if !self.set_board_name(board_index, trimmed) { return false; } } @@ -424,7 +422,7 @@ impl InputState { } pub(crate) fn board_picker_edit_backspace(&mut self) { - if self.board_appearance_key(crate::input::events::Key::Backspace) { + if self.board_appearance_key(Key::Backspace) { return; } if let Some(edit) = self.board_picker_edit_buffer_mut() { @@ -434,7 +432,7 @@ impl InputState { } pub(crate) fn board_picker_edit_append(&mut self, ch: char) { - if self.board_appearance_key(crate::input::events::Key::Char(ch)) { + if self.board_appearance_key(Key::Char(ch)) { return; } let Some(edit) = self.board_picker_edit_buffer_mut() else { diff --git a/src/input/state/core/board_picker/state/lifecycle.rs b/src/input/state/core/board_picker/state/lifecycle.rs index 8d8374a59..67b92aaff 100644 --- a/src/input/state/core/board_picker/state/lifecycle.rs +++ b/src/input/state/core/board_picker/state/lifecycle.rs @@ -1,3 +1,5 @@ +use crate::input::state::core::modal::ModalSurface; + use super::super::super::base::InputState; use super::super::{BoardPickerFocus, BoardPickerMode, BoardPickerPageNavMode, BoardPickerState}; use crate::draw::TextMeasurer; @@ -26,7 +28,7 @@ impl InputState { fn open_board_picker_in_mode(&mut self, measurer: &TextMeasurer, mode: BoardPickerMode) { self.pending_onboarding_usage.used_board_picker = true; - self.close_modals_for_open(crate::input::state::core::modal::ModalSurface::BoardPicker); + self.close_modals_for_open(ModalSurface::BoardPicker); self.cancel_active_interaction_with(measurer); let active_index = self.boards.active_index(); let active_page = self.boards.active_page_index(); @@ -327,6 +329,7 @@ impl InputState { #[cfg(test)] mod tests { + use crate::draw::TextMeasurer; use crate::input::state::test_support::make_test_input_state; #[test] @@ -334,13 +337,13 @@ mod tests { let mut state = make_test_input_state(); state.open_radial_menu(320.0, 240.0); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); assert!(state.is_board_picker_open()); assert!(!state.is_radial_menu_open()); state.close_board_picker(); state.open_radial_menu(320.0, 240.0); - state.open_board_picker_quick_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_quick_with_measurer(&TextMeasurer::default()); assert!(state.is_board_picker_open()); assert!(state.board_picker_is_quick()); assert!(!state.is_radial_menu_open()); diff --git a/src/input/state/core/board_picker/state/nav.rs b/src/input/state/core/board_picker/state/nav.rs index 5a4dff2c4..f11123be7 100644 --- a/src/input/state/core/board_picker/state/nav.rs +++ b/src/input/state/core/board_picker/state/nav.rs @@ -1,3 +1,4 @@ +use crate::draw::TextMeasurer; use crate::i18n::tr; use crate::input::events::Key; use crate::input::state::{Toast, ToastPriority}; @@ -117,7 +118,7 @@ impl InputState { pub(crate) fn handle_board_picker_page_nav_key_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, ) -> Option { match self.board_picker_page_nav_mode() { @@ -192,7 +193,7 @@ impl InputState { fn handle_board_picker_page_search_key_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, ) -> bool { match key { diff --git a/src/input/state/core/captured_image.rs b/src/input/state/core/captured_image.rs index 70336a36b..450d0efbc 100644 --- a/src/input/state/core/captured_image.rs +++ b/src/input/state/core/captured_image.rs @@ -1,7 +1,6 @@ use super::InputState; -use crate::draw::TextMeasurer; use crate::draw::frame::UndoAction; -use crate::draw::{EmbeddedImage, Shape}; +use crate::draw::{EmbeddedImage, Shape, TextMeasurer}; use crate::i18n::tr; use crate::screen_pixels::EmbeddedImageLimits; use crate::util::Rect; @@ -130,11 +129,7 @@ mod tests { let mut state = make_test_input_state(); let target = target(&state); - assert!(state.insert_captured_image_with( - &crate::draw::TextMeasurer::default(), - image(16), - &target - )); + assert!(state.insert_captured_image_with(&TextMeasurer::default(), image(16), &target)); let frame = state.boards.active_frame(); assert_eq!(frame.shapes.len(), 1); @@ -158,11 +153,7 @@ mod tests { let mut target = target(&state); target.page_generation = target.page_generation.wrapping_add(1); - assert!(!state.insert_captured_image_with( - &crate::draw::TextMeasurer::default(), - image(16), - &target - )); + assert!(!state.insert_captured_image_with(&TextMeasurer::default(), image(16), &target)); assert!(state.boards.active_frame().shapes.is_empty()); assert_eq!( state.test_active_toast_message(), @@ -176,11 +167,7 @@ mod tests { let target = target(&state); assert!(state.boards.next_board(), "test config has another board"); - assert!(!state.insert_captured_image_with( - &crate::draw::TextMeasurer::default(), - image(16), - &target - )); + assert!(!state.insert_captured_image_with(&TextMeasurer::default(), image(16), &target)); assert!(state.boards.board_states().iter().all(|board| { board .pages @@ -201,7 +188,7 @@ mod tests { let too_large = EmbeddedImageLimits::default().max_bytes() + 1; assert!(!state.insert_captured_image_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), image(too_large), &target )); diff --git a/src/input/state/core/color_picker_popup/mod.rs b/src/input/state/core/color_picker_popup/mod.rs index 97b7ed4f9..a2c6460df 100644 --- a/src/input/state/core/color_picker_popup/mod.rs +++ b/src/input/state/core/color_picker_popup/mod.rs @@ -181,6 +181,7 @@ impl ColorPickerPopupLayoutOptions { show_eyedropper: true, }; + #[must_use] pub fn for_target(target: ColorPickerTarget, show_default_button: bool) -> Self { Self { show_default_button, @@ -280,6 +281,7 @@ impl ColorPickerPopupLayout { /// Compute the layout for given screen dimensions. `show_default_button` /// comes from the popup's target: recoloring a slot with a built-in color /// adds a third button to the bottom row. + #[must_use] pub fn compute( screen_width: u32, screen_height: u32, @@ -403,11 +405,13 @@ impl ColorPickerPopupLayout { } /// Check if a point is within the saturation/value square. + #[must_use] pub fn point_in_sv(&self, x: f64, y: f64) -> bool { x >= self.sv_x && x <= self.sv_x + self.sv_w && y >= self.sv_y && y <= self.sv_y + self.sv_h } /// Check if a point is within the hue bar. + #[must_use] pub fn point_in_hue(&self, x: f64, y: f64) -> bool { x >= self.hue_x && x <= self.hue_x + self.hue_w @@ -416,6 +420,7 @@ impl ColorPickerPopupLayout { } /// Saturation/value the square would yield for a pointer position. + #[must_use] pub fn sv_from_point(&self, x: f64, y: f64) -> (f64, f64) { let saturation = ((x - self.sv_x) / self.sv_w).clamp(0.0, 1.0); let value = 1.0 - ((y - self.sv_y) / self.sv_h).clamp(0.0, 1.0); @@ -423,11 +428,13 @@ impl ColorPickerPopupLayout { } /// Hue the bar would yield for a pointer position. + #[must_use] pub fn hue_from_point(&self, x: f64) -> f64 { ((x - self.hue_x) / self.hue_w).clamp(0.0, 1.0) } /// Check if a point is within the alpha bar. + #[must_use] pub fn point_in_alpha(&self, x: f64, y: f64) -> bool { self.alpha_h > 0.0 && x >= self.alpha_x @@ -437,11 +444,13 @@ impl ColorPickerPopupLayout { } /// Alpha the bar would yield for a pointer position. + #[must_use] pub fn alpha_from_point(&self, x: f64) -> f64 { ((x - self.alpha_x) / self.alpha_w).clamp(0.0, 1.0) } /// Top-left of the recent-color swatch at `index`. + #[must_use] pub fn recent_swatch_origin(&self, index: usize) -> (f64, f64) { let x = self.recents_x + index as f64 * (RECENT_SWATCH_SIZE + RECENT_SWATCH_GAP); (x, self.recents_y) @@ -449,6 +458,7 @@ impl ColorPickerPopupLayout { /// Index of the recent-color swatch under a point, if any. `count` bounds /// the search so an empty tail is not clickable. + #[must_use] pub fn recent_swatch_at(&self, x: f64, y: f64, count: usize) -> Option { if y < self.recents_y || y > self.recents_y + RECENT_SWATCH_SIZE { return None; @@ -460,6 +470,7 @@ impl ColorPickerPopupLayout { } /// Check if a point is within the hex input field. + #[must_use] pub fn point_in_hex_input(&self, x: f64, y: f64) -> bool { x >= self.hex_input_x && x <= self.hex_input_x + self.hex_input_w @@ -468,6 +479,7 @@ impl ColorPickerPopupLayout { } /// Check if a point is within the OK button. + #[must_use] pub fn point_in_ok_button(&self, x: f64, y: f64) -> bool { x >= self.ok_btn_x && x <= self.ok_btn_x + self.btn_width @@ -476,6 +488,7 @@ impl ColorPickerPopupLayout { } /// Check if a point is within the copy-hex button. + #[must_use] pub fn point_in_copy_button(&self, x: f64, y: f64) -> bool { x >= self.copy_btn_x && x <= self.copy_btn_x + self.action_btn_size @@ -484,6 +497,7 @@ impl ColorPickerPopupLayout { } /// Check if a point is within the paste-hex button. + #[must_use] pub fn point_in_paste_button(&self, x: f64, y: f64) -> bool { x >= self.paste_btn_x && x <= self.paste_btn_x + self.action_btn_size @@ -492,6 +506,7 @@ impl ColorPickerPopupLayout { } /// Check if a point is within the screen eyedropper button. + #[must_use] pub fn point_in_eyedropper_button(&self, x: f64, y: f64) -> bool { self.eyedropper_enabled && x >= self.eyedropper_btn_x @@ -502,6 +517,7 @@ impl ColorPickerPopupLayout { /// Check if a point is within the "Default" button. Always false when the /// popup is not recoloring a slot with a built-in color. + #[must_use] pub fn point_in_default_button(&self, x: f64, y: f64) -> bool { let Some((btn_x, btn_y)) = self.default_btn else { return false; @@ -510,6 +526,7 @@ impl ColorPickerPopupLayout { } /// Check if a point is within the Cancel button. + #[must_use] pub fn point_in_cancel_button(&self, x: f64, y: f64) -> bool { x >= self.cancel_btn_x && x <= self.cancel_btn_x + self.btn_width @@ -570,6 +587,7 @@ impl ColorPickerPopupLayout { } /// Check if a point is within the popup panel. + #[must_use] pub fn point_in_panel(&self, x: f64, y: f64) -> bool { x >= self.origin_x && x <= self.origin_x + self.width @@ -583,6 +601,7 @@ impl ColorPickerPopupLayout { /// `recent_count` is the number of recent colors actually shown, the same /// count rendering and activation use. Passing the maximum instead would /// promise a clickable swatch over the empty positions of a fresh session. + #[must_use] pub fn cursor_hint_at(&self, x: f64, y: f64, recent_count: usize) -> ColorPickerCursorHint { if self.point_in_hex_input(x, y) { ColorPickerCursorHint::Text @@ -624,6 +643,7 @@ pub use crate::draw::color::{hsv_to_rgb, rgb_to_hsv}; pub const HEX_INPUT_MAX_CHARS: usize = 9; /// Convert a color to hex string (e.g., "#FF8040"). +#[must_use] pub fn color_to_hex(color: Color) -> String { let alpha = (color.a.clamp(0.0, 1.0) * 255.0).round() as u8; let rgb = format!( @@ -642,6 +662,7 @@ pub fn color_to_hex(color: Color) -> String { } /// Parse a hex color string (e.g., "#FF8040" or "FF8040"). +#[must_use] pub fn parse_hex_color(value: &str) -> Option { let mut hex = value.trim().trim_start_matches("0x"); if hex.starts_with('#') { diff --git a/src/input/state/core/color_picker_popup/state.rs b/src/input/state/core/color_picker_popup/state.rs index 02cb54ed0..c99a8dc57 100644 --- a/src/input/state/core/color_picker_popup/state.rs +++ b/src/input/state/core/color_picker_popup/state.rs @@ -3,10 +3,10 @@ use crate::draw::TextMeasurer; use std::borrow::Cow; +use crate::config; use crate::draw::Color; use crate::i18n::{tr, trf}; -use crate::input::state::InputState; -use crate::input::state::QuickColorEdit; +use crate::input::state::{InputState, QuickColorEdit}; use super::{ ColorPickerPopupAction, ColorPickerPopupLayout, ColorPickerPopupState, ColorPickerTarget, @@ -478,7 +478,7 @@ impl InputState { /// The built-in color of the slot being recolored, when there is one. pub fn color_picker_popup_default_color(&self) -> Option { let index = self.color_picker_popup_slot()?; - crate::config::default_quick_color_for_index(index) + config::default_quick_color_for_index(index) } /// Load the slot's built-in color as the live candidate. The popup stays diff --git a/src/input/state/core/command_palette/input.rs b/src/input/state/core/command_palette/input.rs index 7a8aba61a..236512684 100644 --- a/src/input/state/core/command_palette/input.rs +++ b/src/input/state/core/command_palette/input.rs @@ -1,12 +1,16 @@ +use crate::draw::TextMeasurer; +use crate::input::state::core::modal::ModalSurface; +use crate::input::state::{InputTextResources, ZoomAnchor}; +use crate::ui_text::UiTextEngine; + use super::super::base::{ InputState, KeybindingEditOperation, KeybindingEditRequest, Toast, ToastPriority, }; +use super::CommandPaletteCursorHint; +use super::layout::{CommandPaletteGeometry, CommandPaletteRowAction}; use super::search::CommandPaletteListRow; -use super::{ - CommandPaletteCursorHint, - layout::{CommandPaletteGeometry, CommandPaletteRowAction}, -}; -use crate::config::{KeyBinding, action_label, keybindings::default_keybindings}; +use crate::config::keybindings::default_keybindings; +use crate::config::{KeyBinding, action_label}; use crate::configurator_destination::keybindings_destination_for_action; use crate::domain::Action; use crate::i18n::trf; @@ -109,7 +113,7 @@ impl InputState { } fn open_command_palette_internal(&mut self, track_usage: bool) { - self.close_modals_for_open(crate::input::state::core::modal::ModalSurface::CommandPalette); + self.close_modals_for_open(ModalSurface::CommandPalette); self.command_palette.open(); self.reconcile_command_palette_scroll(); self.clear_command_palette_repeat(); @@ -136,7 +140,7 @@ impl InputState { /// Returns true if the key was handled. pub(crate) fn handle_command_palette_key_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, ) -> bool { if !self.command_palette_is_engaged() { @@ -177,7 +181,7 @@ impl InputState { self.handle_action_anchored( resources, command.action, - crate::input::state::ZoomAnchor::ScreenCenter, + ZoomAnchor::ScreenCenter, ); } true @@ -343,8 +347,8 @@ impl InputState { screen_width: u32, screen_height: u32, ) -> bool { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.handle_command_palette_click_with_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -359,7 +363,7 @@ impl InputState { pub(crate) fn handle_command_palette_click_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, x: i32, y: i32, screen_width: u32, @@ -447,11 +451,7 @@ impl InputState { Toast::info(command.label).duration_ms(self.command_palette_toast_duration_ms()), ); - self.handle_action_anchored( - resources, - command.action, - crate::input::state::ZoomAnchor::ScreenCenter, - ); + self.handle_action_anchored(resources, command.action, ZoomAnchor::ScreenCenter); return true; } diff --git a/src/input/state/core/command_palette/mod.rs b/src/input/state/core/command_palette/mod.rs index 9ab5a42bf..d4df5666a 100644 --- a/src/input/state/core/command_palette/mod.rs +++ b/src/input/state/core/command_palette/mod.rs @@ -38,7 +38,15 @@ mod tests { use crate::config::KeybindingsConfig; use crate::config::keybindings::Action; - use crate::input::InputState; + use crate::draw::TextMeasurer; + use crate::input::events::Key; + use crate::input::state::core::base::UiToastKind; + use crate::input::state::{ + KeybindingEditOperation, KeybindingEditRequest, PendingBackendAction, test_support, + }; + use crate::input::{InputState, ZoomAction, ZoomAnchor, ZoomRequest}; + use crate::ui_text::UiTextEngine; + use crate::{configurator_destination, i18n, test_env, ui}; use search::command_palette_display_index; use std::collections::HashSet; use std::time::{Duration, Instant}; @@ -94,8 +102,8 @@ mod tests { #[test] fn navigation_and_resize_keep_selection_in_the_viewport() { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, @@ -105,10 +113,7 @@ mod tests { for (width, height) in [(800, 480), (1024, 600), (1280, 720), (240, 320), (240, 180)] { state.update_screen_dimensions(width, height); for _ in 0..24 { - state.handle_command_palette_key_with_resources( - resources, - crate::input::events::Key::Down, - ); + state.handle_command_palette_key_with_resources(resources, Key::Down); let rows = state.command_palette_rows(); let geometry = state.command_palette_geometry_for_rows(width, height, &rows); let display = command_palette_display_index(&rows, state.command_palette.selected); @@ -124,7 +129,7 @@ mod tests { .build_action_bindings() .expect("default keybindings bindings"); - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.set_action_bindings(action_bindings); state } @@ -133,32 +138,22 @@ mod tests { /// owns the keymap; the modal closes itself in the same step. #[test] fn shortcut_capture_emits_a_replace_request() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, }; let mut state = make_state(); assert!(state.begin_keybinding_capture(Action::SelectPenTool)); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Ctrl - ) - ); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('p') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Ctrl)); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('p'))); assert_eq!( state.take_pending_keybinding_edits(), - vec![crate::input::state::KeybindingEditRequest { + vec![KeybindingEditRequest { action: Action::SelectPenTool, - operation: crate::input::state::KeybindingEditOperation::Replace(vec![ - "Ctrl+P".to_string() - ]), + operation: KeybindingEditOperation::Replace(vec!["Ctrl+P".to_string()]), }] ); assert_eq!(state.keybinding_capture_action(), None); @@ -172,8 +167,8 @@ mod tests { /// Recording them in a slot dropped the first with nothing said about it. #[test] fn two_chords_captured_before_a_drain_both_survive_in_order() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -181,41 +176,23 @@ mod tests { let mut state = make_state(); assert!(state.begin_keybinding_capture(Action::SelectPenTool)); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Ctrl - ) - ); - assert!( - state - .handle_command_palette_key_with_resources(route_resources, crate::input::Key::Alt) - ); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('p') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Ctrl)); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Alt)); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('p'))); assert!(state.begin_keybinding_capture(Action::SelectMarkerTool)); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('m') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('m'))); assert_eq!( state.take_pending_keybinding_edits(), vec![ - crate::input::state::KeybindingEditRequest { + KeybindingEditRequest { action: Action::SelectPenTool, - operation: crate::input::state::KeybindingEditOperation::Replace(vec![ - "Ctrl+Alt+P".to_string() - ]), + operation: KeybindingEditOperation::Replace(vec!["Ctrl+Alt+P".to_string()]), }, - crate::input::state::KeybindingEditRequest { + KeybindingEditRequest { action: Action::SelectMarkerTool, - operation: crate::input::state::KeybindingEditOperation::Replace(vec![ - "Ctrl+Alt+M".to_string() - ]), + operation: KeybindingEditOperation::Replace(vec!["Ctrl+Alt+M".to_string()]), }, ], "both captures must reach the backend, oldest first" @@ -229,8 +206,8 @@ mod tests { /// Escape leaves the modal without queueing anything. #[test] fn shortcut_capture_is_cancelled_by_escape() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -239,12 +216,7 @@ mod tests { state.toggle_command_palette(); assert!(state.begin_keybinding_capture(Action::SelectPenTool)); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Escape - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Escape)); assert_eq!(state.keybinding_capture_action(), None); assert!(state.take_pending_keybinding_edits().is_empty()); @@ -255,8 +227,8 @@ mod tests { /// the command. #[test] fn ctrl_e_starts_capture_for_the_selected_row() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -268,10 +240,7 @@ mod tests { assert_eq!(action, Action::SelectPenTool); state.modifiers.ctrl = true; - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('e') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('e'))); assert_eq!(state.keybinding_capture_action(), Some(action)); assert!(state.take_pending_keybinding_edits().is_empty()); @@ -282,8 +251,8 @@ mod tests { /// the modifier flags directly. #[test] fn shift_held_through_the_palette_turns_ctrl_e_into_the_configurator_route() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -292,23 +261,10 @@ mod tests { state.toggle_command_palette(); state.command_palette.query = "pen tool".to_string(); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Ctrl - ) - ); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Shift - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Ctrl)); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Shift)); assert!(state.modifiers.shift, "the palette must track Shift itself"); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('E') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('E'))); assert_eq!( state.keybinding_capture_action(), @@ -319,14 +275,11 @@ mod tests { assert!(!state.command_palette.open, "launching closes the overlay"); // Releasing puts the plain Ctrl+E arm back in charge. - state.on_key_release(crate::input::Key::Shift); + state.on_key_release(Key::Shift); assert!(!state.modifiers.shift); state.toggle_command_palette(); state.command_palette.query = "pen tool".to_string(); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('e') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('e'))); assert_eq!( state.keybinding_capture_action(), Some(Action::SelectPenTool) @@ -339,8 +292,8 @@ mod tests { /// driven here as a real press instead of a flag. #[test] fn alt_held_through_the_palette_reaches_the_captured_chord() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -350,51 +303,35 @@ mod tests { state.command_palette.query = "pen tool".to_string(); // Alt goes down while the list, not the modal, owns the keyboard. - assert!( - state - .handle_command_palette_key_with_resources(route_resources, crate::input::Key::Alt) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Alt)); assert!(state.modifiers.alt, "the palette must track Alt itself"); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Ctrl - ) - ); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('e') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Ctrl)); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('e'))); assert_eq!( state.keybinding_capture_action(), Some(Action::SelectPenTool) ); // Still held when the modal reads the chord. - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('k') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('k'))); assert_eq!( state.take_pending_keybinding_edits(), - vec![crate::input::state::KeybindingEditRequest { + vec![KeybindingEditRequest { action: Action::SelectPenTool, - operation: crate::input::state::KeybindingEditOperation::Replace(vec![ - "Ctrl+Alt+K".to_string() - ]), + operation: KeybindingEditOperation::Replace(vec!["Ctrl+Alt+K".to_string()]), }] ); // Release clears it again, so the next chord starts from nothing. - state.on_key_release(crate::input::Key::Alt); + state.on_key_release(Key::Alt); assert!(!state.modifiers.alt); } #[test] fn super_held_through_the_palette_reaches_the_captured_chord() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -403,50 +340,32 @@ mod tests { state.toggle_command_palette(); state.command_palette.query = "pen tool".to_string(); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Super - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Super)); assert!(state.modifiers.logo, "the palette must track Super itself"); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Ctrl - ) - ); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('e') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Ctrl)); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('e'))); assert_eq!( state.keybinding_capture_action(), Some(Action::SelectPenTool) ); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('k') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('k'))); assert_eq!( state.take_pending_keybinding_edits(), - vec![crate::input::state::KeybindingEditRequest { + vec![KeybindingEditRequest { action: Action::SelectPenTool, - operation: crate::input::state::KeybindingEditOperation::Replace(vec![ - "Ctrl+Super+K".to_string() - ]), + operation: KeybindingEditOperation::Replace(vec!["Ctrl+Super+K".to_string()]), }] ); - state.on_key_release(crate::input::Key::Super); + state.on_key_release(Key::Super); assert!(!state.modifiers.logo); } #[test] fn palette_shortcut_controls_request_delete_and_reset() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -457,29 +376,21 @@ mod tests { let action = state.selected_command().expect("selected command").action; state.modifiers.ctrl = true; - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Delete - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Delete)); assert_eq!( state.take_pending_keybinding_edits(), - vec![crate::input::state::KeybindingEditRequest { + vec![KeybindingEditRequest { action, - operation: crate::input::state::KeybindingEditOperation::Delete, + operation: KeybindingEditOperation::Delete, }] ); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('r') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('r'))); assert_eq!( state.take_pending_keybinding_edits(), - vec![crate::input::state::KeybindingEditRequest { + vec![KeybindingEditRequest { action, - operation: crate::input::state::KeybindingEditOperation::Reset, + operation: KeybindingEditOperation::Reset, }] ); } @@ -536,9 +447,8 @@ mod tests { ); } - let (_, _, visual_width, _) = - crate::ui::command_palette_visual_geometry(&state, 1920, 1000) - .expect("palette plus tooltip geometry"); + let (_, _, visual_width, _) = ui::command_palette_visual_geometry(&state, 1920, 1000) + .expect("palette plus tooltip geometry"); assert!(visual_width > geometry.width + 4.0); } @@ -548,8 +458,8 @@ mod tests { /// onboarding store, so the action hands the replay to the backend. #[test] fn replay_tour_asks_the_backend_to_replay_the_first_run_cards() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -560,7 +470,7 @@ mod tests { assert_eq!( state.take_pending_backend_action(), - Some(crate::input::state::PendingBackendAction::ReplayFirstRunTour) + Some(PendingBackendAction::ReplayFirstRunTour) ); } @@ -580,10 +490,9 @@ mod tests { "Replay Tour has no configurable keyboard shortcut." ); - assert!(!state.request_keybinding_edit( - Action::ReplayTour, - crate::input::state::KeybindingEditOperation::Delete - )); + assert!( + !state.request_keybinding_edit(Action::ReplayTour, KeybindingEditOperation::Delete) + ); assert!(state.take_pending_keybinding_edits().is_empty()); assert!(!state.open_configurator_for_shortcut(Action::ReplayTour)); @@ -592,8 +501,8 @@ mod tests { #[test] fn ctrl_shift_e_requests_the_rows_keybindings_section() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -609,10 +518,7 @@ mod tests { state.modifiers.ctrl = true; state.modifiers.shift = true; - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('e') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('e'))); assert_eq!( state.take_pending_backend_action(), Some(PendingBackendAction::HelperLaunch( @@ -650,7 +556,7 @@ mod tests { #[test] fn language_route_is_searchable_unbound_and_opens_general_ui() { - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::language_route_is_searchable_unbound_and_opens_general_ui" @@ -660,7 +566,7 @@ mod tests { use crate::input::state::{HelperLaunchRequest, PendingBackendAction}; for language in [Language::English, Language::SimplifiedChinese] { - crate::i18n::set_language(language); + i18n::set_language(language); let mut state = make_state(); assert!( state @@ -679,8 +585,8 @@ mod tests { ); } - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, @@ -690,7 +596,7 @@ mod tests { state.take_pending_backend_action(), Some(PendingBackendAction::HelperLaunch( HelperLaunchRequest::Configurator(Some( - crate::configurator_destination::language_destination() + configurator_destination::language_destination() )) )) ); @@ -744,10 +650,7 @@ mod tests { let results = state.filtered_commands(); assert!(!results.is_empty()); - assert_eq!( - results[0].action, - crate::config::keybindings::Action::ToggleFrozenMode - ); + assert_eq!(results[0].action, Action::ToggleFrozenMode); } #[test] @@ -757,30 +660,20 @@ mod tests { let results = state.filtered_commands(); assert!(!results.is_empty()); - assert_eq!( - results[0].action, - crate::config::keybindings::Action::CaptureFileFull - ); + assert_eq!(results[0].action, Action::CaptureFileFull); } #[test] fn recent_commands_rank_first_for_empty_query() { let mut state = make_state(); - state.record_command_palette_action(crate::config::keybindings::Action::CaptureFileFull); - state - .record_command_palette_action(crate::config::keybindings::Action::TogglePresenterMode); + state.record_command_palette_action(Action::CaptureFileFull); + state.record_command_palette_action(Action::TogglePresenterMode); state.command_palette.query.clear(); let results = state.filtered_commands(); assert!(results.len() >= 2); - assert_eq!( - results[0].action, - crate::config::keybindings::Action::TogglePresenterMode - ); - assert_eq!( - results[1].action, - crate::config::keybindings::Action::CaptureFileFull - ); + assert_eq!(results[0].action, Action::TogglePresenterMode); + assert_eq!(results[1].action, Action::CaptureFileFull); } #[test] @@ -848,10 +741,9 @@ mod tests { state.command_palette.query = "monitor".to_string(); let results = state.filtered_commands(); - let actions: HashSet = - results.iter().map(|cmd| cmd.action).collect(); - assert!(actions.contains(&crate::config::keybindings::Action::FocusNextOutput)); - assert!(actions.contains(&crate::config::keybindings::Action::FocusPrevOutput)); + let actions: HashSet = results.iter().map(|cmd| cmd.action).collect(); + assert!(actions.contains(&Action::FocusNextOutput)); + assert!(actions.contains(&Action::FocusPrevOutput)); } #[test] @@ -860,10 +752,9 @@ mod tests { state.command_palette.query = "display".to_string(); let results = state.filtered_commands(); - let actions: HashSet = - results.iter().map(|cmd| cmd.action).collect(); - assert!(actions.contains(&crate::config::keybindings::Action::FocusNextOutput)); - assert!(actions.contains(&crate::config::keybindings::Action::FocusPrevOutput)); + let actions: HashSet = results.iter().map(|cmd| cmd.action).collect(); + assert!(actions.contains(&Action::FocusNextOutput)); + assert!(actions.contains(&Action::FocusPrevOutput)); } #[test] @@ -873,10 +764,7 @@ mod tests { let results = state.filtered_commands(); assert!(!results.is_empty()); - assert_eq!( - results[0].action, - crate::config::keybindings::Action::ToggleRadialMenu - ); + assert_eq!(results[0].action, Action::ToggleRadialMenu); } #[test] @@ -886,10 +774,7 @@ mod tests { let results = state.filtered_commands(); assert!(!results.is_empty()); - assert_eq!( - results[0].action, - crate::config::keybindings::Action::OpenConfigurator - ); + assert_eq!(results[0].action, Action::OpenConfigurator); } #[test] @@ -899,10 +784,7 @@ mod tests { let results = state.filtered_commands(); assert!(!results.is_empty()); - assert_eq!( - results[0].action, - crate::config::keybindings::Action::CaptureFileFull - ); + assert_eq!(results[0].action, Action::CaptureFileFull); } #[test] @@ -933,8 +815,8 @@ mod tests { #[test] fn backspace_resets_selection_and_scroll_when_query_changes() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -945,10 +827,7 @@ mod tests { state.command_palette.selected = 4; state.command_palette.scroll = 3; - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Backspace - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Backspace)); assert_eq!(state.command_palette.query, "zoo"); assert_eq!(state.command_palette.selected, 0); assert_eq!(state.command_palette.scroll, 0); @@ -956,8 +835,8 @@ mod tests { #[test] fn ctrl_backspace_deletes_previous_query_word_and_resets_position() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -968,16 +847,8 @@ mod tests { state.command_palette.selected = 4; state.command_palette.scroll = 3; - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Ctrl - ) - ); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Backspace - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Ctrl)); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Backspace)); assert_eq!(state.command_palette.query, "export canvas "); assert_eq!(state.command_palette.selected, 0); @@ -986,8 +857,8 @@ mod tests { #[test] fn ctrl_backspace_stops_at_shortcut_token_separator() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -996,24 +867,16 @@ mod tests { state.toggle_command_palette(); state.command_palette.query = "ctrl+shift+f".to_string(); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Ctrl - ) - ); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Backspace - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Ctrl)); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Backspace)); assert_eq!(state.command_palette.query, "ctrl+shift+"); } #[test] fn ctrl_backspace_stops_at_slash_token_separator() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1022,24 +885,16 @@ mod tests { state.toggle_command_palette(); state.command_palette.query = "capture/file".to_string(); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Ctrl - ) - ); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Backspace - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Ctrl)); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Backspace)); assert_eq!(state.command_palette.query, "capture/"); } #[test] fn ctrl_u_clears_query_and_resets_position() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1050,16 +905,8 @@ mod tests { state.command_palette.selected = 4; state.command_palette.scroll = 3; - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Ctrl - ) - ); - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('u') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Ctrl)); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('u'))); assert!(state.command_palette.query.is_empty()); assert_eq!(state.command_palette.selected, 0); @@ -1068,8 +915,8 @@ mod tests { #[test] fn down_key_keeps_selection_visible_while_scrolling_past_headers() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1082,10 +929,7 @@ mod tests { let mut scrolled = false; for step in 0..COMMAND_PALETTE_MAX_VISIBLE * 2 { - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Down - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Down)); // Selection advances one command per press. assert_eq!(state.command_palette.selected, step + 1); // The selected command's display row stays inside the visible window @@ -1106,8 +950,8 @@ mod tests { #[test] fn home_key_jumps_to_first_command() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1117,12 +961,7 @@ mod tests { state.command_palette.selected = 5; state.command_palette.scroll = 3; - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Home - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Home)); assert_eq!(state.command_palette.selected, 0); assert_eq!(state.command_palette.scroll, 0); @@ -1130,8 +969,8 @@ mod tests { #[test] fn end_key_jumps_to_last_command_and_scrolls_into_view() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1141,10 +980,7 @@ mod tests { let filtered_len = state.filtered_commands().len(); assert!(filtered_len > COMMAND_PALETTE_MAX_VISIBLE); - assert!( - state - .handle_command_palette_key_with_resources(route_resources, crate::input::Key::End) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::End)); assert_eq!(state.command_palette.selected, filtered_len - 1); // Scroll is measured in display rows (headers included), so the bottom of @@ -1162,8 +998,8 @@ mod tests { #[test] fn held_down_key_repeats_after_delay_until_release() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1171,12 +1007,7 @@ mod tests { let mut state = make_state(); state.toggle_command_palette(); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Down - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Down)); assert_eq!(state.command_palette.selected, 1); assert!( state @@ -1187,7 +1018,7 @@ mod tests { assert!(state.tick_command_palette_repeat(Instant::now() + Duration::from_secs(1))); assert_eq!(state.command_palette.selected, 2); - state.on_key_release(crate::input::Key::Down); + state.on_key_release(Key::Down); assert!( state .command_palette_repeat_timeout(Instant::now()) @@ -1200,7 +1031,7 @@ mod tests { #[test] fn record_command_palette_action_notes_shortcut_coach_slow_path() { let mut state = make_state(); - let action = crate::config::keybindings::Action::Undo; + let action = Action::Undo; assert!( state.shortcut_for_action(action).is_some(), "test relies on Undo having a default shortcut" @@ -1222,23 +1053,20 @@ mod tests { #[test] fn repeated_recent_action_moves_to_front_without_duplication() { let mut state = make_state(); - state.record_command_palette_action(crate::config::keybindings::Action::CaptureFileFull); - state.record_command_palette_action(crate::config::keybindings::Action::ToggleHelp); - state.record_command_palette_action(crate::config::keybindings::Action::CaptureFileFull); + state.record_command_palette_action(Action::CaptureFileFull); + state.record_command_palette_action(Action::ToggleHelp); + state.record_command_palette_action(Action::CaptureFileFull); assert_eq!( state.command_palette.recent, - vec![ - crate::config::keybindings::Action::CaptureFileFull, - crate::config::keybindings::Action::ToggleHelp, - ] + vec![Action::CaptureFileFull, Action::ToggleHelp,] ); } #[test] fn escape_key_closes_command_palette() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1247,19 +1075,14 @@ mod tests { state.toggle_command_palette(); assert!(state.command_palette.open); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Escape - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Escape)); assert!(!state.command_palette.open); } #[test] fn return_key_executes_selected_command_and_records_it() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1268,23 +1091,15 @@ mod tests { state.toggle_command_palette(); state.command_palette.query = "status bar".to_string(); let selected = state.selected_command().expect("selected command"); - assert_eq!( - selected.action, - crate::config::keybindings::Action::ToggleStatusBar - ); + assert_eq!(selected.action, Action::ToggleStatusBar); assert!(state.ui_visibility.show_status_bar); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Return - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Return)); assert!(!state.command_palette.open); assert!(!state.ui_visibility.show_status_bar); assert_eq!( state.command_palette.recent.first().copied(), - Some(crate::config::keybindings::Action::ToggleStatusBar) + Some(Action::ToggleStatusBar) ); } @@ -1292,8 +1107,8 @@ mod tests { /// wants magnified, so a zoom it runs centres the screen, not the pointer. #[test] fn return_key_runs_palette_zoom_around_the_screen_centre() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1302,20 +1117,15 @@ mod tests { state.toggle_command_palette(); state.command_palette.query = "zoom in".to_string(); let selected = state.selected_command().expect("selected command"); - assert_eq!(selected.action, crate::config::keybindings::Action::ZoomIn); + assert_eq!(selected.action, Action::ZoomIn); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Return - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Return)); assert_eq!( state.take_pending_zoom_request(), - Some(crate::input::ZoomRequest { - action: crate::input::ZoomAction::In, - anchor: crate::input::ZoomAnchor::ScreenCenter, + Some(ZoomRequest { + action: ZoomAction::In, + anchor: ZoomAnchor::ScreenCenter, }) ); assert_eq!( @@ -1326,8 +1136,8 @@ mod tests { #[test] fn return_key_sets_pending_canvas_export_backend_action() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1336,30 +1146,22 @@ mod tests { state.toggle_command_palette(); state.command_palette.query = "export canvas clipboard".to_string(); let selected = state.selected_command().expect("selected command"); - assert_eq!( - selected.action, - crate::config::keybindings::Action::ExportCanvasClipboard - ); + assert_eq!(selected.action, Action::ExportCanvasClipboard); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Return - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Return)); assert_eq!( state.take_pending_backend_action(), - Some(crate::input::state::PendingBackendAction::CanvasExport( - crate::config::keybindings::Action::ExportCanvasClipboard + Some(PendingBackendAction::CanvasExport( + Action::ExportCanvasClipboard )) ); } #[test] fn return_key_sets_pending_board_pdf_export_backend_action() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1368,30 +1170,22 @@ mod tests { state.toggle_command_palette(); state.command_palette.query = "export pdf".to_string(); let selected = state.selected_command().expect("selected command"); - assert_eq!( - selected.action, - crate::config::keybindings::Action::ExportBoardPdfFile - ); + assert_eq!(selected.action, Action::ExportBoardPdfFile); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Return - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Return)); assert_eq!( state.take_pending_backend_action(), - Some(crate::input::state::PendingBackendAction::BoardPdfExport( - crate::config::keybindings::Action::ExportBoardPdfFile + Some(PendingBackendAction::BoardPdfExport( + Action::ExportBoardPdfFile )) ); } #[test] fn return_key_sets_pending_all_boards_pdf_export_backend_action() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1400,30 +1194,22 @@ mod tests { state.toggle_command_palette(); state.command_palette.query = "all boards pdf".to_string(); let selected = state.selected_command().expect("selected command"); - assert_eq!( - selected.action, - crate::config::keybindings::Action::ExportAllBoardsPdfFile - ); + assert_eq!(selected.action, Action::ExportAllBoardsPdfFile); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Return - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Return)); assert_eq!( state.take_pending_backend_action(), - Some(crate::input::state::PendingBackendAction::BoardPdfExport( - crate::config::keybindings::Action::ExportAllBoardsPdfFile + Some(PendingBackendAction::BoardPdfExport( + Action::ExportAllBoardsPdfFile )) ); } #[test] fn return_key_sets_pending_clear_saved_tool_state_backend_action() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1432,21 +1218,13 @@ mod tests { state.toggle_command_palette(); state.command_palette.query = "clear saved tool state".to_string(); let selected = state.selected_command().expect("selected command"); - assert_eq!( - selected.action, - crate::config::keybindings::Action::ClearSavedToolState - ); + assert_eq!(selected.action, Action::ClearSavedToolState); - assert!( - state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Return - ) - ); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Return)); assert_eq!( state.take_pending_backend_action(), - Some(crate::input::state::PendingBackendAction::ClearSavedToolState) + Some(PendingBackendAction::ClearSavedToolState) ); } @@ -1461,8 +1239,8 @@ mod tests { #[test] fn char_key_appends_query_and_resets_selection_and_scroll() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -1472,10 +1250,7 @@ mod tests { state.command_palette.selected = 3; state.command_palette.scroll = 2; - assert!(state.handle_command_palette_key_with_resources( - route_resources, - crate::input::Key::Char('z') - )); + assert!(state.handle_command_palette_key_with_resources(route_resources, Key::Char('z'))); assert_eq!(state.command_palette.query, "z"); assert_eq!(state.command_palette.selected, 0); assert_eq!(state.command_palette.scroll, 0); @@ -1506,10 +1281,7 @@ mod tests { state.command_palette.selected = 0; let filtered = state.filtered_commands(); let selected = filtered.first().expect("selected command"); - assert_eq!( - selected.action, - crate::config::keybindings::Action::ToggleStatusBar - ); + assert_eq!(selected.action, Action::ToggleStatusBar); let (x, y) = command_row_click_point(&state, 0, 1920, 1000); assert!(state.ui_visibility.show_status_bar); @@ -1517,10 +1289,7 @@ mod tests { assert!(!state.command_palette.open); assert!(!state.ui_visibility.show_status_bar); let toast = state.active_toast().expect("command toast"); - assert_eq!( - toast.kind, - crate::input::state::core::base::UiToastKind::Info - ); + assert_eq!(toast.kind, UiToastKind::Info); assert_eq!(toast.message, selected.label); } @@ -1533,7 +1302,7 @@ mod tests { let filtered = state.filtered_commands(); assert_eq!( filtered.first().expect("selected command").action, - crate::config::keybindings::Action::ExportCanvasFile + Action::ExportCanvasFile ); let (x, y) = command_row_click_point(&state, 0, 1920, 1000); @@ -1541,9 +1310,7 @@ mod tests { assert_eq!( state.take_pending_backend_action(), - Some(crate::input::state::PendingBackendAction::CanvasExport( - crate::config::keybindings::Action::ExportCanvasFile - )) + Some(PendingBackendAction::CanvasExport(Action::ExportCanvasFile)) ); } @@ -1556,7 +1323,7 @@ mod tests { let filtered = state.filtered_commands(); assert_eq!( filtered.first().expect("selected command").action, - crate::config::keybindings::Action::ExportBoardPdfFile + Action::ExportBoardPdfFile ); let (x, y) = command_row_click_point(&state, 0, 1920, 1000); @@ -1564,8 +1331,8 @@ mod tests { assert_eq!( state.take_pending_backend_action(), - Some(crate::input::state::PendingBackendAction::BoardPdfExport( - crate::config::keybindings::Action::ExportBoardPdfFile + Some(PendingBackendAction::BoardPdfExport( + Action::ExportBoardPdfFile )) ); } @@ -1579,7 +1346,7 @@ mod tests { let filtered = state.filtered_commands(); assert_eq!( filtered.first().expect("selected command").action, - crate::config::keybindings::Action::ExportAllBoardsPdfFile + Action::ExportAllBoardsPdfFile ); let (x, y) = command_row_click_point(&state, 0, 1920, 1000); @@ -1587,8 +1354,8 @@ mod tests { assert_eq!( state.take_pending_backend_action(), - Some(crate::input::state::PendingBackendAction::BoardPdfExport( - crate::config::keybindings::Action::ExportAllBoardsPdfFile + Some(PendingBackendAction::BoardPdfExport( + Action::ExportAllBoardsPdfFile )) ); } @@ -1625,8 +1392,8 @@ mod tests { let mut state = make_state(); state.toggle_command_palette(); - let (x, y, width, height) = crate::ui::command_palette_visual_geometry(&state, 3840, 2160) - .expect("open palette geometry"); + let (x, y, width, height) = + ui::command_palette_visual_geometry(&state, 3840, 2160).expect("open palette geometry"); assert!(x > 0.0 && y > 0.0); assert!(width > 0.0 && width < 3840.0); diff --git a/src/input/state/core/command_palette/search.rs b/src/input/state/core/command_palette/search.rs index 94bd146db..8869d7ba2 100644 --- a/src/input/state/core/command_palette/search.rs +++ b/src/input/state/core/command_palette/search.rs @@ -1,3 +1,6 @@ +use crate::domain::Language; +use crate::i18n; + use super::super::base::InputState; use super::empty_query::{EmptyQueryGroups, empty_query_commands, empty_query_groups}; use super::{CommandEntry, CommandPaletteState, command_palette_entries}; @@ -29,6 +32,7 @@ pub enum CommandPaletteListRow { } impl CommandPaletteListRow { + #[must_use] pub fn command_index(&self) -> Option { match self { Self::Header(_) => None, @@ -42,7 +46,7 @@ impl CommandPaletteListRow { #[derive(Debug)] pub(in crate::input::state::core) struct CommandPaletteResults { pub(super) query: String, - pub(super) language: crate::domain::Language, + pub(super) language: Language, pub(super) recents: Vec, pub(super) keymap_revision: u64, pub(super) results: Vec<&'static CommandEntry>, @@ -333,7 +337,7 @@ fn action_category_score(category: ActionCategory, query: &str) -> i32 { let source = fuzzy_score(query, action_category_name(category)); // English headings are different words from the keys (Interface / ui, // Boards / board). Adding them would change existing English results. - if crate::i18n::language() == crate::domain::Language::English { + if i18n::language() == Language::English { source } else { source.max(fuzzy_score(query, action_category_display_name(category))) @@ -485,6 +489,8 @@ mod tests { #[cfg(test)] mod cache_tests { + use std::collections::HashMap; + use crate::config::keybindings::Action; use crate::input::state::test_support::make_test_input_state; @@ -526,7 +532,7 @@ mod cache_tests { // Shortcut labels are scored, so replacing the keymap must invalidate. state.command_palette.query = "undo".to_string(); let _ = state.filtered_commands(); - state.set_action_bindings(std::collections::HashMap::new()); + state.set_action_bindings(HashMap::new()); assert!( !state.filtered_commands().is_empty(), "a keymap change must produce a fresh ranking, not a stale one" diff --git a/src/input/state/core/command_palette/search/language_tests.rs b/src/input/state/core/command_palette/search/language_tests.rs index fdfe0855a..ebba51e23 100644 --- a/src/input/state/core/command_palette/search/language_tests.rs +++ b/src/input/state/core/command_palette/search/language_tests.rs @@ -1,3 +1,8 @@ +use std::collections::HashMap; + +use crate::config::{KeybindingsConfig, Shortcut}; +use crate::{config, test_env}; + use super::*; use crate::config::action_meta::{action_description, action_label}; use crate::domain::Language; @@ -5,16 +10,16 @@ use crate::input::state::test_support::make_test_input_state; #[test] fn cached_results_follow_the_active_language() { - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::cached_results_follow_the_active_language" ), || { let mut state = make_test_input_state(); - state.set_action_bindings(std::collections::HashMap::new()); + state.set_action_bindings(HashMap::new()); state.command_palette.query = "撤销".to_string(); - crate::i18n::set_language(Language::English); + i18n::set_language(Language::English); assert!( !state .filtered_commands() @@ -22,7 +27,7 @@ fn cached_results_follow_the_active_language() { .any(|entry| entry.action == Action::Undo) ); - crate::i18n::set_language(Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); assert_eq!( state.filtered_commands().first().map(|entry| entry.action), Some(Action::Undo) @@ -33,20 +38,20 @@ fn cached_results_follow_the_active_language() { #[test] fn chinese_palette_search_matches_labels_categories_and_mixed_terms() { - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::chinese_palette_search_matches_labels_categories_and_mixed_terms" ), || { - crate::i18n::set_language(Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); let mut state = make_test_input_state(); - let mut bindings = crate::config::KeybindingsConfig::default() + let mut bindings = KeybindingsConfig::default() .build_action_bindings() .expect("default bindings"); bindings.insert( Action::ClearCanvas, - vec![crate::config::Shortcut::parse("Ctrl+Alt+ArrowLeft").expect("shortcut")], + vec![Shortcut::parse("Ctrl+Alt+ArrowLeft").expect("shortcut")], ); state.set_action_bindings(bindings); @@ -92,16 +97,16 @@ fn chinese_palette_search_matches_labels_categories_and_mixed_terms() { #[test] fn english_search_keeps_its_category_keys_and_optional_short_label_scores() { - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::english_search_keeps_its_category_keys_and_optional_short_label_scores" ), || { - let help = crate::config::action_meta(Action::ToggleHelp).expect("help metadata"); - let next = crate::config::action_meta(Action::BoardNext).expect("board metadata"); + let help = config::action_meta(Action::ToggleHelp).expect("help metadata"); + let next = config::action_meta(Action::BoardNext).expect("board metadata"); for language in [Language::English, Language::SimplifiedChinese] { - crate::i18n::set_language(language); + i18n::set_language(language); assert!(help.short_label.is_none()); assert_eq!(action_meta_token_score(help, "help"), 75); assert_eq!(action_meta_query_bonus(help, "help"), 150); @@ -113,9 +118,9 @@ fn english_search_keeps_its_category_keys_and_optional_short_label_scores() { assert_eq!(action_meta_token_score(next, "下一步"), 100); assert_eq!(action_meta_query_bonus(next, "下一步"), 100); - crate::i18n::set_language(Language::English); + i18n::set_language(Language::English); let mut state = make_test_input_state(); - state.set_action_bindings(std::collections::HashMap::new()); + state.set_action_bindings(HashMap::new()); for (query, excluded) in [ ("interface", Action::ToggleHelp), ("boards", Action::BoardNext), @@ -127,7 +132,7 @@ fn english_search_keeps_its_category_keys_and_optional_short_label_scores() { .iter() .any(|entry| entry.action == excluded) ); - let meta = crate::config::action_meta(excluded).expect("metadata"); + let meta = config::action_meta(excluded).expect("metadata"); assert_eq!(action_meta_token_score(meta, query), 0); assert_eq!(action_meta_query_bonus(meta, query), 0); } diff --git a/src/input/state/core/command_palette/state.rs b/src/input/state/core/command_palette/state.rs index 28060c5bb..8621f0eaa 100644 --- a/src/input/state/core/command_palette/state.rs +++ b/src/input/state/core/command_palette/state.rs @@ -1,3 +1,9 @@ +use crate::i18n; +use crate::input::Key; + +use std::mem; +use std::time::{Duration, Instant}; + use super::CommandPaletteResults; use crate::config::Action; use crate::input::state::core::key_repeat::OverlayKeyRepeat; @@ -92,7 +98,7 @@ impl CommandPaletteState { let cached = self.results.borrow(); let cached = cached.as_ref()?; (cached.query == self.query - && cached.language == crate::i18n::language() + && cached.language == i18n::language() && cached.keymap_revision == revision && cached.recents == self.recent) .then(|| cached.results.clone()) @@ -100,7 +106,7 @@ impl CommandPaletteState { pub(super) fn cache_results(&self, revision: u64, results: Vec<&'static super::CommandEntry>) { *self.results.borrow_mut() = Some(CommandPaletteResults { query: self.query.clone(), - language: crate::i18n::language(), + language: i18n::language(), recents: self.recent.clone(), keymap_revision: revision, results, @@ -135,7 +141,7 @@ impl CommandPaletteState { } pub(super) fn take_recents_dirty(&mut self) -> bool { - std::mem::take(&mut self.recents_dirty) + mem::take(&mut self.recents_dirty) } pub(super) fn clear_recents_dirty(&mut self) { @@ -195,7 +201,7 @@ impl CommandPaletteState { pub(super) fn navigate( &mut self, - key: crate::input::Key, + key: Key, rows: &[super::CommandPaletteListRow], capacity: usize, ) -> bool { @@ -293,17 +299,16 @@ impl CommandPaletteState { } true } - pub(super) fn start_repeat(&mut self, key: crate::input::Key, now: std::time::Instant) { - self.repeat - .start(key, now, std::time::Duration::from_millis(280)); + pub(super) fn start_repeat(&mut self, key: Key, now: Instant) { + self.repeat.start(key, now, Duration::from_millis(280)); } pub(crate) fn clear_repeat(&mut self) { self.repeat.clear(); } - pub(crate) fn release_repeat(&mut self, key: crate::input::Key) { + pub(crate) fn release_repeat(&mut self, key: Key) { self.repeat.release(key); } - pub(super) fn repeat_timeout(&self, now: std::time::Instant) -> Option { + pub(super) fn repeat_timeout(&self, now: Instant) -> Option { if self.open { self.repeat.timeout(now) } else { @@ -312,7 +317,7 @@ impl CommandPaletteState { } pub(super) fn tick_repeat( &mut self, - now: std::time::Instant, + now: Instant, capacity: usize, revision: u64, labels: impl Fn(Action) -> Vec, @@ -326,8 +331,7 @@ impl CommandPaletteState { }; let rows = self.prepare(revision, labels, capacity); let changed = self.navigate(key, &rows, capacity); - self.repeat - .schedule_fixed(now, std::time::Duration::from_millis(55)); + self.repeat.schedule_fixed(now, Duration::from_millis(55)); changed } } diff --git a/src/input/state/core/dirty.rs b/src/input/state/core/dirty.rs index 4c9eef264..60c773f47 100644 --- a/src/input/state/core/dirty.rs +++ b/src/input/state/core/dirty.rs @@ -1,3 +1,6 @@ +use crate::draw; +use crate::input::Tool; + use super::base::{DrawingState, InputState}; use crate::draw::shape::{CaretGeometry, LogicalBounds, bounding_box_for_points}; use crate::draw::{Shape, TextMeasurer}; @@ -113,7 +116,7 @@ impl InputState { /// shape, or a switch between ink and shape, can change anywhere. fn provisional_grows_at_tail(&mut self, current_x: i32, current_y: i32) -> bool { let DrawingState::Drawing { - tool: crate::input::Tool::LiveShape, + tool: Tool::LiveShape, .. } = &self.state else { @@ -142,7 +145,7 @@ impl InputState { ToolMotionBehavior::AccumulatePath { size_source: ToolMotionSizeSource::ToolSize, } => { - if *tool == crate::input::Tool::Marker { + if *tool == Tool::Marker { let size = self.thickness_for_tool(*tool); (size * 1.35).max(size + 1.0) } else if point_thicknesses.len() == points.len() && !point_thicknesses.is_empty() { @@ -370,7 +373,7 @@ impl InputState { /// rounding it first and then subtracting a whole-pixel half-width can move the /// rectangle off the leftmost column the stroke actually touches. fn caret_damage_rect(geom: CaretGeometry, x: i32, y: i32, size: f64) -> Option { - let half_w = crate::draw::caret_outline_width(size) / 2.0; + let half_w = draw::caret_outline_width(size) / 2.0; let caret_x = f64::from(x) + geom.x; let left = (caret_x - half_w).floor() as i32 - 1; let right = (caret_x + half_w).ceil() as i32 + 1; @@ -606,7 +609,7 @@ mod tests { let rect = state .caret_damage_rect_for(&measurer, "hi", 100, 100, 1) .expect("caret has a damage rect"); - let half = crate::draw::caret_outline_width(state.style.current_font_size) / 2.0; + let half = draw::caret_outline_width(state.style.current_font_size) / 2.0; let painted_left = 100.0 + geom.x - half; let painted_right = 100.0 + geom.x + half; assert!( diff --git a/src/input/state/core/editing.rs b/src/input/state/core/editing.rs index 6685cc213..d2b41d30a 100644 --- a/src/input/state/core/editing.rs +++ b/src/input/state/core/editing.rs @@ -1,11 +1,13 @@ //! Canvas mutation transactions. InputState applies UI and persistence effects. +use std::collections::HashSet; + use crate::draw::frame::{Frame, ShapeSnapshot, UndoAction}; use crate::draw::{Shape, ShapeId, TextMeasurer}; use crate::util::Rect; use std::borrow::Cow; -pub(in crate::input::state) struct CanvasEdit<'a> { - snapshots: Cow<'a, [(ShapeId, ShapeSnapshot)]>, +pub(in crate::input::state) struct CanvasEdit<'snapshots> { + snapshots: Cow<'snapshots, [(ShapeId, ShapeSnapshot)]>, } #[derive(Default)] @@ -15,7 +17,7 @@ pub(in crate::input::state) struct EditEffects { committed: bool, } -impl<'a> CanvasEdit<'a> { +impl<'snapshots> CanvasEdit<'snapshots> { pub(in crate::input::state) fn capture(frame: &Frame, ids: &[ShapeId]) -> Self { Self { snapshots: Cow::Owned( @@ -48,7 +50,7 @@ impl<'a> CanvasEdit<'a> { /// Live resize borrows the gesture's original snapshots; motion events /// must not clone whole freehand paths just to read their starting geometry. pub(in crate::input::state) fn borrow_snapshots( - snapshots: &'a [(ShapeId, ShapeSnapshot)], + snapshots: &'snapshots [(ShapeId, ShapeSnapshot)], ) -> Self { Self { snapshots: Cow::Borrowed(snapshots), @@ -159,7 +161,7 @@ impl<'a> CanvasEdit<'a> { pub(in crate::input::state) fn delete( frame: &mut Frame, - ids: &std::collections::HashSet, + ids: &HashSet, measurer: &TextMeasurer, limit: usize, ) -> EditEffects { diff --git a/src/input/state/core/editing/tests.rs b/src/input/state/core/editing/tests.rs index 10f54291f..91d5e01ef 100644 --- a/src/input/state/core/editing/tests.rs +++ b/src/input/state/core/editing/tests.rs @@ -1,3 +1,5 @@ +use crate::draw; + use super::*; fn rectangle(x: i32) -> Shape { @@ -8,7 +10,7 @@ fn rectangle(x: i32) -> Shape { h: 20, fill: false, fill_color: None, - color: crate::draw::WHITE, + color: draw::WHITE, thick: 2.0, } } diff --git a/src/input/state/core/eyedropper.rs b/src/input/state/core/eyedropper.rs index bb713ae5d..78cf2291c 100644 --- a/src/input/state/core/eyedropper.rs +++ b/src/input/state/core/eyedropper.rs @@ -1,3 +1,6 @@ +use crate::draw::TextMeasurer; +use crate::input::state::interaction; + use super::InputState; use crate::domain::Action; use crate::input::Key; @@ -24,18 +27,22 @@ pub enum EyedropperUiState { } impl EyedropperUiState { + #[must_use] pub fn is_active(self) -> bool { matches!(self, Self::Active { .. }) } + #[must_use] pub fn is_pending(self) -> bool { matches!(self, Self::PendingCapture { .. }) } + #[must_use] pub fn is_engaged(self) -> bool { self.is_active() || self.is_pending() } + #[must_use] pub fn hover(self) -> Option<(f64, f64)> { match self { Self::Active { hover, .. } => hover, @@ -43,6 +50,7 @@ impl EyedropperUiState { } } + #[must_use] pub fn pending_source(self) -> Option { match self { Self::PendingCapture { source, .. } => Some(source), @@ -83,7 +91,7 @@ impl InputState { } pub(crate) fn action_for_key(&self, key: Key) -> Option { - crate::input::state::interaction::action_for_key_binding(self, key) + interaction::action_for_key_binding(self, key) .ok() .flatten() } @@ -99,7 +107,7 @@ impl InputState { pub(crate) fn activate_eyedropper_with( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, owned_frozen_generation: Option, ) { // A capture can take long enough for another interaction to begin while @@ -146,7 +154,7 @@ mod tests { fn cancel_returns_the_exact_owned_frozen_generation() { let mut state = make_test_input_state(); state.set_eyedropper_pending_capture(EyedropperCaptureSource::Frozen); - state.activate_eyedropper_with(&crate::draw::TextMeasurer::default(), Some(17)); + state.activate_eyedropper_with(&TextMeasurer::default(), Some(17)); assert_eq!(state.cancel_eyedropper(), Some(17)); assert_eq!(state.eyedropper_state(), EyedropperUiState::Inactive); @@ -168,7 +176,7 @@ mod tests { state.on_mouse_press(MouseButton::Left, 10, 20); assert!(matches!(state.state, DrawingState::Drawing { .. })); - state.activate_eyedropper_with(&crate::draw::TextMeasurer::default(), Some(1)); + state.activate_eyedropper_with(&TextMeasurer::default(), Some(1)); assert!(matches!(state.state, DrawingState::Idle)); assert!(!state.pointer_drag_active()); @@ -185,7 +193,7 @@ mod tests { state.set_eyedropper_pending_capture(EyedropperCaptureSource::Frozen); assert!(state.modal_blocks_canvas_key_repeat()); - state.activate_eyedropper_with(&crate::draw::TextMeasurer::default(), Some(1)); + state.activate_eyedropper_with(&TextMeasurer::default(), Some(1)); assert!(state.modal_blocks_canvas_key_repeat()); state.cancel_eyedropper(); diff --git a/src/input/state/core/font_cycle.rs b/src/input/state/core/font_cycle.rs index 01a1f62fa..9090fc4a7 100644 --- a/src/input/state/core/font_cycle.rs +++ b/src/input/state/core/font_cycle.rs @@ -10,8 +10,7 @@ //! blur tools already use for their variants. use super::InputState; -use crate::draw::TextMeasurer; -use crate::draw::{FontDescriptor, families_match}; +use crate::draw::{FontDescriptor, TextMeasurer, families_match}; use crate::i18n::{tr, trf}; impl InputState { @@ -112,6 +111,8 @@ const FONT_CYCLE_TOAST_SOURCE: &str = "font-cycle"; #[cfg(test)] mod tests { + use crate::draw::Color; + use super::*; use crate::draw::Shape; use crate::input::state::test_support::make_test_input_state; @@ -170,7 +171,7 @@ mod tests { #[test] fn an_empty_list_turns_the_action_off_rather_than_panicking() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.set_font_cycle(Vec::new()); @@ -180,7 +181,7 @@ mod tests { #[test] fn cycling_with_nothing_selected_sets_what_the_next_label_uses() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = state_with_cycle(); let before = state.style.font_descriptor.family.clone(); @@ -197,14 +198,14 @@ mod tests { #[test] fn cycling_with_text_selected_restyles_that_text_and_leaves_the_tool_alone() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = state_with_cycle(); let tool_font = state.style.font_descriptor.family.clone(); let id = state.boards.active_frame_mut().add_shape(Shape::Text { x: 10, y: 10, text: "hello".to_string(), - color: crate::draw::Color::new(1.0, 1.0, 1.0, 1.0), + color: Color::new(1.0, 1.0, 1.0, 1.0), size: 24.0, font_descriptor: FontDescriptor::new( "Sans".to_string(), @@ -234,7 +235,7 @@ mod tests { #[test] fn a_selection_with_no_text_in_it_falls_through_to_the_tool_font() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = state_with_cycle(); let before = state.style.font_descriptor.family.clone(); let id = state.boards.active_frame_mut().add_shape(Shape::Rect { @@ -244,7 +245,7 @@ mod tests { h: 10, fill: false, fill_color: None, - color: crate::draw::Color::new(1.0, 1.0, 1.0, 1.0), + color: Color::new(1.0, 1.0, 1.0, 1.0), thick: 2.0, }); state.set_selection(vec![id]); diff --git a/src/input/state/core/font_picker/input.rs b/src/input/state/core/font_picker/input.rs index 1d2783255..7d9592675 100644 --- a/src/input/state/core/font_picker/input.rs +++ b/src/input/state/core/font_picker/input.rs @@ -2,6 +2,9 @@ use std::time::{Duration, Instant}; +use crate::draw::TextMeasurer; +use crate::util::Rect; + use super::InputState; use super::layout::{font_picker_layout, font_picker_row_at}; use crate::input::events::Key; @@ -27,7 +30,7 @@ impl InputState { /// toggle is `Tab` rather than a letter. pub(crate) fn handle_font_picker_key_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, text: Option<&str>, ) -> bool { @@ -119,9 +122,7 @@ impl InputState { } /// The panel's rectangle, grown to cover the shadow it casts. - pub(in crate::input::state::core) fn font_picker_panel_bounds( - &self, - ) -> Option { + pub(in crate::input::state::core) fn font_picker_panel_bounds(&self) -> Option { const SHADOW: f64 = 4.0; let (screen_width, screen_height) = self.view.screen_size(); let layout = font_picker_layout( @@ -129,7 +130,7 @@ impl InputState { screen_height, self.font_picker_families().len(), ); - crate::util::Rect::new( + Rect::new( (layout.panel_x - SHADOW).floor() as i32, (layout.panel_y - SHADOW).floor() as i32, (layout.panel_width + SHADOW * 2.0).ceil() as i32, @@ -293,7 +294,7 @@ impl InputState { /// from a modal means everywhere else in the overlay. pub(crate) fn font_picker_press_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, x: f64, y: f64, ) -> bool { diff --git a/src/input/state/core/font_picker/layout.rs b/src/input/state/core/font_picker/layout.rs index d3ca7dac0..34f583456 100644 --- a/src/input/state/core/font_picker/layout.rs +++ b/src/input/state/core/font_picker/layout.rs @@ -3,6 +3,8 @@ //! Pure geometry from the surface size, so the renderer and the pointer hit //! test cannot disagree about which row is under the cursor. +use crate::draw; + /// Most rows the panel will ever show. Enough to scan, few enough that laying /// each one out in its own font stays bounded whatever the system has installed. /// @@ -73,6 +75,7 @@ pub struct FontPickerRow { /// /// One row is always reserved even with nothing to show, so the "no matches" /// note lands in the list rather than on the caption. +#[must_use] pub fn font_picker_layout( surface_width: u32, surface_height: u32, @@ -121,6 +124,7 @@ pub fn font_picker_layout( } /// The rows a layout shows, for the window starting at `scroll`. +#[must_use] pub fn font_picker_rows( layout: FontPickerLayout, families: &[String], @@ -142,7 +146,7 @@ pub fn font_picker_rows( width: layout.list_width, height: layout.row_height, selected: index == selected, - current: crate::draw::families_match(family, current), + current: draw::families_match(family, current), }) .collect() } diff --git a/src/input/state/core/font_picker/mod.rs b/src/input/state/core/font_picker/mod.rs index 7117c0885..33ff0593b 100644 --- a/src/input/state/core/font_picker/mod.rs +++ b/src/input/state/core/font_picker/mod.rs @@ -25,8 +25,7 @@ pub(crate) use state::FontPickerState; pub use layout::{FontPickerLayout, FontPickerRow, font_picker_layout, font_picker_rows}; use super::InputState; -use crate::draw::TextMeasurer; -use crate::draw::{FontDescriptor, families_match, system_font_catalog_is_ready}; +use crate::draw::{FontDescriptor, TextMeasurer, families_match, system_font_catalog_is_ready}; use crate::i18n::{tr, trf}; /// The picker's memoized result list, keyed by what produced it. @@ -43,6 +42,7 @@ pub enum FontPickerFilter { } impl FontPickerFilter { + #[must_use] pub fn label(self) -> &'static str { match self { Self::All => tr("All fonts"), @@ -50,6 +50,7 @@ impl FontPickerFilter { } } + #[must_use] pub fn next(self) -> Self { match self { Self::All => Self::Monospace, @@ -68,6 +69,7 @@ pub enum FontPickerTarget { } impl FontPickerTarget { + #[must_use] pub fn label(self) -> &'static str { match self { Self::ToolDefault => tr("Sets the font for new text"), @@ -152,7 +154,10 @@ impl InputState { if !self.font_picker.finish_catalog_load() { return false; } - debug_assert!(system_font_catalog_is_ready()); + debug_assert!( + system_font_catalog_is_ready(), + "finish the picker's catalog load only after the system font catalog is ready" + ); self.position_font_picker_on_current_family(); self.mark_font_picker_dirty(); true diff --git a/src/input/state/core/font_picker/state.rs b/src/input/state/core/font_picker/state.rs index 02c57416d..0ce02db71 100644 --- a/src/input/state/core/font_picker/state.rs +++ b/src/input/state/core/font_picker/state.rs @@ -1,4 +1,5 @@ use std::cell::RefCell; +use std::cmp::Reverse; use std::time::{Duration, Instant}; use super::{FontPickerFilter, FontPickerResults, FontPickerTarget}; @@ -127,7 +128,7 @@ impl FontPickerState { (score > 0).then_some((score, family)) }) .collect(); - scored.sort_by_key(|(score, _)| std::cmp::Reverse(*score)); + scored.sort_by_key(|(score, _)| Reverse(*score)); scored .into_iter() .map(|(_, family)| family.clone()) diff --git a/src/input/state/core/font_picker/tests.rs b/src/input/state/core/font_picker/tests.rs index 8719e7316..cbcf0cf84 100644 --- a/src/input/state/core/font_picker/tests.rs +++ b/src/input/state/core/font_picker/tests.rs @@ -1,3 +1,8 @@ +use std::env; +use std::process::Command; + +use crate::draw; + use super::layout::FONT_PICKER_MAX_VISIBLE; use super::*; use crate::draw::{Color, Shape, system_font_families}; @@ -18,7 +23,7 @@ fn installed_family() -> String { /// state explicitly so they cannot pass or fail according to which unrelated /// font test happened to initialize the global cache first. fn open_ready_font_picker(state: &mut InputState) { - crate::draw::prewarm_system_font_catalog(); + draw::prewarm_system_font_catalog(); state.open_font_picker(); assert!(!state.font_picker_is_loading()); } @@ -60,8 +65,8 @@ fn first_open_can_show_loading_without_enumerating_in_input_dispatch() { // in parallel. Run the production opener in a fresh test process so the // first-open condition is deterministic and a synchronous call added // before the loading branch cannot hide behind test ordering. - if std::env::var_os(CHILD_ENV).is_none() { - let status = std::process::Command::new(std::env::current_exe().expect("test binary")) + if env::var_os(CHILD_ENV).is_none() { + let status = Command::new(env::current_exe().expect("test binary")) .arg(TEST_NAME) .arg("--exact") .arg("--test-threads=1") @@ -72,7 +77,7 @@ fn first_open_can_show_loading_without_enumerating_in_input_dispatch() { return; } - assert!(!crate::draw::system_font_catalog_is_ready()); + assert!(!draw::system_font_catalog_is_ready()); let mut state = make_test_input_state(); state.open_font_picker(); @@ -80,7 +85,7 @@ fn first_open_can_show_loading_without_enumerating_in_input_dispatch() { assert!(state.is_font_picker_open()); assert!(state.font_picker_is_loading()); assert!( - !crate::draw::system_font_catalog_is_ready(), + !draw::system_font_catalog_is_ready(), "opening input dispatch must only probe the cache, never enumerate it" ); assert!(state.font_picker_families().is_empty()); @@ -94,7 +99,7 @@ fn catalog_completion_populates_a_picker_that_opened_in_loading_state() { state.open_font_picker_with_catalog_ready(false); let _ = state.take_dirty_regions(); - crate::draw::prewarm_system_font_catalog(); + draw::prewarm_system_font_catalog(); assert!(state.finish_font_picker_catalog_load()); assert!(!state.font_picker_is_loading()); @@ -124,7 +129,7 @@ fn catalog_worker_failure_is_visible_and_reopening_allows_a_retry() { #[test] fn opening_lists_every_installed_family_and_closing_forgets_the_query() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); open_ready_font_picker(&mut state); @@ -163,7 +168,7 @@ fn the_picker_opens_on_the_font_already_in_use() { #[test] fn typing_narrows_the_list_and_backspace_widens_it_again() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); open_ready_font_picker(&mut state); let all = state.font_picker_families().len(); @@ -185,7 +190,7 @@ fn typing_narrows_the_list_and_backspace_widens_it_again() { #[test] fn a_query_that_matches_nothing_leaves_an_empty_list_rather_than_the_whole_one() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); open_ready_font_picker(&mut state); @@ -205,7 +210,7 @@ fn a_query_that_matches_nothing_leaves_an_empty_list_rather_than_the_whole_one() #[test] fn arrow_keys_clamp_at_both_ends_instead_of_wrapping() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); open_ready_font_picker(&mut state); let count = state.font_picker_families().len(); @@ -262,7 +267,7 @@ fn a_short_output_still_opens_with_the_font_in_use_on_screen() { } let target = families[20].clone(); state.set_font_descriptor(FontDescriptor::new( - target.clone(), + target, "normal".to_string(), "normal".to_string(), )); @@ -312,7 +317,7 @@ fn a_short_output_scrolls_by_the_rows_it_actually_shows() { #[test] fn tab_switches_to_monospace_and_back() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); open_ready_font_picker(&mut state); let all = state.font_picker_families().len(); @@ -328,7 +333,7 @@ fn tab_switches_to_monospace_and_back() { #[test] fn choosing_a_font_with_nothing_selected_sets_what_the_next_label_uses() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); open_ready_font_picker(&mut state); @@ -343,7 +348,7 @@ fn choosing_a_font_with_nothing_selected_sets_what_the_next_label_uses() { #[test] fn choosing_a_font_with_text_selected_restyles_it_and_leaves_the_tool_alone() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let tool_font = state.style.font_descriptor.family.clone(); @@ -375,7 +380,7 @@ fn choosing_a_font_with_text_selected_restyles_it_and_leaves_the_tool_alone() { #[test] fn chosen_fonts_come_back_to_the_top_of_an_unfiltered_list() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); open_ready_font_picker(&mut state); @@ -395,7 +400,7 @@ fn chosen_fonts_come_back_to_the_top_of_an_unfiltered_list() { #[test] fn recents_keep_the_most_recent_first_without_repeats() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let families = system_font_families(); @@ -417,7 +422,7 @@ fn recents_keep_the_most_recent_first_without_repeats() { #[test] fn escape_closes_without_changing_anything() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let before = state.style.font_descriptor.family.clone(); open_ready_font_picker(&mut state); @@ -432,7 +437,7 @@ fn escape_closes_without_changing_anything() { #[test] fn stray_keys_are_swallowed_rather_than_reaching_the_canvas_behind_the_modal() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); open_ready_font_picker(&mut state); @@ -443,7 +448,7 @@ fn stray_keys_are_swallowed_rather_than_reaching_the_canvas_behind_the_modal() { #[test] fn a_closed_picker_consumes_nothing() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); assert!(!state.handle_font_picker_key_with_measurer(&route_measurer, Key::Escape, None)); @@ -526,7 +531,7 @@ fn scrolling_carries_the_highlight_only_when_it_would_be_left_behind() { #[test] fn a_held_arrow_repeats_after_a_delay_and_stops_on_release() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); use std::time::{Duration, Instant}; let mut state = open_picker(); @@ -561,7 +566,7 @@ fn a_held_arrow_repeats_after_a_delay_and_stops_on_release() { #[test] fn a_long_hold_repeats_faster_than_a_short_one() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); use std::time::{Duration, Instant}; // The list runs to hundreds of families. At the command palette's flat rate @@ -606,7 +611,7 @@ fn a_long_hold_repeats_faster_than_a_short_one() { #[test] fn moving_the_highlight_repaints_the_panel_rather_than_the_screen() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); // A held arrow ticks up to fifty times a second. Marking the whole surface // each time is the entire canvas re-rendered per row. let mut state = open_picker(); @@ -626,7 +631,7 @@ fn moving_the_highlight_repaints_the_panel_rather_than_the_screen() { #[test] fn the_first_query_that_shrinks_the_list_repaints_the_panel_it_is_leaving() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); // Opening on the installed catalog draws a tall panel; the first query can // cut it to no rows. Partial repaints clip to their damage, so unless the // taller panel is damaged too its lower half stays on screen underneath. @@ -663,7 +668,7 @@ fn the_first_query_that_shrinks_the_list_repaints_the_panel_it_is_leaving() { #[test] fn the_first_narrowing_query_after_resize_repaints_the_resized_panel() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.update_screen_dimensions(1920, 1080); open_ready_font_picker(&mut state); @@ -699,7 +704,7 @@ fn the_first_narrowing_query_after_resize_repaints_the_resized_panel() { #[test] fn reopening_the_picker_does_not_leave_the_old_key_repeating() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); use std::time::{Duration, Instant}; let mut state = make_test_input_state(); diff --git a/src/input/state/core/help_overlay/tests.rs b/src/input/state/core/help_overlay/tests.rs index 3ad0c272c..9ad02b70a 100644 --- a/src/input/state/core/help_overlay/tests.rs +++ b/src/input/state/core/help_overlay/tests.rs @@ -1,10 +1,13 @@ +use crate::config::Action; +use crate::help_overlay_interaction::{HelpHitMap, HelpRenderResult}; +use crate::input::events::Key; use crate::input::state::{ HelpOverlayClick, HelpOverlayCursorHint, HelpOverlayPressSource, HelpOverlayReleaseOutcome, - InputState, + InputState, test_support, }; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } #[test] @@ -24,12 +27,12 @@ fn tab_toggles_unbound_rows_without_typing_into_search() { state.toggle_help_overlay(); state.needs_redraw = false; - assert!(state.handle_help_overlay_key(crate::input::events::Key::Tab)); + assert!(state.handle_help_overlay_key(Key::Tab)); assert!(state.help_overlay.shows_unbound()); assert!(state.needs_redraw); assert_eq!(state.help_overlay.query(), ""); - assert!(state.handle_help_overlay_key(crate::input::events::Key::Tab)); + assert!(state.handle_help_overlay_key(Key::Tab)); assert!(!state.help_overlay.shows_unbound()); } @@ -37,14 +40,10 @@ fn tab_toggles_unbound_rows_without_typing_into_search() { fn help_overlay_toggle_region_maps_to_a_pointer_toggle_click() { let mut state = make_state(); state.toggle_help_overlay(); - state.install_help_overlay_render_result(crate::help_overlay_interaction::HelpRenderResult { + state.install_help_overlay_render_result(HelpRenderResult { scroll_max: 0.0, - hit_map: crate::help_overlay_interaction::HelpHitMap::new( - (100.0, 100.0, 200.0, 300.0), - None, - [], - ) - .with_unbound_toggle((120.0, 350.0, 80.0, 30.0)), + hit_map: HelpHitMap::new((100.0, 100.0, 200.0, 300.0), None, []) + .with_unbound_toggle((120.0, 350.0, 80.0, 30.0)), }); assert_eq!( @@ -97,7 +96,7 @@ fn help_overlay_cursor_hint_maps_real_layout_regions() { &mut state, (100.0, 100.0, 200.0, 300.0), Some((110.0, 130.0, 180.0, 24.0)), - &[(120.0, 200.0, 160.0, 30.0, crate::config::Action::ToggleHelp)], + &[(120.0, 200.0, 160.0, 30.0, Action::ToggleHelp)], ); assert_eq!( @@ -123,18 +122,12 @@ fn help_overlay_click_runs_rows_and_dismisses_outside() { &mut state, (100.0, 100.0, 200.0, 300.0), Some((110.0, 130.0, 180.0, 24.0)), - &[( - 120.0, - 200.0, - 160.0, - 30.0, - crate::config::Action::ToggleStatusBar, - )], + &[(120.0, 200.0, 160.0, 30.0, Action::ToggleStatusBar)], ); assert_eq!( state.help_overlay_click_at(150, 215), - HelpOverlayClick::Run(crate::config::Action::ToggleStatusBar) + HelpOverlayClick::Run(Action::ToggleStatusBar) ); assert_eq!( state.help_overlay_click_at(150, 140), @@ -158,7 +151,7 @@ fn close_help_overlay_resets_state_and_clears_hit_map() { &mut state, (100.0, 100.0, 200.0, 300.0), None, - &[(120.0, 200.0, 160.0, 30.0, crate::config::Action::ToggleHelp)], + &[(120.0, 200.0, 160.0, 30.0, Action::ToggleHelp)], ); state.help_overlay.update_scroll_extent(100.0); @@ -182,7 +175,7 @@ fn close_help_overlay_resets_state_and_clears_hit_map() { /// 130..154). The overlay is opened first so the install survives the /// open-time defensive clear, mirroring a real render pass populating the /// map while help is visible. -fn state_with_help_row(action: crate::config::Action) -> InputState { +fn state_with_help_row(action: Action) -> InputState { let mut state = make_state(); state.toggle_help_overlay(); install_hit_map( @@ -196,15 +189,13 @@ fn state_with_help_row(action: crate::config::Action) -> InputState { #[test] fn help_release_runs_row_only_when_press_and_release_share_the_row() { - let mut state = state_with_help_row(crate::config::Action::ClearCanvas); + let mut state = state_with_help_row(Action::ClearCanvas); // Press and release both on the row -> the row's action runs. state.note_help_overlay_press(HelpOverlayPressSource::Pointer(1), 150, 215); assert_eq!( state.resolve_help_overlay_release(HelpOverlayPressSource::Pointer(1), 150, 215), - Some(HelpOverlayReleaseOutcome::Run( - crate::config::Action::ClearCanvas - )) + Some(HelpOverlayReleaseOutcome::Run(Action::ClearCanvas)) ); // The recorded press was consumed. assert!(state.help_overlay.pending_presses.is_empty()); @@ -215,7 +206,7 @@ fn help_press_on_chrome_then_drag_onto_row_does_not_run() { // The destructive-action hazard: a press that starts on bare chrome and // is dragged onto a clickable row (here the ClearCanvas row) must never // execute it. - let mut state = state_with_help_row(crate::config::Action::ClearCanvas); + let mut state = state_with_help_row(Action::ClearCanvas); // (150, 280) is inside the box but below the row and search well: chrome. state.note_help_overlay_press(HelpOverlayPressSource::Pointer(1), 150, 280); @@ -231,7 +222,7 @@ fn help_press_on_chrome_then_drag_onto_row_does_not_run() { #[test] fn help_press_outside_then_release_on_row_does_not_run() { - let mut state = state_with_help_row(crate::config::Action::ClearCanvas); + let mut state = state_with_help_row(Action::ClearCanvas); state.note_help_overlay_press(HelpOverlayPressSource::Pointer(1), 10, 10); assert_eq!( @@ -249,7 +240,7 @@ fn help_press_outside_then_release_on_row_does_not_run() { #[test] fn help_press_and_release_outside_dismisses() { - let mut state = state_with_help_row(crate::config::Action::ClearCanvas); + let mut state = state_with_help_row(Action::ClearCanvas); state.note_help_overlay_press(HelpOverlayPressSource::Pointer(1), 10, 10); assert_eq!( @@ -260,7 +251,7 @@ fn help_press_and_release_outside_dismisses() { #[test] fn help_press_on_row_then_release_on_chrome_does_not_run() { - let mut state = state_with_help_row(crate::config::Action::ClearCanvas); + let mut state = state_with_help_row(Action::ClearCanvas); state.note_help_overlay_press(HelpOverlayPressSource::Pointer(1), 150, 215); assert_eq!( @@ -271,7 +262,7 @@ fn help_press_on_row_then_release_on_chrome_does_not_run() { #[test] fn help_release_without_a_recorded_press_is_inert() { - let mut state = state_with_help_row(crate::config::Action::ClearCanvas); + let mut state = state_with_help_row(Action::ClearCanvas); // No note_help_overlay_press call: a release cannot fabricate intent. assert!(state.help_overlay.pending_presses.is_empty()); @@ -283,7 +274,7 @@ fn help_release_without_a_recorded_press_is_inert() { #[test] fn help_release_requires_the_modality_that_owned_the_press() { - let mut state = state_with_help_row(crate::config::Action::ClearCanvas); + let mut state = state_with_help_row(Action::ClearCanvas); state.note_help_overlay_press(HelpOverlayPressSource::Touch, 150, 215); assert_eq!( @@ -297,15 +288,13 @@ fn help_release_requires_the_modality_that_owned_the_press() { ); assert_eq!( state.resolve_help_overlay_release(HelpOverlayPressSource::Touch, 150, 215), - Some(HelpOverlayReleaseOutcome::Run( - crate::config::Action::ClearCanvas - )) + Some(HelpOverlayReleaseOutcome::Run(Action::ClearCanvas)) ); } #[test] fn help_pointer_ownership_is_tracked_per_button() { - let mut state = state_with_help_row(crate::config::Action::ClearCanvas); + let mut state = state_with_help_row(Action::ClearCanvas); let left = HelpOverlayPressSource::Pointer(1); let middle = HelpOverlayPressSource::Pointer(2); state.note_help_overlay_press(left, 150, 215); @@ -321,16 +310,14 @@ fn help_pointer_ownership_is_tracked_per_button() { ); assert_eq!( state.resolve_help_overlay_release(left, 150, 215), - Some(HelpOverlayReleaseOutcome::Run( - crate::config::Action::ClearCanvas - )), + Some(HelpOverlayReleaseOutcome::Run(Action::ClearCanvas)), "middle ownership must not consume the left help click" ); } #[test] fn closing_help_keeps_its_press_release_owned_without_running_the_stale_target() { - let mut state = state_with_help_row(crate::config::Action::ClearCanvas); + let mut state = state_with_help_row(Action::ClearCanvas); let pointer = HelpOverlayPressSource::Pointer(1); state.note_help_overlay_press(pointer, 150, 215); @@ -350,7 +337,7 @@ fn closing_help_keeps_its_press_release_owned_without_running_the_stale_target() #[test] fn reopening_help_cannot_retarget_a_press_from_the_previous_layout() { - let mut state = state_with_help_row(crate::config::Action::ClearCanvas); + let mut state = state_with_help_row(Action::ClearCanvas); let pointer = HelpOverlayPressSource::Pointer(1); state.note_help_overlay_press(pointer, 150, 215); @@ -360,13 +347,7 @@ fn reopening_help_cannot_retarget_a_press_from_the_previous_layout() { &mut state, (100.0, 100.0, 200.0, 300.0), None, - &[( - 120.0, - 200.0, - 160.0, - 30.0, - crate::config::Action::ClearCanvas, - )], + &[(120.0, 200.0, 160.0, 30.0, Action::ClearCanvas)], ); assert_eq!( @@ -378,7 +359,7 @@ fn reopening_help_cannot_retarget_a_press_from_the_previous_layout() { #[test] fn a_new_help_press_supersedes_consume_only_ownership_for_its_source() { - let mut state = state_with_help_row(crate::config::Action::ClearCanvas); + let mut state = state_with_help_row(Action::ClearCanvas); let pointer = HelpOverlayPressSource::Pointer(1); state.note_help_overlay_press(pointer, 150, 215); state.close_help_overlay(); @@ -387,22 +368,14 @@ fn a_new_help_press_supersedes_consume_only_ownership_for_its_source() { &mut state, (100.0, 100.0, 200.0, 300.0), None, - &[( - 120.0, - 200.0, - 160.0, - 30.0, - crate::config::Action::ClearCanvas, - )], + &[(120.0, 200.0, 160.0, 30.0, Action::ClearCanvas)], ); state.note_help_overlay_press(pointer, 150, 215); assert_eq!( state.resolve_help_overlay_release(pointer, 150, 215), - Some(HelpOverlayReleaseOutcome::Run( - crate::config::Action::ClearCanvas - )) + Some(HelpOverlayReleaseOutcome::Run(Action::ClearCanvas)) ); } @@ -414,7 +387,7 @@ fn opening_help_drops_stale_hit_map_geometry() { &mut state, (100.0, 100.0, 200.0, 300.0), None, - &[(120.0, 200.0, 160.0, 30.0, crate::config::Action::ToggleHelp)], + &[(120.0, 200.0, 160.0, 30.0, Action::ToggleHelp)], ); // Opening must drop it so a click can never act on the previous layout @@ -434,7 +407,7 @@ fn opening_the_command_palette_routes_help_close_through_the_canonical_closer() &mut state, (100.0, 100.0, 200.0, 300.0), None, - &[(120.0, 200.0, 160.0, 30.0, crate::config::Action::ToggleHelp)], + &[(120.0, 200.0, 160.0, 30.0, Action::ToggleHelp)], ); state.toggle_command_palette(); @@ -448,19 +421,17 @@ fn install_hit_map( state: &mut InputState, bounds: (f64, f64, f64, f64), search: Option<(f64, f64, f64, f64)>, - rows: &[(f64, f64, f64, f64, crate::config::Action)], + rows: &[(f64, f64, f64, f64, Action)], ) { - state - .help_overlay - .install_render_result(crate::help_overlay_interaction::HelpRenderResult { - scroll_max: 0.0, - hit_map: crate::help_overlay_interaction::HelpHitMap::new( - bounds, - search, - rows.iter() - .map(|&(x, y, w, h, action)| ((x, y, w, h), action)), - ), - }); + state.help_overlay.install_render_result(HelpRenderResult { + scroll_max: 0.0, + hit_map: HelpHitMap::new( + bounds, + search, + rows.iter() + .map(|&(x, y, w, h, action)| ((x, y, w, h), action)), + ), + }); } #[test] diff --git a/src/input/state/core/history.rs b/src/input/state/core/history.rs index 872200c77..31c46ddf9 100644 --- a/src/input/state/core/history.rs +++ b/src/input/state/core/history.rs @@ -112,11 +112,14 @@ impl InputState { #[cfg(test)] mod tests { + use crate::input::state::test_support; + use super::*; - use crate::draw::{Color, Shape, frame::ShapeSnapshot}; + use crate::draw::frame::ShapeSnapshot; + use crate::draw::{Color, Shape}; fn make_state() -> InputState { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.update_screen_dimensions(200, 120); let _ = state.take_dirty_regions(); state @@ -167,7 +170,7 @@ mod tests { #[test] fn apply_action_side_effects_closes_properties_panel_after_modify() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_state(); let shape_id = state.boards.active_frame_mut().add_shape(rect(10, 20)); state.set_selection(vec![shape_id]); diff --git a/src/input/state/core/ime.rs b/src/input/state/core/ime.rs index f3dbe0bd5..c502ba2c7 100644 --- a/src/input/state/core/ime.rs +++ b/src/input/state/core/ime.rs @@ -12,8 +12,11 @@ //! transient insertion preview that never enters the committed buffer until //! the IME commits it. +use std::mem; use std::ops::Range; +use crate::draw::TextMeasurer; + use super::TextEditing; use super::base::{DrawingState, InputState}; @@ -245,7 +248,7 @@ impl TextEditing { return false; } - let pending = std::mem::take(&mut self.ime.pending); + let pending = mem::take(&mut self.ime.pending); let mut buffer_changed = false; if let DrawingState::TextInput { @@ -383,11 +386,11 @@ impl InputState { } pub fn ime_apply_done(&mut self) -> bool { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); self.ime_apply_done_with(&measurer) } - pub(crate) fn ime_apply_done_with(&mut self, measurer: &crate::draw::TextMeasurer) -> bool { + pub(crate) fn ime_apply_done_with(&mut self, measurer: &TextMeasurer) -> bool { self.note_session_interaction_activity(); let changed = self.text_editing.apply_ime_done(&mut self.state); if changed { @@ -398,11 +401,11 @@ impl InputState { } pub fn ime_clear(&mut self) -> bool { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); self.ime_clear_with(&measurer) } - pub(crate) fn ime_clear_with(&mut self, measurer: &crate::draw::TextMeasurer) -> bool { + pub(crate) fn ime_clear_with(&mut self, measurer: &TextMeasurer) -> bool { let had_preedit = self.text_editing.clear_ime(); if had_preedit { self.needs_redraw = true; diff --git a/src/input/state/core/index.rs b/src/input/state/core/index.rs index 6ad3457c2..05c11b705 100644 --- a/src/input/state/core/index.rs +++ b/src/input/state/core/index.rs @@ -3,6 +3,9 @@ mod grid; mod owner; +#[cfg(test)] +use std::cell::Cell; + use super::base::InputState; use crate::draw::{ShapeId, TextMeasurer}; use owner::ActiveFrameOrderGuard; @@ -18,7 +21,7 @@ impl InputState { #[cfg(test)] pub(crate) fn spatial_shape_index_build_count() -> usize { - SPATIAL_SHAPE_INDEX_BUILDS.with(std::cell::Cell::get) + SPATIAL_SHAPE_INDEX_BUILDS.with(Cell::get) } /// Returns all shapes intersecting any of the provided points within tolerance. diff --git a/src/input/state/core/index/grid.rs b/src/input/state/core/index/grid.rs index b0fe4d603..5816a5cac 100644 --- a/src/input/state/core/index/grid.rs +++ b/src/input/state/core/index/grid.rs @@ -186,7 +186,11 @@ impl SpatialGrid { self.cells.entry(key).or_default().push(id); cell_keys.push(key); } - debug_assert_eq!(cell_keys.len(), cell_range.count); + debug_assert_eq!( + cell_keys.len(), + cell_range.count, + "the cell range yields exactly the count it reserved" + ); self.shape_cells.insert(id, cell_keys); } CellCoverage::Bounded(_) | CellCoverage::Global => { diff --git a/src/input/state/core/index/measurement_tests.rs b/src/input/state/core/index/measurement_tests.rs index 36b1ac7ac..efd528edd 100644 --- a/src/input/state/core/index/measurement_tests.rs +++ b/src/input/state/core/index/measurement_tests.rs @@ -1,9 +1,11 @@ +use crate::input::state::test_support; + use super::*; use crate::draw::{ArrowLabel, ArrowStyle, FontDescriptor, RED, Shape, StepMarkerLabel}; use crate::input::hit_test; fn scene(threshold: usize) -> (InputState, Vec) { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.set_hit_test_threshold(threshold); state.set_hit_test_tolerance(1.0); let shapes = [ diff --git a/src/input/state/core/index/owner.rs b/src/input/state/core/index/owner.rs index b1d768805..1274bceef 100644 --- a/src/input/state/core/index/owner.rs +++ b/src/input/state/core/index/owner.rs @@ -1,5 +1,11 @@ //! Spatial index guards, invalidation, and hit-query policy. +use crate::util::Rect; + +#[cfg(test)] +use std::cell::Cell; +use std::cmp::Reverse; + use super::grid::SpatialGrid; use crate::draw::{Frame, ShapeId, TextMeasurer}; use crate::input::boards::BoardIdentityGeneration; @@ -34,12 +40,12 @@ struct SpatialIndexCache { #[cfg(test)] std::thread_local! { - pub(super) static SPATIAL_SHAPE_INDEX_BUILDS: std::cell::Cell = const { std::cell::Cell::new(0) }; + pub(super) static SPATIAL_SHAPE_INDEX_BUILDS: Cell = const { Cell::new(0) }; } #[derive(Debug, Clone)] pub(in crate::input::state) struct CanvasIndex { - hit_test_cache: HashMap, + hit_test_cache: HashMap, /// The frame whose shapes `hit_test_cache` describes. Shape ids are /// page-local, so an entry is only meaningful for the frame it came from. hit_test_cache_frame: Option, @@ -96,7 +102,7 @@ impl CanvasIndex { pub(super) fn invalidate_shape( &mut self, id: ShapeId, - new_bounds: Option>, + new_bounds: Option>, guard: ActiveFrameOrderGuard, ) { self.content_generation = self.content_generation.wrapping_add(1); @@ -286,7 +292,7 @@ impl CanvasIndex { Some(shape_index) }) .collect(); - sorted_candidates.sort_unstable_by_key(|&index| std::cmp::Reverse(index)); + sorted_candidates.sort_unstable_by_key(|&index| Reverse(index)); if !stale && let Some(id) = diff --git a/src/input/state/core/input_hud_controls.rs b/src/input/state/core/input_hud_controls.rs index e3b627858..510066d18 100644 --- a/src/input/state/core/input_hud_controls.rs +++ b/src/input/state/core/input_hud_controls.rs @@ -1,3 +1,4 @@ +use std::collections::vec_deque::Iter; use std::time::Instant; use crate::config::{InputHudMode, InputHudPosition}; @@ -42,7 +43,7 @@ impl InputState { } /// Live chips, oldest first. - pub fn input_hud_entries(&self) -> std::collections::vec_deque::Iter<'_, InputHudEntry> { + pub fn input_hud_entries(&self) -> Iter<'_, InputHudEntry> { self.input_hud.entries() } diff --git a/src/input/state/core/keymap.rs b/src/input/state/core/keymap.rs index 422edea72..a3d636460 100644 --- a/src/input/state/core/keymap.rs +++ b/src/input/state/core/keymap.rs @@ -5,7 +5,7 @@ use crate::config::{ }; use crate::draw::Color; use crate::input::state::core::utility::{PendingSequence, SequenceMatch, SequenceTrie}; -use crate::input::{DragToolBindings, Modifiers, MouseButton}; +use crate::input::{DragBinding, DragToolBindings, Modifiers, MouseButton}; use log::warn; use std::collections::{HashMap, HashSet}; use std::time::{Duration, Instant}; @@ -164,7 +164,7 @@ impl Keymap { &self, button: MouseButton, modifiers: Modifiers, - ) -> crate::input::DragBinding { + ) -> DragBinding { self.drag_tool_bindings .binding_for_button_modifier(button, modifiers.active_drag_modifier()) } diff --git a/src/input/state/core/laser_controls.rs b/src/input/state/core/laser_controls.rs index a01f67492..c36b61943 100644 --- a/src/input/state/core/laser_controls.rs +++ b/src/input/state/core/laser_controls.rs @@ -1,5 +1,8 @@ use std::time::{Duration, Instant}; +use crate::draw::Color; +use crate::ui::anim; + use super::base::{DrawingState, InputState}; use crate::config::LaserConfig; use crate::draw::LaserStyle; @@ -29,7 +32,7 @@ impl InputState { } /// Color the status bar shows for `tool`, following the laser's own color. - pub(crate) fn status_color_for_tool(&self, tool: Tool) -> crate::draw::Color { + pub(crate) fn status_color_for_tool(&self, tool: Tool) -> Color { if tool == Tool::Laser { self.laser_style().color } else { @@ -53,7 +56,7 @@ impl InputState { /// Advances the laser fade; returns whether it needs continuous frames. pub fn advance_laser_ink(&mut self, now: Instant) -> bool { - self.advance_laser_ink_for(now, crate::ui::anim::motion_enabled()) + self.advance_laser_ink_for(now, anim::motion_enabled()) } /// Split from the accessor so both motion settings can be exercised @@ -67,7 +70,7 @@ impl InputState { /// When the laser ink next changes on its own, for a loop that is not /// already ticking for animation. pub(crate) fn laser_ink_wake_after(&self, now: Instant) -> Option { - self.laser_ink_wake_after_for(now, crate::ui::anim::motion_enabled()) + self.laser_ink_wake_after_for(now, anim::motion_enabled()) } pub(crate) fn laser_ink_wake_after_for(&self, now: Instant, motion: bool) -> Option { diff --git a/src/input/state/core/menus/commands.rs b/src/input/state/core/menus/commands.rs index 993ffbf64..8d2a25090 100644 --- a/src/input/state/core/menus/commands.rs +++ b/src/input/state/core/menus/commands.rs @@ -1,3 +1,8 @@ +use crate::draw::{PageDeleteOutcome, TextMeasurer}; +use crate::input::state::InputTextResources; +use crate::input::{ZoomAction, ZoomAnchor}; +use crate::ui_text::UiTextEngine; + use super::super::base::{InputState, PasteAnchor}; use super::types::{ContextMenuState, MenuCommand}; use crate::domain::Action; @@ -23,12 +28,12 @@ impl InputState { /// Zoom from the menu centres where the menu was opened. A canvas menu /// opened from the keyboard sits in a corner, so that one centres the /// screen instead. - fn context_menu_zoom_anchor(&self) -> crate::input::ZoomAnchor { + fn context_menu_zoom_anchor(&self) -> ZoomAnchor { match self.context_menu.state { ContextMenuState::Open { anchor, .. } if !self.context_menu.anchored_in_corner => { - crate::input::ZoomAnchor::At(anchor.0, anchor.1) + ZoomAnchor::At(anchor.0, anchor.1) } - _ => crate::input::ZoomAnchor::ScreenCenter, + _ => ZoomAnchor::ScreenCenter, } } @@ -62,7 +67,7 @@ impl InputState { true } - fn select_hovered_context_menu_shape_with(&mut self, measurer: &crate::draw::TextMeasurer) { + fn select_hovered_context_menu_shape_with(&mut self, measurer: &TextMeasurer) { if let Some(hovered_shape) = self.hovered_context_menu_shape() { self.set_selection_with(measurer, vec![hovered_shape]); self.close_context_menu(); @@ -74,18 +79,14 @@ impl InputState { /// Closes the menu, then runs `action`. Closing first keeps the menu from /// painting over what the action shows, and keeps Exit from spending /// itself on cancelling the menu. - fn close_menu_and_run( - &mut self, - resources: crate::input::state::InputTextResources<'_>, - action: Action, - ) { + fn close_menu_and_run(&mut self, resources: InputTextResources<'_>, action: Action) { self.close_context_menu(); self.handle_action_with_resources(resources, action); } pub fn execute_menu_command(&mut self, command: MenuCommand) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.execute_menu_command_with_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -97,7 +98,7 @@ impl InputState { pub(crate) fn execute_menu_command_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, command: MenuCommand, ) { match command { @@ -177,22 +178,22 @@ impl InputState { } MenuCommand::ZoomIn => { let anchor = self.context_menu_zoom_anchor(); - self.request_zoom_action_at(crate::input::ZoomAction::In, anchor); + self.request_zoom_action_at(ZoomAction::In, anchor); self.close_context_menu(); } MenuCommand::ZoomOut => { let anchor = self.context_menu_zoom_anchor(); - self.request_zoom_action_at(crate::input::ZoomAction::Out, anchor); + self.request_zoom_action_at(ZoomAction::Out, anchor); self.close_context_menu(); } MenuCommand::ResetZoom => { let anchor = self.context_menu_zoom_anchor(); - self.request_zoom_action_at(crate::input::ZoomAction::Reset, anchor); + self.request_zoom_action_at(ZoomAction::Reset, anchor); self.close_context_menu(); } MenuCommand::ToggleZoomLock => { let anchor = self.context_menu_zoom_anchor(); - self.request_zoom_action_at(crate::input::ZoomAction::ToggleLock, anchor); + self.request_zoom_action_at(ZoomAction::ToggleLock, anchor); self.close_context_menu(); } MenuCommand::Undo => self.close_menu_and_run(resources, Action::Undo), @@ -227,7 +228,7 @@ impl InputState { MenuCommand::PageDelete => { if matches!( self.page_delete_with_measurer(resources.measurer), - crate::draw::PageDeleteOutcome::Cleared + PageDeleteOutcome::Cleared ) { self.push_toast( ToastPriority::Info, @@ -267,8 +268,7 @@ impl InputState { target.board_index, target.page_index, ); - if !matches!(outcome, crate::draw::PageDeleteOutcome::Pending) && affects_panel - { + if !matches!(outcome, PageDeleteOutcome::Pending) && affects_panel { self.board_picker_reconcile_page_nav_after_page_change(); } } diff --git a/src/input/state/core/menus/context_menu.rs b/src/input/state/core/menus/context_menu.rs index 59fec15c1..5d83b965f 100644 --- a/src/input/state/core/menus/context_menu.rs +++ b/src/input/state/core/menus/context_menu.rs @@ -1,3 +1,4 @@ +use std::mem; use std::time::Instant; use super::super::board_picker::BoardPickerPageTarget; @@ -82,7 +83,7 @@ impl ContextMenuPanel { } pub(in crate::input::state) fn take_dismissal_release_pending(&mut self) -> bool { - std::mem::take(&mut self.dismissal_release_pending) + mem::take(&mut self.dismissal_release_pending) } /// Closes the menu and any submenu. The frame damage history repaints the diff --git a/src/input/state/core/menus/focus.rs b/src/input/state/core/menus/focus.rs index 91027026f..74dfd619c 100644 --- a/src/input/state/core/menus/focus.rs +++ b/src/input/state/core/menus/focus.rs @@ -1,3 +1,5 @@ +use crate::input::state::InputTextResources; + use super::super::base::InputState; use super::types::ContextMenuLevel; @@ -96,7 +98,7 @@ impl InputState { pub(crate) fn activate_context_menu_selection_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, ) -> bool { if !self.is_context_menu_open() { return false; @@ -112,7 +114,7 @@ impl InputState { /// is there. pub(crate) fn activate_context_menu_row_at_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, x: i32, y: i32, ) -> bool { @@ -129,7 +131,7 @@ impl InputState { /// the keyboard with focus inside it, from a click as a toggle. fn activate_context_menu_row_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, level: ContextMenuLevel, index: usize, focus_submenu: bool, diff --git a/src/input/state/core/menus/hover.rs b/src/input/state/core/menus/hover.rs index 9040c026c..4dd092f9b 100644 --- a/src/input/state/core/menus/hover.rs +++ b/src/input/state/core/menus/hover.rs @@ -223,7 +223,7 @@ impl InputState { } } - pub(crate) fn focus_context_menu_command(&mut self, command: MenuCommand) -> bool { + pub(crate) fn focus_context_menu_command(&mut self, command: &MenuCommand) -> bool { if !self.is_context_menu_open() { return false; } @@ -232,7 +232,7 @@ impl InputState { if entry.disabled { continue; } - if entry.command.as_ref() == Some(&command) { + if entry.command.as_ref() == Some(command) { self.set_context_menu_focus(Some(index)); return true; } diff --git a/src/input/state/core/menus/lifecycle.rs b/src/input/state/core/menus/lifecycle.rs index fcf764cae..482a3c405 100644 --- a/src/input/state/core/menus/lifecycle.rs +++ b/src/input/state/core/menus/lifecycle.rs @@ -1,7 +1,9 @@ +use crate::draw::Shape; +use crate::input::state::core::modal::ModalSurface; + use super::super::base::InputState; use super::types::{ContextMenuKind, MenuCommand}; -use crate::draw::ShapeId; -use crate::draw::TextMeasurer; +use crate::draw::{ShapeId, TextMeasurer}; impl InputState { /// Closes the currently open context menu. @@ -27,7 +29,7 @@ impl InputState { if !self.context_menu.enabled { return; } - self.close_modals_for_open(crate::input::state::core::modal::ModalSurface::ContextMenu); + self.close_modals_for_open(ModalSurface::ContextMenu); self.context_menu .open(anchor, shape_ids, kind, hovered_shape_id); self.pointer.request_menu_hover_recalc(); @@ -105,17 +107,14 @@ impl InputState { .active_frame() .shape(selection[0]) .map(|shape| { - matches!( - shape.shape, - crate::draw::Shape::Text { .. } | crate::draw::Shape::StickyNote { .. } - ) + matches!(shape.shape, Shape::Text { .. } | Shape::StickyNote { .. }) }) .unwrap_or(false); let anchor = self.keyboard_shape_menu_anchor(measurer, &selection); self.update_pointer_position_synthetic(anchor.0, anchor.1); self.open_context_menu(anchor, selection, ContextMenuKind::Shape, None); self.pointer.clear_menu_hover_recalc(); - if !focus_edit || !self.focus_context_menu_command(MenuCommand::EditText) { + if !focus_edit || !self.focus_context_menu_command(&MenuCommand::EditText) { self.focus_first_context_menu_entry(); } } diff --git a/src/input/state/core/menus/types.rs b/src/input/state/core/menus/types.rs index cd1bb0667..cffca7ea6 100644 --- a/src/input/state/core/menus/types.rs +++ b/src/input/state/core/menus/types.rs @@ -159,12 +159,14 @@ impl ContextMenuEntry { } /// Starts a new group at this row. + #[must_use] pub fn with_separator(mut self) -> Self { self.separator_before = true; self } /// Makes this a parent row that opens `kind` as a submenu. + #[must_use] pub fn with_submenu(mut self, kind: ContextMenuKind) -> Self { self.submenu = Some(kind); self diff --git a/src/input/state/core/mod.rs b/src/input/state/core/mod.rs index 108655464..594e4f7e7 100644 --- a/src/input/state/core/mod.rs +++ b/src/input/state/core/mod.rs @@ -78,25 +78,20 @@ pub use base::{ ZoomRequest, }; pub(crate) use base::{ - BoardPickerClickState, TextClipboardRequest, TextCutTarget, TextPasteEdit, TextPasteTarget, - ToastCommand, ToastPress, -}; -pub(crate) use base::{ - ClipboardFingerprint, ClipboardPasteRequest, HelperLaunchRequest, PasteAnchor, + BoardPickerClickState, ClipboardFingerprint, ClipboardPasteRequest, HelperLaunchRequest, + InputEffect, InputEffectDrain, KeybindingEditOperation, KeybindingEditRequest, PasteAnchor, PendingBackendAction, PendingOnboardingUsage, PendingSelectionClipboardPublish, - PendingToolbarPersistence, WayscriberClipboardSelection, + PendingToolbarPersistence, TextClipboardRequest, TextCutTarget, TextPasteEdit, TextPasteTarget, + ToastCommand, ToastPress, WayscriberClipboardSelection, }; -pub(crate) use base::{InputEffect, InputEffectDrain}; -pub(crate) use base::{KeybindingEditOperation, KeybindingEditRequest}; pub(in crate::input::state) use board::transitions::BoardTransitions; pub use board_picker::{BoardPickerCursorHint, BoardPickerLayout, BoardPickerPanel}; pub(crate) use captured_image::BoardPasteTarget; pub(crate) use color_picker_popup::HexPasteTarget; -pub use color_picker_popup::PickerDrag; pub use color_picker_popup::{ ColorPickerCursorHint, ColorPickerPopupLayout, ColorPickerPopupLayoutOptions, ColorPickerPopupPanel, ColorPickerPopupState, POPUP_HEIGHT as COLOR_PICKER_POPUP_HEIGHT, - POPUP_WIDTH as COLOR_PICKER_POPUP_WIDTH, PREVIEW_SIZE as COLOR_PICKER_PREVIEW_SIZE, + POPUP_WIDTH as COLOR_PICKER_POPUP_WIDTH, PREVIEW_SIZE as COLOR_PICKER_PREVIEW_SIZE, PickerDrag, RECENT_SWATCH_COUNT as COLOR_PICKER_RECENT_SWATCH_COUNT, RECENT_SWATCH_SIZE as COLOR_PICKER_RECENT_SWATCH_SIZE, rgb_to_hsv as color_picker_rgb_to_hsv, }; @@ -123,9 +118,8 @@ pub use ime::ImePreedit; pub(crate) use ime::build_text_input_preview; pub use menus::{ ContextMenuCursorHint, ContextMenuEntry, ContextMenuKind, ContextMenuLayout, ContextMenuState, - ContextSubmenu, MenuCommand, SubmenuSide, + ContextSubmenu, MenuCommand, SUBMENU_AIM_GRACE, SUBMENU_HOVER_DELAY, SubmenuSide, }; -pub use menus::{SUBMENU_AIM_GRACE, SUBMENU_HOVER_DELAY}; pub(crate) use properties::metrics as properties_panel_metrics; pub use properties::{ LevelRange, PanelAction, PanelRect, PanelScroll, PropertiesPanelHit, PropertiesPanelLayout, @@ -148,7 +142,5 @@ pub(crate) use selection::{ClipboardFallbackSaveRequest, LocalSelectionContext}; pub(crate) use style::DrawingStyle; pub(crate) use text_editing::TextEditing; pub use tool_controls::PrecisionEntryState; -pub(crate) use utility::HelpOverlayPressSource; -pub(crate) use utility::SequenceMatch; -pub(crate) use utility::default_step_marker_size; pub use utility::{HelpOverlayClick, HelpOverlayCursorHint, HelpOverlayReleaseOutcome}; +pub(crate) use utility::{HelpOverlayPressSource, SequenceMatch, default_step_marker_size}; diff --git a/src/input/state/core/modal.rs b/src/input/state/core/modal.rs index 4550d2b47..590863193 100644 --- a/src/input/state/core/modal.rs +++ b/src/input/state/core/modal.rs @@ -308,6 +308,9 @@ impl InputState { #[cfg(test)] mod wheel_tests { + use crate::draw::{Color, Shape, TextMeasurer}; + use crate::ui::toolbar::PrecisionEntryTarget; + use super::ModalSurface; use crate::input::state::test_support::make_test_input_state; @@ -326,7 +329,7 @@ mod wheel_tests { assert!(state.modal_owns_wheel(), "colour picker"); state.close_color_picker_popup(false); - state.open_precision_entry(crate::ui::toolbar::PrecisionEntryTarget::Thickness); + state.open_precision_entry(PrecisionEntryTarget::Thickness); assert!(state.modal_owns_wheel(), "precision entry"); } @@ -337,7 +340,7 @@ mod wheel_tests { // stop at them; the wheel used to carry on to zoom, Spotlight, and // stroke thickness behind them. let mut state = make_test_input_state(); - state.activate_eyedropper_with(&crate::draw::TextMeasurer::default(), None); + state.activate_eyedropper_with(&TextMeasurer::default(), None); assert!(state.eyedropper_is_active()); assert!(state.modal_owns_wheel()); @@ -351,7 +354,7 @@ mod wheel_tests { state.open_font_picker(); assert!(state.is_font_picker_open()); - state.prepare_for_screen_modal_with_measurer(&crate::draw::TextMeasurer::default()); + state.prepare_for_screen_modal_with_measurer(&TextMeasurer::default()); assert!(!state.is_font_picker_open()); assert!( @@ -364,23 +367,20 @@ mod wheel_tests { #[test] fn the_properties_panel_leaves_the_wheel_to_the_canvas() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); // It docks beside the canvas rather than over it, and the canvas stays // drawable underneath, so the wheel still means what it means elsewhere. let mut state = make_test_input_state(); - let id = state - .boards - .active_frame_mut() - .add_shape(crate::draw::Shape::Rect { - x: 0, - y: 0, - w: 10, - h: 10, - fill: false, - fill_color: None, - color: crate::draw::Color::new(1.0, 1.0, 1.0, 1.0), - thick: 2.0, - }); + let id = state.boards.active_frame_mut().add_shape(Shape::Rect { + x: 0, + y: 0, + w: 10, + h: 10, + fill: false, + fill_color: None, + color: Color::new(1.0, 1.0, 1.0, 1.0), + thick: 2.0, + }); state.set_selection(vec![id]); assert!(state.show_properties_panel_with(&route_measurer)); @@ -390,22 +390,19 @@ mod wheel_tests { #[test] fn a_popup_a_canvas_click_would_only_dismiss_hides_the_tool_preview() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); assert!(!state.canvas_press_dismisses_popup()); - let id = state - .boards - .active_frame_mut() - .add_shape(crate::draw::Shape::Rect { - x: 0, - y: 0, - w: 10, - h: 10, - fill: false, - fill_color: None, - color: crate::draw::Color::new(1.0, 1.0, 1.0, 1.0), - thick: 2.0, - }); + let id = state.boards.active_frame_mut().add_shape(Shape::Rect { + x: 0, + y: 0, + w: 10, + h: 10, + fill: false, + fill_color: None, + color: Color::new(1.0, 1.0, 1.0, 1.0), + thick: 2.0, + }); state.set_selection(vec![id]); assert!(state.show_properties_panel_with(&measurer)); @@ -419,6 +416,7 @@ mod wheel_tests { #[cfg(test)] mod toolbar_chrome_tests { use crate::draw::TextMeasurer; + use crate::input::state::ContextMenuKind; use crate::input::state::test_support::make_test_input_state; #[test] @@ -470,12 +468,7 @@ mod toolbar_chrome_tests { fn pointer_anchored_surfaces_and_the_tour_keep_the_toolbar() { let mut state = make_test_input_state(); - state.open_context_menu( - (40, 40), - Vec::new(), - crate::input::state::ContextMenuKind::Canvas, - None, - ); + state.open_context_menu((40, 40), Vec::new(), ContextMenuKind::Canvas, None); assert!(state.is_context_menu_open()); assert!(!state.modal_hides_toolbar_chrome(), "context menu"); state.close_context_menu(); diff --git a/src/input/state/core/modes.rs b/src/input/state/core/modes.rs index 62efd36b1..d2a5486ea 100644 --- a/src/input/state/core/modes.rs +++ b/src/input/state/core/modes.rs @@ -220,26 +220,34 @@ pub(in crate::input::state) struct PresenterRestore { tool_override: Option>, } +/// The chrome settings presenter mode can override, as they are on entry. +#[derive(Debug, Clone, Copy)] +pub(in crate::input::state) struct PresenterChrome { + pub(in crate::input::state) show_status_bar: bool, + pub(in crate::input::state) show_tool_preview: bool, + pub(in crate::input::state) toolbar_visibility: ToolbarVisibility, + pub(in crate::input::state) click_highlight_enabled: bool, + pub(in crate::input::state) input_hud_enabled: bool, + pub(in crate::input::state) tool_override: Option, +} + impl PresenterRestore { pub(in crate::input::state) fn capture( config: &PresenterModeConfig, - show_status_bar: bool, - show_tool_preview: bool, - toolbar_visibility: ToolbarVisibility, - click_highlight_enabled: bool, - input_hud_enabled: bool, - tool_override: Option, + current: PresenterChrome, ) -> Self { Self { - show_status_bar: config.hide_status_bar.then_some(show_status_bar), - show_tool_preview: config.hide_tool_preview.then_some(show_tool_preview), - toolbar_visibility: config.hide_toolbars.then_some(toolbar_visibility), + show_status_bar: config.hide_status_bar.then_some(current.show_status_bar), + show_tool_preview: config + .hide_tool_preview + .then_some(current.show_tool_preview), + toolbar_visibility: config.hide_toolbars.then_some(current.toolbar_visibility), click_highlight_enabled: config .enable_click_highlight - .then_some(click_highlight_enabled), - input_hud_enabled: config.enable_input_hud.then_some(input_hud_enabled), + .then_some(current.click_highlight_enabled), + input_hud_enabled: config.enable_input_hud.then_some(current.input_hud_enabled), tool_override: (!matches!(config.tool_behavior, PresenterToolBehavior::Keep)) - .then_some(tool_override), + .then_some(current.tool_override), } } @@ -361,15 +369,29 @@ impl LightModeRestore { #[cfg(test)] mod tests { + use crate::input::state; + use super::*; use crate::config::{PresenterModeConfig, PresenterToolbarMode}; use crate::input::state::core::ToolbarInteraction; use crate::input::tool::Tool; - fn toolbar_visibility() -> crate::input::state::core::ToolbarVisibility { + fn toolbar_visibility() -> state::core::ToolbarVisibility { ToolbarInteraction::default().visibility_snapshot() } + /// Status bar and tool preview shown, highlights and HUD off, no override. + fn shown_chrome() -> PresenterChrome { + PresenterChrome { + show_status_bar: true, + show_tool_preview: true, + toolbar_visibility: toolbar_visibility(), + click_highlight_enabled: false, + input_hud_enabled: false, + tool_override: None, + } + } + #[test] fn presenter_snapshot_round_trips_complete_toolbar_visibility_for_micro_mapping() { let config = PresenterModeConfig { @@ -380,12 +402,11 @@ mod tests { let visibility = toolbar_visibility(); let restore = PresenterRestore::capture( &config, - true, - true, - visibility, - false, - false, - Some(Tool::Arrow), + PresenterChrome { + toolbar_visibility: visibility, + tool_override: Some(Tool::Arrow), + ..shown_chrome() + }, ); let mut modes = ChromeModes::new(config); @@ -401,22 +422,12 @@ mod tests { hide_toolbars: false, ..PresenterModeConfig::default() }; - let restore = PresenterRestore::capture( - &config, - true, - true, - toolbar_visibility(), - false, - false, - None, - ); + let restore = PresenterRestore::capture(&config, shown_chrome()); let mut modes = ChromeModes::new(config); modes.begin_presenter(restore); - assert!( - !modes.retarget_presenter_toolbar_display_mode(crate::config::TopDisplayMode::Micro) - ); + assert!(!modes.retarget_presenter_toolbar_display_mode(TopDisplayMode::Micro)); assert_eq!(modes.end_presenter().unwrap().toolbar_visibility(), None); } diff --git a/src/input/state/core/pointer.rs b/src/input/state/core/pointer.rs index e0af38deb..817d9dbcc 100644 --- a/src/input/state/core/pointer.rs +++ b/src/input/state/core/pointer.rs @@ -1,5 +1,7 @@ //! Pointer positions, hover invalidation, activity timing, and provisional bounds. +use std::mem; + use crate::input::tool::LiveShapeMemo; use crate::util::Rect; use std::time::Instant; @@ -70,7 +72,7 @@ impl PointerTracking { } pub(in crate::input::state) fn take_menu_hover_recalc(&mut self) -> bool { - std::mem::take(&mut self.menu_hover_recalc_pending) + mem::take(&mut self.menu_hover_recalc_pending) } pub(in crate::input::state) fn clear_menu_hover_recalc(&mut self) { @@ -97,7 +99,7 @@ impl PointerTracking { &mut self, bounds: Option, ) -> Option { - std::mem::replace(&mut self.provisional_bounds, bounds) + mem::replace(&mut self.provisional_bounds, bounds) } /// Forget the previous stroke's Shape Pen preview. @@ -116,7 +118,7 @@ impl PointerTracking { &mut self, shape: bool, ) -> bool { - std::mem::replace(&mut self.live_shape_previewed_shape, shape) + mem::replace(&mut self.live_shape_previewed_shape, shape) } pub(in crate::input::state) fn union_provisional_bounds(&mut self, bounds: Rect) { diff --git a/src/input/state/core/presets.rs b/src/input/state/core/presets.rs index f26972bfa..9b8499e0d 100644 --- a/src/input/state/core/presets.rs +++ b/src/input/state/core/presets.rs @@ -134,7 +134,7 @@ impl Default for PresetSlots { impl From<&PresetSlotsConfig> for PresetSlots { fn from(config: &PresetSlotsConfig) -> Self { let mut presets = (1..=PRESET_SLOTS_MAX) - .map(|slot| config.get_slot(slot).cloned()) + .map(|slot| config.slot(slot).cloned()) .collect::>(); presets.resize_with(PRESET_SLOTS_MAX, || None); @@ -149,13 +149,16 @@ impl From<&PresetSlotsConfig> for PresetSlots { #[cfg(test)] mod tests { + use crate::config::ColorSpec; + use crate::input::Tool; + use super::*; fn marker_preset() -> ToolPresetConfig { ToolPresetConfig { name: Some("Marker".into()), - tool: crate::input::Tool::Marker, - color: crate::config::ColorSpec::Name("yellow".into()), + tool: Tool::Marker, + color: ColorSpec::Name("yellow".into()), size: 8.0, tool_settings: None, eraser_kind: None, diff --git a/src/input/state/core/properties/apply_selection/actions/arrow.rs b/src/input/state/core/properties/apply_selection/actions/arrow.rs index b005994a1..687ce3262 100644 --- a/src/input/state/core/properties/apply_selection/actions/arrow.rs +++ b/src/input/state/core/properties/apply_selection/actions/arrow.rs @@ -1,5 +1,4 @@ -use crate::draw::TextMeasurer; -use crate::draw::{ArrowStyle, Shape}; +use crate::draw::{ArrowStyle, Shape, TextMeasurer}; use crate::i18n::tr; use crate::input::state::core::base::InputState; use crate::input::state::core::properties::apply_selection::constants::{ @@ -59,7 +58,7 @@ impl InputState { }, ); - self.report_selection_apply_result(result, tr("arrow head")) + self.report_selection_apply_result(&result, tr("arrow head")) } /// Steps the style of every selected arrow. @@ -73,7 +72,7 @@ impl InputState { direction: i32, ) -> bool { let target = self.selection_arrow_style_target(direction); - self.restyle_selected_arrows(measurer, target) + self.restyle_selected_arrows(measurer, &target) } /// Sets every editable selected arrow to `style`, as picking one of the @@ -87,15 +86,15 @@ impl InputState { ArrowStyleTarget::Style(_) => ArrowStyleTarget::Style(style), other => other, }; - self.restyle_selected_arrows(measurer, target) + self.restyle_selected_arrows(measurer, &target) } fn restyle_selected_arrows( &mut self, measurer: &TextMeasurer, - target: ArrowStyleTarget, + target: &ArrowStyleTarget, ) -> bool { - let target = match target { + let target = match *target { ArrowStyleTarget::NoArrows => { self.push_toast( ToastPriority::Info, @@ -133,7 +132,7 @@ impl InputState { }, ); - self.report_selection_apply_result(result, tr("arrow style")) + self.report_selection_apply_result(&result, tr("arrow style")) } /// The style a restyle should move the selection to. @@ -194,7 +193,7 @@ impl InputState { }, ); - self.report_selection_apply_result(result, tr("arrow length")) + self.report_selection_apply_result(&result, tr("arrow length")) } pub(in crate::input::state::core::properties) fn apply_selection_arrow_angle( @@ -220,23 +219,25 @@ impl InputState { }, ); - self.report_selection_apply_result(result, tr("arrow angle")) + self.report_selection_apply_result(&result, tr("arrow angle")) } } #[cfg(test)] mod tests { + use crate::domain::Action; + use crate::draw::ShapeId; + use crate::input::SelectionPropertyKind; + use crate::input::state::test_support; + use crate::ui_text::UiTextEngine; + use super::*; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } - fn add_arrow( - state: &mut InputState, - head_at_end: bool, - arrow_angle: f64, - ) -> crate::draw::ShapeId { + fn add_arrow(state: &mut InputState, head_at_end: bool, arrow_angle: f64) -> ShapeId { add_styled_arrow(state, head_at_end, arrow_angle, ArrowStyle::Standard, 0.0) } @@ -246,7 +247,7 @@ mod tests { arrow_angle: f64, style: ArrowStyle, bend: f64, - ) -> crate::draw::ShapeId { + ) -> ShapeId { state.boards.active_frame_mut().add_shape(Shape::Arrow { x1: 0, y1: 0, @@ -263,14 +264,14 @@ mod tests { }) } - fn arrow_style(state: &InputState, id: crate::draw::ShapeId) -> ArrowStyle { + fn arrow_style(state: &InputState, id: ShapeId) -> ArrowStyle { match &state.boards.active_frame().shape(id).expect("arrow").shape { Shape::Arrow { style, .. } => *style, other => panic!("expected arrow, got {other:?}"), } } - fn arrow_bend(state: &InputState, id: crate::draw::ShapeId) -> f64 { + fn arrow_bend(state: &InputState, id: ShapeId) -> f64 { match &state.boards.active_frame().shape(id).expect("arrow").shape { Shape::Arrow { bend, .. } => *bend, other => panic!("expected arrow, got {other:?}"), @@ -379,8 +380,8 @@ mod tests { #[test] fn cycle_arrow_style_reports_a_locked_arrow_while_its_property_control_stays_disabled() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -402,7 +403,7 @@ mod tests { let style_entry = state .build_selection_property_entries(&[id]) .into_iter() - .find(|entry| entry.kind == crate::input::SelectionPropertyKind::ArrowStyle) + .find(|entry| entry.kind == SelectionPropertyKind::ArrowStyle) .expect("locked arrow style property"); assert!( style_entry.disabled, @@ -410,10 +411,7 @@ mod tests { ); assert_eq!(style_entry.value, "Locked"); - state.handle_action_with_resources( - test_text_resources, - crate::domain::Action::CycleArrowStyle, - ); + state.handle_action_with_resources(test_text_resources, Action::CycleArrowStyle); assert_eq!( state.active_toast().map(|toast| toast.message.as_str()), Some("All arrow style shapes are locked.") diff --git a/src/input/state/core/properties/apply_selection/actions/color.rs b/src/input/state/core/properties/apply_selection/actions/color.rs index 97483fb67..71550515a 100644 --- a/src/input/state/core/properties/apply_selection/actions/color.rs +++ b/src/input/state/core/properties/apply_selection/actions/color.rs @@ -1,5 +1,4 @@ -use crate::draw::TextMeasurer; -use crate::draw::{Color, Shape}; +use crate::draw::{Color, Shape, TextMeasurer}; use crate::i18n::tr; use crate::input::state::core::base::InputState; use crate::input::state::core::properties::apply_selection::constants::{ @@ -156,7 +155,7 @@ impl InputState { }, ); - self.report_selection_apply_result(result, tr("color")) + self.report_selection_apply_result(&result, tr("color")) } /// Whether recoloring the selection to `target` would change any shape it @@ -191,7 +190,7 @@ impl InputState { }, ); - self.report_selection_apply_result(result, tr("opacity")) + self.report_selection_apply_result(&result, tr("opacity")) } /// Steps the selection's color through the quick-color palette, the same @@ -210,7 +209,7 @@ impl InputState { .collect(); let current = self .selection_primary_color() - .and_then(|color| palette_position(palette.iter().copied(), color)); + .and_then(|color| palette_position(&palette.iter().copied(), color)); let offset = if direction < 0 { -1 } else { 1 }; let Some(first) = palette_step(palette.len(), current, offset) else { self.push_toast( @@ -238,16 +237,19 @@ impl InputState { #[cfg(test)] mod tests { + use crate::draw::ShapeId; + use crate::input::state::test_support; + use super::*; use crate::draw::RED; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } #[test] fn apply_selection_color_value_preserves_marker_alpha() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_state(); let marker_id = state .boards @@ -286,7 +288,7 @@ mod tests { } } - fn add_rect(state: &mut InputState, color: Color) -> crate::draw::ShapeId { + fn add_rect(state: &mut InputState, color: Color) -> ShapeId { state.boards.active_frame_mut().add_shape(Shape::Rect { x: 0, y: 0, @@ -299,7 +301,7 @@ mod tests { }) } - fn rect_color(state: &InputState, id: crate::draw::ShapeId) -> Color { + fn rect_color(state: &InputState, id: ShapeId) -> Color { match &state.boards.active_frame().shape(id).expect("rect").shape { Shape::Rect { color, .. } => *color, other => panic!("expected rect, got {other:?}"), diff --git a/src/input/state/core/properties/apply_selection/actions/fill.rs b/src/input/state/core/properties/apply_selection/actions/fill.rs index 5d98063b6..3d4acfb04 100644 --- a/src/input/state/core/properties/apply_selection/actions/fill.rs +++ b/src/input/state/core/properties/apply_selection/actions/fill.rs @@ -1,6 +1,5 @@ use super::color::RecolorOpacity; -use crate::draw::TextMeasurer; -use crate::draw::{Color, Shape}; +use crate::draw::{Color, Shape, TextMeasurer}; use crate::i18n::tr; use crate::input::state::core::base::InputState; use crate::input::state::{Toast, ToastPriority}; @@ -52,7 +51,7 @@ impl InputState { }, ); - self.report_selection_apply_result(result, tr("fill")) + self.report_selection_apply_result(&result, tr("fill")) } /// Fills every editable selected closed shape with `paint`, or turns the @@ -84,7 +83,7 @@ impl InputState { }, ); - self.report_selection_apply_result(result, tr("fill")) + self.report_selection_apply_result(&result, tr("fill")) } /// The fill of the first editable selected closed shape, for the color @@ -197,10 +196,12 @@ fn fill_fields(shape: &mut Shape) -> Option<(&mut bool, &mut Option)> { #[cfg(test)] mod tests { + use crate::input::state::test_support; + use super::*; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } #[test] diff --git a/src/input/state/core/properties/apply_selection/actions/preset.rs b/src/input/state/core/properties/apply_selection/actions/preset.rs index bda69f17b..f235699e1 100644 --- a/src/input/state/core/properties/apply_selection/actions/preset.rs +++ b/src/input/state/core/properties/apply_selection/actions/preset.rs @@ -210,7 +210,7 @@ impl InputState { apply_preset_style(shape, &preset) }); - self.report_selection_apply_result(result, tr("preset")) + self.report_selection_apply_result(&result, tr("preset")) } /// The preset for the first editable selected shape a tool can draw. diff --git a/src/input/state/core/properties/apply_selection/actions/spotlight.rs b/src/input/state/core/properties/apply_selection/actions/spotlight.rs index e691310f7..507500b48 100644 --- a/src/input/state/core/properties/apply_selection/actions/spotlight.rs +++ b/src/input/state/core/properties/apply_selection/actions/spotlight.rs @@ -1,5 +1,5 @@ -use crate::draw::TextMeasurer; -use crate::draw::{SPOTLIGHT_MAGNIFICATION_STEP, Shape}; +use crate::draw; +use crate::draw::{SPOTLIGHT_MAGNIFICATION_STEP, Shape, TextMeasurer}; use crate::i18n::tr; use crate::input::state::core::base::InputState; @@ -16,12 +16,10 @@ impl InputState { |shape| matches!(shape, Shape::Spotlight { .. }), |shape| match shape { Shape::Spotlight { magnification, .. } => { - let next = - crate::draw::normalize_spotlight_magnification(*magnification + delta); + let next = draw::normalize_spotlight_magnification(*magnification + delta); if (next - *magnification).abs() > f64::EPSILON { *magnification = next; - changed_to_magnified |= - crate::draw::spotlight_magnification_is_active(next); + changed_to_magnified |= draw::spotlight_magnification_is_active(next); true } else { false @@ -31,7 +29,7 @@ impl InputState { }, ); - let applied = self.report_selection_apply_result(result, tr("Spotlight magnification")); + let applied = self.report_selection_apply_result(&result, tr("Spotlight magnification")); if applied && changed_to_magnified { self.request_spotlight_magnifier_feedback(); } diff --git a/src/input/state/core/properties/apply_selection/actions/stroke.rs b/src/input/state/core/properties/apply_selection/actions/stroke.rs index 1f5d39283..5f492735f 100644 --- a/src/input/state/core/properties/apply_selection/actions/stroke.rs +++ b/src/input/state/core/properties/apply_selection/actions/stroke.rs @@ -1,5 +1,5 @@ -use crate::draw::Shape; -use crate::draw::TextMeasurer; +use crate::domain; +use crate::draw::{Shape, TextMeasurer}; use crate::i18n::tr; use crate::input::state::PressureThicknessEditMode; use crate::input::state::core::base::{InputState, MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; @@ -44,7 +44,7 @@ impl InputState { strength: thick, .. } | Shape::MarkerStroke { thick, .. } => { - let next = crate::domain::step_stroke_thickness(*thick, delta); + let next = domain::step_stroke_thickness(*thick, delta); if (next - *thick).abs() > f64::EPSILON { *thick = next; true @@ -88,7 +88,7 @@ impl InputState { }, ); - self.report_selection_apply_result(result, tr("thickness")) + self.report_selection_apply_result(&result, tr("thickness")) } } diff --git a/src/input/state/core/properties/apply_selection/actions/text.rs b/src/input/state/core/properties/apply_selection/actions/text.rs index a88b497f6..1c3e81104 100644 --- a/src/input/state/core/properties/apply_selection/actions/text.rs +++ b/src/input/state/core/properties/apply_selection/actions/text.rs @@ -1,5 +1,4 @@ -use crate::draw::Shape; -use crate::draw::TextMeasurer; +use crate::draw::{Shape, TextMeasurer}; use crate::i18n::tr; use crate::input::state::core::base::InputState; use crate::input::state::core::properties::apply_selection::constants::{ @@ -31,7 +30,7 @@ impl InputState { }, ); - self.report_selection_apply_result(result, tr("font size")) + self.report_selection_apply_result(&result, tr("font size")) } pub(in crate::input::state::core::properties) fn apply_selection_text_background( @@ -73,16 +72,18 @@ impl InputState { }, ); - self.report_selection_apply_result(result, tr("text background")) + self.report_selection_apply_result(&result, tr("text background")) } } #[cfg(test)] mod tests { + use crate::input::state::test_support; + use super::*; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } #[test] diff --git a/src/input/state/core/properties/apply_selection/helpers.rs b/src/input/state/core/properties/apply_selection/helpers.rs index 6c4c52db6..73aeb7741 100644 --- a/src/input/state/core/properties/apply_selection/helpers.rs +++ b/src/input/state/core/properties/apply_selection/helpers.rs @@ -91,7 +91,7 @@ impl InputState { pub(in crate::input::state::core) fn report_selection_apply_result( &mut self, - result: SelectionApplyResult, + result: &SelectionApplyResult, label: &str, ) -> bool { if result.applicable == 0 { diff --git a/src/input/state/core/properties/apply_selection/helpers/tests.rs b/src/input/state/core/properties/apply_selection/helpers/tests.rs index 2c98edd9c..565e845c3 100644 --- a/src/input/state/core/properties/apply_selection/helpers/tests.rs +++ b/src/input/state/core/properties/apply_selection/helpers/tests.rs @@ -1,19 +1,17 @@ +use crate::draw::ShapeId; +use crate::input::state::test_support; + use super::*; use crate::draw::Color; fn make_state() -> InputState { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.update_screen_dimensions(200, 120); let _ = state.take_dirty_regions(); state } -fn add_rect( - state: &mut InputState, - color: Color, - fill: bool, - locked: bool, -) -> crate::draw::ShapeId { +fn add_rect(state: &mut InputState, color: Color, fill: bool, locked: bool) -> ShapeId { let id = state.boards.active_frame_mut().add_shape(Shape::Rect { x: 10, y: 20, @@ -237,7 +235,7 @@ fn report_selection_apply_result_emits_expected_toasts() { let mut state = make_state(); assert!(!state.report_selection_apply_result( - SelectionApplyResult { + &SelectionApplyResult { changed: 0, locked: 0, applicable: 0, @@ -250,7 +248,7 @@ fn report_selection_apply_result_emits_expected_toasts() { ); assert!(!state.report_selection_apply_result( - SelectionApplyResult { + &SelectionApplyResult { changed: 0, locked: 2, applicable: 2, @@ -263,7 +261,7 @@ fn report_selection_apply_result_emits_expected_toasts() { ); assert!(!state.report_selection_apply_result( - SelectionApplyResult { + &SelectionApplyResult { changed: 0, locked: 1, applicable: 2, @@ -276,7 +274,7 @@ fn report_selection_apply_result_emits_expected_toasts() { ); assert!(state.report_selection_apply_result( - SelectionApplyResult { + &SelectionApplyResult { changed: 1, locked: 2, applicable: 3, diff --git a/src/input/state/core/properties/entries.rs b/src/input/state/core/properties/entries.rs index 130539193..af24f964c 100644 --- a/src/input/state/core/properties/entries.rs +++ b/src/input/state/core/properties/entries.rs @@ -1,3 +1,5 @@ +use crate::draw; + use super::super::base::InputState; use super::summary::{ PropertySummary, resolve_selected_shapes, shape_arrow_angle, shape_arrow_head, @@ -250,7 +252,7 @@ impl InputState { tr("Magnification"), SelectionPropertyKind::SpotlightMagnification, &spotlight_summary, - crate::draw::format_spotlight_magnification, + draw::format_spotlight_magnification, SelectionPropertyValue::Number, )); } @@ -264,10 +266,11 @@ mod tests { use super::*; use crate::domain::color::{PALETTE_GREEN, PALETTE_RED}; - use crate::draw::{ArrowStyle, Color}; + use crate::draw::{ArrowStyle, Color, Frame}; + use crate::input::state::test_support::TestInputStateBuilder; fn make_state() -> InputState { - crate::input::state::test_support::TestInputStateBuilder::default() + TestInputStateBuilder::default() .text_background_enabled(true) .build() } @@ -299,13 +302,13 @@ mod tests { for selected in [vec![ids[2], ids[1_000], ids[2_047]], ids] { state.set_selection(selected); - crate::draw::Frame::reset_linear_id_lookup_count(); + Frame::reset_linear_id_lookup_count(); let entries = state.selection_pill_entries(); assert_eq!(entry(&entries, "Thickness").value, "3.0px"); assert_eq!(entry(&entries, "Opacity").value, "100%"); assert!(!entry(&entries, "Color").disabled); - assert_eq!(crate::draw::Frame::linear_id_lookup_count(), 0); + assert_eq!(Frame::linear_id_lookup_count(), 0); } } @@ -466,7 +469,7 @@ mod tests { fn property_entries_name_colors_from_the_quick_color_palette() { let mut state = make_state(); let palette_green = add_line(&mut state, PALETTE_GREEN); - let pure_green = add_line(&mut state, crate::draw::GREEN); + let pure_green = add_line(&mut state, draw::GREEN); let entries = state.build_selection_property_entries(&[palette_green]); assert_eq!(entry(&entries, "Color").value, "Green"); diff --git a/src/input/state/core/properties/metrics.rs b/src/input/state/core/properties/metrics.rs index 30d9aedf2..6f1b78f09 100644 --- a/src/input/state/core/properties/metrics.rs +++ b/src/input/state/core/properties/metrics.rs @@ -1,6 +1,8 @@ //! Sizes shared by the properties panel's layout, hit-testing, and painting, //! so what the pointer hits is exactly what the renderer draws. +use crate::ui_text::UiTextStyle; + pub(crate) const TITLE_FONT: f64 = 15.0; pub(crate) const SUBTITLE_FONT: f64 = 12.0; pub(crate) const BODY_FONT: f64 = 13.0; @@ -103,10 +105,7 @@ pub(crate) const TOOLTIP_PADDING_Y: f64 = 5.0; pub(crate) const TOOLTIP_GAP: f64 = 6.0; /// The panel's text style at `size`. -pub(crate) fn text_style( - size: f64, - weight: cairo::FontWeight, -) -> crate::ui_text::UiTextStyle<'static> { +pub(crate) fn text_style(size: f64, weight: cairo::FontWeight) -> UiTextStyle<'static> { crate::ui_text::UiTextStyle { family: "Sans", slant: cairo::FontSlant::Normal, diff --git a/src/input/state/core/properties/panel.rs b/src/input/state/core/properties/panel.rs index b487f11a1..3445a130c 100644 --- a/src/input/state/core/properties/panel.rs +++ b/src/input/state/core/properties/panel.rs @@ -1,3 +1,5 @@ +use crate::input::state::core::modal::ModalSurface; + use super::super::base::InputState; use super::metrics::MAX_SWATCHES; use super::panel_layout::selection_panel_anchor; @@ -90,7 +92,7 @@ impl InputState { return false; } - self.close_modals_for_open(crate::input::state::core::modal::ModalSurface::PropertiesPanel); + self.close_modals_for_open(ModalSurface::PropertiesPanel); let Some(contents) = self.properties_panel_contents(measurer) else { return false; @@ -288,11 +290,13 @@ impl InputState { #[cfg(test)] mod tests { + use crate::input::state::test_support; + use super::*; use crate::draw::{Shape, ShapeId}; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } fn add_rect(state: &mut InputState, x: i32, y: i32, w: i32, h: i32) -> ShapeId { diff --git a/src/input/state/core/properties/panel_layout/focus.rs b/src/input/state/core/properties/panel_layout/focus.rs index b80c062fa..9136185a2 100644 --- a/src/input/state/core/properties/panel_layout/focus.rs +++ b/src/input/state/core/properties/panel_layout/focus.rs @@ -116,16 +116,19 @@ impl InputState { #[cfg(test)] mod tests { + use crate::draw::TextMeasurer; + use crate::input::state::test_support; + use super::super::super::types::PropertiesPanelHit; use super::*; use crate::draw::Shape; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } fn open_rect_panel(state: &mut InputState) { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Rect { x: 10, y: 20, diff --git a/src/input/state/core/properties/panel_layout/geometry.rs b/src/input/state/core/properties/panel_layout/geometry.rs index 90b2f82b5..2f1289dfa 100644 --- a/src/input/state/core/properties/panel_layout/geometry.rs +++ b/src/input/state/core/properties/panel_layout/geometry.rs @@ -546,6 +546,7 @@ fn swatch_grid(x: f64, top: f64, swatches: usize, no_fill: bool) -> PropertiesRo impl SelectionPropertyEntry { /// What a stepper shows between its buttons. A pressure stroke's width /// varies along it, which a stepper readout can only say briefly. + #[must_use] pub fn stepper_text(&self) -> &str { match self.state { SelectionPropertyValue::PressureVaries if !self.disabled => tr("Varies"), @@ -563,7 +564,7 @@ impl ShapePropertiesPanel { else { return None; }; - palette_position(self.swatches.iter().map(|swatch| swatch.color), color) + palette_position(&self.swatches.iter().map(|swatch| swatch.color), color) } /// The text shown while the pointer rests on `hit`, if it has any. diff --git a/src/input/state/core/properties/panel_layout/layout.rs b/src/input/state/core/properties/panel_layout/layout.rs index dbdf617ba..7893ebee1 100644 --- a/src/input/state/core/properties/panel_layout/layout.rs +++ b/src/input/state/core/properties/panel_layout/layout.rs @@ -1,5 +1,7 @@ -use cairo::Context as CairoContext; -use cairo::FontWeight; +use cairo::{Context as CairoContext, FontWeight}; + +use crate::draw::TextMeasurer; +use crate::ui::theme::overlay; use super::super::super::base::InputState; use super::super::metrics::{ @@ -89,11 +91,7 @@ fn measure_panel( .map(|text| width(VALUE_FONT, FontWeight::Normal, text)) .fold(0.0, f64::max); let empty_width = if panel.entries.is_empty() { - width( - BODY_FONT, - FontWeight::Normal, - crate::ui::theme::overlay::empty_properties(), - ) + width(BODY_FONT, FontWeight::Normal, overlay::empty_properties()) } else { 0.0 }; @@ -156,7 +154,7 @@ impl InputState { ) { self.update_properties_panel_layout_with_resources( &UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), ctx, screen_width, screen_height, @@ -166,7 +164,7 @@ impl InputState { pub(crate) fn update_properties_panel_layout_with_resources( &mut self, engine: &UiTextEngine, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ctx: &CairoContext, screen_width: u32, screen_height: u32, diff --git a/src/input/state/core/properties/panel_layout/tests.rs b/src/input/state/core/properties/panel_layout/tests.rs index 4f74f5700..d86260f46 100644 --- a/src/input/state/core/properties/panel_layout/tests.rs +++ b/src/input/state/core/properties/panel_layout/tests.rs @@ -1,3 +1,9 @@ +use std::collections::BTreeSet; + +use crate::input::Key; +use crate::input::state::test_support; +use crate::ui; + use super::super::metrics::{ACTIONS_HEIGHT, FOOTER_HEIGHT, PADDING_BOTTOM}; use super::super::types::{ PanelRect, PropertiesPanelHit, PropertiesRowControl, PropertiesRowGeometry, @@ -62,7 +68,7 @@ fn paint(state: &InputState) -> Vec { .unwrap(); { let ctx = cairo::Context::new(&surface).unwrap(); - crate::ui::render_properties_panel_with_engine( + ui::render_properties_panel_with_engine( &UiTextEngine::default(), &ctx, state, @@ -146,7 +152,7 @@ fn control_hits(row: &PropertiesRowGeometry) -> Vec<(PanelRect, PropertiesPanelH #[test] fn rows_stack_from_the_divider_to_the_footer() { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let id = arrow(&mut state); open_panel(vec![id], &mut state); let layout = *state.properties_panel_layout().expect("layout"); @@ -174,7 +180,7 @@ fn rows_stack_from_the_divider_to_the_footer() { #[test] fn every_control_is_hit_where_it_is_drawn_and_stays_in_its_row() { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let arrow = arrow(&mut state); let note = text(&mut state, "Label"); let rect = state.boards.active_frame_mut().add_shape(Shape::Rect { @@ -218,7 +224,7 @@ fn every_control_is_hit_where_it_is_drawn_and_stays_in_its_row() { #[test] fn a_small_screen_keeps_the_whole_panel_on_screen() { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let id = arrow(&mut state); state.set_selection(vec![id]); assert!(state.show_properties_panel_with(&TextMeasurer::default())); @@ -233,7 +239,7 @@ fn a_small_screen_keeps_the_whole_panel_on_screen() { #[test] fn hovering_a_swatch_shows_its_name_in_a_tooltip_on_screen() { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let id = arrow(&mut state); open_panel(vec![id], &mut state); let (swatch, expected) = rows(&state) @@ -266,7 +272,7 @@ fn hovering_a_swatch_shows_its_name_in_a_tooltip_on_screen() { #[test] fn deferred_refresh_updates_the_header_before_layout_and_paint() { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let id = text(&mut state, "你好 initial"); open_panel(vec![id], &mut state); let before = state.properties_panel().unwrap().subtitle.clone(); @@ -331,7 +337,7 @@ fn assert_all_reachable(state: &InputState, height: f64) { #[test] fn a_slightly_short_screen_drops_the_keyboard_hints_first() { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let id = arrow(&mut state); open_panel(vec![id], &mut state); let full = state.properties_panel_layout().expect("layout").height; @@ -347,7 +353,7 @@ fn a_slightly_short_screen_drops_the_keyboard_hints_first() { #[test] fn a_selection_taller_than_the_screen_flows_into_columns() { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let rect = state.boards.active_frame_mut().add_shape(Shape::Rect { x: 300, y: 200, @@ -379,8 +385,7 @@ fn a_selection_taller_than_the_screen_flows_into_columns() { let layout = *state.properties_panel_layout().expect("layout"); let rows = rows(&state); - let columns: std::collections::BTreeSet = - rows.iter().map(|row| row.content_x.to_bits()).collect(); + let columns: BTreeSet = rows.iter().map(|row| row.content_x.to_bits()).collect(); assert!(columns.len() > 1, "the rows spread over columns"); assert!(layout.width > single.width); assert!(layout.origin_x + layout.width <= f64::from(wide) - 12.0 + 1e-9); @@ -443,7 +448,7 @@ fn assert_visible_rows_reachable(state: &InputState, screen: (f64, f64)) -> Vec< fn when_columns_would_leave_the_screen_the_rows_scroll_instead() { let measurer = TextMeasurer::default(); let screen = (480, 360); - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let ids = four_kinds(&mut state); open_panel(ids, &mut state); @@ -494,7 +499,7 @@ fn when_columns_would_leave_the_screen_the_rows_scroll_instead() { #[test] fn keyboard_focus_scrolls_its_row_into_view() { let screen = (480, 360); - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let ids = four_kinds(&mut state); open_panel(ids, &mut state); lay_out(&mut state, screen); @@ -520,7 +525,7 @@ fn keyboard_focus_scrolls_its_row_into_view() { fn scrolling_after_a_click_moves_hover_off_the_control_that_scrolled_away() { let measurer = TextMeasurer::default(); let screen = (480, 360); - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let ids = four_kinds(&mut state); open_panel(ids, &mut state); lay_out(&mut state, screen); @@ -565,7 +570,7 @@ fn scrolling_after_a_click_moves_hover_off_the_control_that_scrolled_away() { fn keyboard_scrolling_drops_the_hover_of_a_swatch_it_scrolls_away() { let measurer = TextMeasurer::default(); let screen = (480, 360); - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let ids = four_kinds(&mut state); open_panel(ids, &mut state); lay_out(&mut state, screen); @@ -590,9 +595,7 @@ fn keyboard_scrolling_drops_the_hover_of_a_swatch_it_scrolls_away() { .expect("arrow length row"); state.set_properties_panel_focus(Some(0)); while state.properties_panel().unwrap().keyboard_focus != Some(length) { - assert!( - state.handle_properties_panel_key_with_measurer(&measurer, crate::input::Key::Down) - ); + assert!(state.handle_properties_panel_key_with_measurer(&measurer, Key::Down)); lay_out(&mut state, screen); } diff --git a/src/input/state/core/properties/slider.rs b/src/input/state/core/properties/slider.rs index 6fbbe1f96..493ffcddd 100644 --- a/src/input/state/core/properties/slider.rs +++ b/src/input/state/core/properties/slider.rs @@ -1,6 +1,8 @@ //! Dragging a properties-panel slider: the shapes follow the thumb live, and //! the whole drag lands as one undo entry when it ends. +use crate::input::state::PanelRect; + use super::super::base::InputState; use super::apply_selection::{level_applies, set_level}; use super::types::{PropertiesRowControl, SelectionPropertyKind}; @@ -113,7 +115,7 @@ impl InputState { } } - fn properties_slider_track(&self, row: usize) -> Option { + fn properties_slider_track(&self, row: usize) -> Option { let panel = self.properties.panel.as_ref()?; let layout = self.properties.layout.as_ref()?; match layout.rows(panel).into_iter().nth(row)?.control { diff --git a/src/input/state/core/properties/state.rs b/src/input/state/core/properties/state.rs index 278ffdd2c..4bc29fdf0 100644 --- a/src/input/state/core/properties/state.rs +++ b/src/input/state/core/properties/state.rs @@ -64,6 +64,8 @@ impl PropertiesPanelState { #[cfg(test)] mod tests { + use crate::input::state::PropertiesPanelLock; + use super::*; fn panel() -> ShapePropertiesPanel { @@ -71,7 +73,7 @@ mod tests { title: "Properties".into(), subtitle: None, details: None, - lock: crate::input::state::PropertiesPanelLock::Unlocked, + lock: PropertiesPanelLock::Unlocked, anchor: (10.0, 20.0), anchor_rect: None, entries: Vec::new(), diff --git a/src/input/state/core/properties/summary.rs b/src/input/state/core/properties/summary.rs index 5c3f66816..4bef7d6c8 100644 --- a/src/input/state/core/properties/summary.rs +++ b/src/input/state/core/properties/summary.rs @@ -1,10 +1,12 @@ +#[cfg(test)] +use std::cell::Cell; use std::collections::HashMap; use crate::draw::{ArrowStyle, Color, DrawnShape, Frame, Shape, ShapeId}; #[cfg(test)] thread_local! { - static SELECTION_RESOLUTIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static SELECTION_RESOLUTIONS: Cell = const { Cell::new(0) }; } impl super::super::InputState { @@ -19,7 +21,7 @@ impl super::super::InputState { #[cfg(test)] pub(crate) fn selection_resolution_count() -> usize { - SELECTION_RESOLUTIONS.with(std::cell::Cell::get) + SELECTION_RESOLUTIONS.with(Cell::get) } } diff --git a/src/input/state/core/properties/types.rs b/src/input/state/core/properties/types.rs index bf6970e35..e0a3c9a01 100644 --- a/src/input/state/core/properties/types.rs +++ b/src/input/state/core/properties/types.rs @@ -1,3 +1,5 @@ +use crate::domain; + use super::metrics::SLIDER_THUMB_RADIUS; use crate::draw::{ArrowStyle, Color}; use crate::util::Rect; @@ -56,11 +58,12 @@ fn slider_travel(track: PanelRect) -> f64 { impl SelectionPropertyKind { /// The slider range of a property set on a slider. + #[must_use] pub fn level_range(self) -> Option { match self { Self::Thickness => Some(LevelRange { - min: crate::domain::MIN_STROKE_THICKNESS, - max: crate::domain::MAX_STROKE_THICKNESS, + min: domain::MIN_STROKE_THICKNESS, + max: domain::MAX_STROKE_THICKNESS, step: 1.0, }), Self::Opacity => Some(LevelRange { @@ -75,6 +78,7 @@ impl SelectionPropertyKind { /// The kind's value with nothing known about it, for fixtures that only /// care about an entry's text. #[cfg(test)] + #[must_use] pub fn unknown_value(self) -> SelectionPropertyValue { match self { Self::Color => SelectionPropertyValue::Color(None), diff --git a/src/input/state/core/properties/utils.rs b/src/input/state/core/properties/utils.rs index 0a7b2bf11..17c4d2d73 100644 --- a/src/input/state/core/properties/utils.rs +++ b/src/input/state/core/properties/utils.rs @@ -35,7 +35,7 @@ pub(super) fn color_palette_index( palette: &[QuickColorPaletteEntry], color: Color, ) -> Option { - palette_position(palette.iter().map(|entry| entry.color), color) + palette_position(&palette.iter().map(|entry| entry.color), color) } /// Where `color` sits among `colors`: the entry with exactly this color and @@ -44,7 +44,7 @@ pub(super) fn color_palette_index( /// apart; a recolor copies the palette's value, so an exact match is there /// to find. pub(super) fn palette_position( - colors: impl Iterator + Clone, + colors: &(impl Iterator + Clone), color: Color, ) -> Option { colors @@ -155,17 +155,17 @@ mod tests { let colors = [PALETTE_RED, translucent_red, PALETTE_GREEN]; assert_eq!( - palette_position(colors.into_iter(), translucent_red), + palette_position(&colors.into_iter(), translucent_red), Some(1) ); - assert_eq!(palette_position(colors.into_iter(), PALETTE_RED), Some(0)); + assert_eq!(palette_position(&colors.into_iter(), PALETTE_RED), Some(0)); let nearly_opaque = Color { a: 254.0 / 255.0, ..PALETTE_RED }; let close = [PALETTE_RED, nearly_opaque]; assert_eq!( - palette_position(close.into_iter(), nearly_opaque), + palette_position(&close.into_iter(), nearly_opaque), Some(1), "near-identical opacities are still different entries" ); @@ -174,7 +174,7 @@ mod tests { ..PALETTE_RED }; assert_eq!( - palette_position(colors.into_iter(), other_opacity), + palette_position(&colors.into_iter(), other_opacity), Some(0), "without an exact match the first entry of the hue stands in" ); diff --git a/src/input/state/core/radial_menu/compass.rs b/src/input/state/core/radial_menu/compass.rs index ff1fb59b9..2c32576e4 100644 --- a/src/input/state/core/radial_menu/compass.rs +++ b/src/input/state/core/radial_menu/compass.rs @@ -11,6 +11,7 @@ use std::sync::OnceLock; use crate::domain::Action; use crate::input::tool::Tool; +use crate::ui::toolbar::model; /// Number of segments in the primary compass ring. pub const TOOL_SEGMENT_COUNT: usize = 8; @@ -45,6 +46,7 @@ impl CompassDir { ]; /// Primary-ring segment index of this direction. + #[must_use] pub fn index(self) -> u8 { self as u8 } @@ -63,6 +65,7 @@ pub enum RadialParent { impl RadialParent { /// Wedge label. Parents are not actions, so their labels live here /// rather than in the `ActionMeta` registry. + #[must_use] pub fn label(self) -> &'static str { match self { RadialParent::Shapes => "Shapes", @@ -72,6 +75,7 @@ impl RadialParent { /// Sub-ring children in slice order (child 0 sits at the parent wedge's /// leading edge). + #[must_use] pub fn children(self) -> &'static [Action] { match self { RadialParent::Shapes => shapes_children(), @@ -91,7 +95,7 @@ const NOTES_CHILDREN: [Action; 2] = [Action::SelectStepMarkerTool, Action::Enter fn shapes_children() -> &'static [Action] { static CHILDREN: OnceLock> = OnceLock::new(); CHILDREN.get_or_init(|| { - crate::ui::toolbar::model::shape_tools() + model::shape_tools() .iter() .filter(|tool| { matches!( @@ -162,11 +166,13 @@ pub const COMPASS_SLICES: [RadialSlice; TOOL_SEGMENT_COUNT] = [ ]; /// Slice at a primary-ring segment index. +#[must_use] pub fn compass_slice(idx: u8) -> Option<&'static RadialSlice> { COMPASS_SLICES.get(idx as usize) } /// Parent at a segment index, if that slice expands a sub-ring. +#[must_use] pub fn slice_parent(idx: u8) -> Option { match compass_slice(idx)?.kind { RadialSliceKind::Parent(parent) => Some(parent), @@ -182,6 +188,7 @@ pub fn sub_ring_children(parent_idx: u8) -> &'static [Action] { } /// Number of sub-ring children for a segment index, or 0 if none. +#[must_use] pub fn sub_ring_child_count(parent_idx: u8) -> usize { sub_ring_children(parent_idx).len() } diff --git a/src/input/state/core/radial_menu/mod.rs b/src/input/state/core/radial_menu/mod.rs index 5e35c62bf..cce7760a8 100644 --- a/src/input/state/core/radial_menu/mod.rs +++ b/src/input/state/core/radial_menu/mod.rs @@ -16,6 +16,8 @@ pub use size_ring::{ SIZE_RING_ARC_SPAN, SIZE_RING_ARC_START, size_ring_angle_for_value, size_ring_value_for_angle, }; +use crate::draw::Color; + /// How long an open radial menu stays unpainted. Layout and hit-testing are /// live from the moment of opening so a press-flick-release commits without /// the menu ever appearing; only painting waits out this window. This is an @@ -76,7 +78,7 @@ pub enum RadialSegmentId { #[derive(Debug, Clone, Copy, PartialEq)] pub struct RadialRingSwatch { /// Swatch fill color. - pub color: crate::draw::Color, + pub color: Color, /// True for the appended recent-color segments. pub recent: bool, } diff --git a/src/input/state/core/radial_menu/size_ring.rs b/src/input/state/core/radial_menu/size_ring.rs index b0b655039..b915b4aea 100644 --- a/src/input/state/core/radial_menu/size_ring.rs +++ b/src/input/state/core/radial_menu/size_ring.rs @@ -19,6 +19,7 @@ pub const SIZE_RING_ARC_START: f64 = 0.75 * PI; pub const SIZE_RING_ARC_SPAN: f64 = 1.5 * PI; /// Angle (raw atan2 space) for a thickness value, clamped to the gauge span. +#[must_use] pub fn size_ring_angle_for_value(value: f64) -> f64 { let frac = ((value - MIN_STROKE_THICKNESS) / (MAX_STROKE_THICKNESS - MIN_STROKE_THICKNESS)) .clamp(0.0, 1.0); @@ -28,6 +29,7 @@ pub fn size_ring_angle_for_value(value: f64) -> f64 { /// Thickness value for an angle (raw atan2 space): the exact inverse of /// [`size_ring_angle_for_value`] within the span. Angles in the bottom gap /// snap to the nearest end so drags past the ends pin to min/max. +#[must_use] pub fn size_ring_value_for_angle(angle: f64) -> f64 { let rel = normalize_rel(angle); let rel = if rel <= SIZE_RING_ARC_SPAN { diff --git a/src/input/state/core/radial_menu/state.rs b/src/input/state/core/radial_menu/state.rs index 9dc68ccaa..adf009833 100644 --- a/src/input/state/core/radial_menu/state.rs +++ b/src/input/state/core/radial_menu/state.rs @@ -1,10 +1,15 @@ use std::time::{Duration, Instant}; +use crate::draw::TextMeasurer; +use crate::input::state::InputTextResources; +use crate::input::state::core::modal::ModalSurface; +use crate::ui_text::UiTextEngine; + use super::layout::CENTER_RADIUS; use super::size_ring::size_ring_value_for_angle; -use super::{RadialMenuState, RadialRingSwatch, RadialSegmentId}; use super::{ - RadialSliceKind, compass_slice, slice_parent, sub_ring_child_count, sub_ring_children, + RadialMenuState, RadialRingSwatch, RadialSegmentId, RadialSliceKind, compass_slice, + slice_parent, sub_ring_child_count, sub_ring_children, }; use crate::input::events::MouseButton; use crate::input::state::InputState; @@ -16,7 +21,7 @@ impl InputState { } fn open_radial_menu_internal(&mut self, x: f64, y: f64, track_usage: bool) { - self.close_modals_for_open(crate::input::state::core::modal::ModalSurface::RadialMenu); + self.close_modals_for_open(ModalSurface::RadialMenu); self.radial_menu.open(x, y, Instant::now()); if track_usage { @@ -139,8 +144,8 @@ impl InputState { /// Select the currently hovered segment and close the menu. pub fn radial_menu_select_hovered(&mut self) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.radial_menu_select_hovered_with_resources(crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, @@ -149,7 +154,7 @@ impl InputState { pub(crate) fn radial_menu_select_hovered_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, ) { let hover = match &self.radial_menu.state { RadialMenuState::Open { hover, .. } => *hover, @@ -185,13 +190,13 @@ impl InputState { /// Adjust thickness via scroll wheel while the menu is open. pub fn radial_menu_adjust_thickness(&mut self, delta: f64) -> bool { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); self.radial_menu_adjust_thickness_with_measurer(&measurer, delta) } pub(crate) fn radial_menu_adjust_thickness_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, delta: f64, ) -> bool { if !self.nudge_thickness_for_active_tool_with(measurer, delta) { @@ -224,7 +229,7 @@ impl InputState { /// (click-to-open browsing). pub(crate) fn radial_menu_handle_release_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, button: MouseButton, x: f64, y: f64, @@ -323,7 +328,7 @@ impl InputState { /// value immediately). pub(crate) fn radial_menu_begin_size_drag_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, x: f64, y: f64, ) { @@ -335,7 +340,7 @@ impl InputState { /// it to the active tool, regardless of pointer distance. pub(crate) fn radial_menu_drag_size_to_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, x: f64, y: f64, ) { @@ -404,11 +409,7 @@ impl InputState { // never behave differently from the same action fired by a keybinding // or the toolbar. - fn dispatch_tool_segment_with_resources( - &mut self, - resources: crate::input::state::InputTextResources<'_>, - idx: u8, - ) { + fn dispatch_tool_segment_with_resources(&mut self, resources: InputTextResources<'_>, idx: u8) { if let Some(slice) = compass_slice(idx) && let RadialSliceKind::Action(action) = slice.kind { @@ -418,7 +419,7 @@ impl InputState { fn dispatch_sub_tool_segment_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, parent: u8, child: u8, ) { @@ -427,11 +428,7 @@ impl InputState { } } - fn dispatch_color_segment_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - idx: u8, - ) { + fn dispatch_color_segment_with_measurer(&mut self, measurer: &TextMeasurer, idx: u8) { if let Some(swatch) = self.radial_ring_swatches().get(idx as usize) { self.apply_color_from_ui_with_measurer(measurer, swatch.color); } diff --git a/src/input/state/core/recognition_chip.rs b/src/input/state/core/recognition_chip.rs index 42093187c..6b95023d7 100644 --- a/src/input/state/core/recognition_chip.rs +++ b/src/input/state/core/recognition_chip.rs @@ -8,6 +8,8 @@ use std::time::{Duration, Instant}; +use crate::ui::anim; + use super::base::InputState; use crate::domain::Action; use crate::draw::Shape; @@ -52,7 +54,7 @@ impl RecognitionChip { /// Fully opaque, then fading out over the end of its lifetime. Under /// `[ui] reduced_motion` it stays opaque and then disappears. pub(crate) fn opacity(&self, now: Instant) -> f64 { - crate::ui::anim::end_fade( + anim::end_fade( self.age(now).as_secs_f64(), RECOGNITION_CHIP_LIFETIME.as_secs_f64(), RECOGNITION_CHIP_FADE.as_secs_f64(), @@ -181,6 +183,8 @@ fn recognized_shape_label(shape: &Shape) -> &'static str { #[cfg(test)] mod tests { + use crate::config::Config; + use super::*; use crate::draw::Color; @@ -205,7 +209,7 @@ mod tests { #[test] fn the_drawing_config_switch_reaches_input_state() { - let mut config = crate::config::Config::default(); + let mut config = Config::default(); assert!( InputState::from_config(&config) .recognition_feedback diff --git a/src/input/state/core/region_select.rs b/src/input/state/core/region_select.rs index 6ac98ce94..d951f310c 100644 --- a/src/input/state/core/region_select.rs +++ b/src/input/state/core/region_select.rs @@ -1,3 +1,6 @@ +#[cfg(test)] +use crate::draw::TextMeasurer; + use super::InputState; /// Why the generalized screen-region selector is engaged. @@ -17,24 +20,29 @@ pub struct SelectionPolicy { } impl SelectionPolicy { + #[must_use] pub const fn min_submit_logical_px(self) -> Option { self.min_submit_logical_px } + #[must_use] pub const fn allow_square(self) -> bool { self.allow_square } + #[must_use] pub const fn snap_anchor(self) -> bool { self.snap_anchor } } impl RegionPurposeTag { + #[must_use] pub const fn is_capture(self) -> bool { matches!(self, Self::CaptureDeliver | Self::CaptureInteractive) } + #[must_use] pub const fn selection_policy(self) -> SelectionPolicy { match self { Self::Ocr => SelectionPolicy { @@ -131,6 +139,7 @@ pub enum RegionSelectUiState { } impl RegionSelectUiState { + #[must_use] pub fn is_active(self) -> bool { matches!( self, @@ -141,27 +150,33 @@ impl RegionSelectUiState { ) } + #[must_use] pub fn is_pending(self) -> bool { matches!(self, Self::PendingCapture { .. }) } + #[must_use] pub fn is_engaged(self) -> bool { self.is_active() || self.is_pending() } + #[must_use] pub fn is_selecting(self) -> bool { matches!(self, Self::Selecting { .. }) } + #[must_use] pub fn is_review(self) -> bool { matches!(self, Self::Review { .. }) } + #[must_use] pub fn is_measured(self) -> bool { matches!(self, Self::Measured { .. }) } /// The region drag in logical screen coordinates, if one is in progress. + #[must_use] pub fn selection(self) -> Option { match self { Self::Selecting { start, current, .. } => Some(RegionSelection { @@ -174,6 +189,7 @@ impl RegionSelectUiState { } /// The device dragging the region, if one is in progress. + #[must_use] pub fn selection_owner(self) -> Option { match self { Self::Selecting { owner, .. } => Some(owner), @@ -185,6 +201,7 @@ impl RegionSelectUiState { } } + #[must_use] pub fn pending_source(self) -> Option { match self { Self::PendingCapture { source, .. } => Some(source), @@ -196,6 +213,7 @@ impl RegionSelectUiState { } } + #[must_use] pub fn purpose(self) -> Option { match self { Self::PendingCapture { purpose, .. } @@ -207,6 +225,7 @@ impl RegionSelectUiState { } } + #[must_use] pub fn generation(self) -> Option { match self { Self::PendingCapture { generation, .. } @@ -267,11 +286,7 @@ impl InputState { #[cfg(test)] impl InputState { - pub(crate) fn activate_measure_mode_with( - &mut self, - measurer: &crate::draw::TextMeasurer, - generation: u64, - ) { + pub(crate) fn activate_measure_mode_with(&mut self, measurer: &TextMeasurer, generation: u64) { self.activate_region_with(measurer, RegionPurposeTag::Measure, generation); } @@ -290,7 +305,7 @@ impl InputState { pub(crate) fn activate_region_with( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, purpose: RegionPurposeTag, generation: u64, ) { @@ -478,6 +493,9 @@ impl InputState { #[cfg(test)] mod tests { + use crate::domain::Action; + use crate::ui_text::UiTextEngine; + use super::*; use crate::input::state::test_support::make_test_input_state; use crate::input::state::{InputEffect, InputEffectDrain}; @@ -489,11 +507,7 @@ mod tests { // interaction, tool, and history guarantees at their generalized seam. fn activate_ocr_region(state: &mut InputState, generation: u64) { - state.activate_region_with( - &crate::draw::TextMeasurer::default(), - RegionPurposeTag::Ocr, - generation, - ); + state.activate_region_with(&TextMeasurer::default(), RegionPurposeTag::Ocr, generation); } #[test] @@ -596,7 +610,7 @@ mod tests { #[test] fn measure_mode_keeps_the_completed_rectangle_without_capture_state() { let mut state = make_test_input_state(); - state.activate_measure_mode_with(&crate::draw::TextMeasurer::default(), 7); + state.activate_measure_mode_with(&TextMeasurer::default(), 7); assert!(state.start_region_selection(RegionInputSource::Pointer, (12.0, 18.0))); state.update_region_selection(RegionInputSource::Pointer, (42.0, 63.0)); @@ -833,8 +847,8 @@ mod tests { /// remove the stroke drawn before it, proving no region history entry was added. #[test] fn the_selector_lifecycle_adds_no_shape_and_no_history_entry() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -852,7 +866,7 @@ mod tests { state.cancel_region_ui_only(); assert_eq!(state.boards.active_frame().shapes.len(), 1); - state.handle_action_with_resources(test_text_resources, crate::domain::Action::Undo); + state.handle_action_with_resources(test_text_resources, Action::Undo); assert_eq!(state.boards.active_frame().shapes.len(), 0); } diff --git a/src/input/state/core/selection/clipboard.rs b/src/input/state/core/selection/clipboard.rs index 211deab3a..6786a943a 100644 --- a/src/input/state/core/selection/clipboard.rs +++ b/src/input/state/core/selection/clipboard.rs @@ -1,6 +1,9 @@ //! Clipboard state for copied selections and image-save fallback. -use crate::capture::{ImageOperationKind, file::FileSaveConfig}; +use std::process; + +use crate::capture::ImageOperationKind; +use crate::capture::file::FileSaveConfig; use crate::draw::Shape; use crate::input::state::core::base::{ ClipboardFingerprint, ClipboardPasteRequest, PasteAnchor, PendingSelectionClipboardPublish, @@ -385,7 +388,7 @@ fn new_clipboard_app_instance_id() -> String { .duration_since(UNIX_EPOCH) .map(|duration| duration.as_millis()) .unwrap_or(0); - format!("{}-{millis}", std::process::id()) + format!("{}-{millis}", process::id()) } #[cfg(test)] diff --git a/src/input/state/core/selection_actions/arrow_bend.rs b/src/input/state/core/selection_actions/arrow_bend.rs index 167c2a90e..390930296 100644 --- a/src/input/state/core/selection_actions/arrow_bend.rs +++ b/src/input/state/core/selection_actions/arrow_bend.rs @@ -6,9 +6,12 @@ //! dragging it sets that scalar from the pointer's perpendicular distance to //! the chord. -use crate::draw::TextMeasurer; -use crate::draw::{ArrowStyle, Shape, ShapeId}; +use std::mem; + +use crate::draw::frame::{ShapeSnapshot, UndoAction}; +use crate::draw::{ArrowStyle, Shape, ShapeId, TextMeasurer}; use crate::input::InputState; +use crate::input::state::DrawingState; use crate::util::{self, Rect}; /// Side length of the square bend handle, in canvas pixels. @@ -95,7 +98,7 @@ impl InputState { y: i32, snap: bool, ) -> bool { - let crate::input::state::DrawingState::BendingArrow { shape_id, .. } = self.state else { + let DrawingState::BendingArrow { shape_id, .. } = self.state else { return false; }; let frame = self.boards.active_frame(); @@ -172,14 +175,11 @@ impl InputState { // Checked before the take, not after: `mem::replace` would install // `Idle` on the way to discovering there was no bend to end, silently // cancelling whatever interaction was actually running. - if !matches!( - self.state, - crate::input::state::DrawingState::BendingArrow { .. } - ) { + if !matches!(self.state, DrawingState::BendingArrow { .. }) { return false; } - let crate::input::state::DrawingState::BendingArrow { shape_id, snapshot } = - std::mem::replace(&mut self.state, crate::input::state::DrawingState::Idle) + let DrawingState::BendingArrow { shape_id, snapshot } = + mem::replace(&mut self.state, DrawingState::Idle) else { return false; }; @@ -193,15 +193,11 @@ impl InputState { /// The live updates during the drag deliberately recorded nothing, so the /// whole gesture undoes in one step rather than once per motion event. A /// drag that ended where it started records nothing at all. - pub(crate) fn commit_arrow_bend( - &mut self, - shape_id: ShapeId, - snapshot: crate::draw::frame::ShapeSnapshot, - ) { + pub(crate) fn commit_arrow_bend(&mut self, shape_id: ShapeId, snapshot: ShapeSnapshot) { let Some(shape) = self.boards.active_frame().shape(shape_id) else { return; }; - let after = crate::draw::frame::ShapeSnapshot { + let after = ShapeSnapshot { shape: shape.shape.clone(), locked: shape.locked, }; @@ -210,7 +206,7 @@ impl InputState { } let limit = self.history_limits.undo_stack_limit(); self.boards.active_frame_mut().push_undo_action( - crate::draw::frame::UndoAction::Modify { + UndoAction::Modify { shape_id, before: snapshot, after, @@ -223,7 +219,7 @@ impl InputState { /// Bit pattern of an arrow snapshot's bend, so the comparison above is exact /// rather than an epsilon nobody chose. -fn snapshot_bend(snapshot: &crate::draw::frame::ShapeSnapshot) -> Option { +fn snapshot_bend(snapshot: &ShapeSnapshot) -> Option { match &snapshot.shape { Shape::Arrow { bend, .. } => Some(bend.to_bits()), _ => None, diff --git a/src/input/state/core/selection_actions/arrow_bend/tests.rs b/src/input/state/core/selection_actions/arrow_bend/tests.rs index 6accbe9a2..9be857856 100644 --- a/src/input/state/core/selection_actions/arrow_bend/tests.rs +++ b/src/input/state/core/selection_actions/arrow_bend/tests.rs @@ -1,3 +1,9 @@ +use crate::config::Action; +use crate::input::Key; +use crate::input::state::core::properties::SelectionPropertyKind; +use crate::ui::toolbar::ToolbarEvent; +use crate::ui_text::UiTextEngine; + use super::*; use crate::draw::Color; use crate::input::state::DrawingState; @@ -8,7 +14,7 @@ use crate::input::state::test_support::make_test_input_state; /// `head_at_end` is true, so `(x2, y2)` is the tip and the tail-to-tip /// direction runs left to right — which is what the bend's sign is measured /// against. -fn add_curved_arrow(state: &mut crate::input::InputState, bend: f64) -> crate::draw::ShapeId { +fn add_curved_arrow(state: &mut InputState, bend: f64) -> ShapeId { state.boards.active_frame_mut().add_shape(Shape::Arrow { x1: 0, y1: 100, @@ -97,14 +103,14 @@ fn handle_is_offered_only_for_a_single_unlocked_curved_arrow() { #[test] fn dragging_the_handle_bends_toward_the_pointer() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let id = add_curved_arrow(&mut state, 0.0); state.set_selection(vec![id]); state.state = DrawingState::BendingArrow { shape_id: id, - snapshot: crate::draw::frame::ShapeSnapshot { + snapshot: ShapeSnapshot { shape: state .boards .active_frame() @@ -129,14 +135,14 @@ fn dragging_the_handle_bends_toward_the_pointer() { #[test] fn dragging_along_the_chord_does_not_change_the_bend() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let id = add_curved_arrow(&mut state, 0.0); state.set_selection(vec![id]); state.state = DrawingState::BendingArrow { shape_id: id, - snapshot: crate::draw::frame::ShapeSnapshot { + snapshot: ShapeSnapshot { shape: state .boards .active_frame() @@ -161,14 +167,14 @@ fn dragging_along_the_chord_does_not_change_the_bend() { #[test] fn shift_snaps_the_bend_to_tenths() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let id = add_curved_arrow(&mut state, 0.0); state.set_selection(vec![id]); state.state = DrawingState::BendingArrow { shape_id: id, - snapshot: crate::draw::frame::ShapeSnapshot { + snapshot: ShapeSnapshot { shape: state .boards .active_frame() @@ -213,7 +219,7 @@ fn the_handle_follows_the_head_end() { ); } -fn arrow_bend(state: &crate::input::InputState, id: crate::draw::ShapeId) -> f64 { +fn arrow_bend(state: &InputState, id: ShapeId) -> f64 { match &state.boards.active_frame().shape(id).expect("arrow").shape { Shape::Arrow { bend, .. } => *bend, other => panic!("expected arrow, got {other:?}"), @@ -222,8 +228,8 @@ fn arrow_bend(state: &crate::input::InputState, id: crate::draw::ShapeId) -> f64 #[test] fn restyling_mid_gesture_ends_the_bend_instead_of_stacking_on_it() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -238,7 +244,7 @@ fn restyling_mid_gesture_ends_the_bend_instead_of_stacking_on_it() { let mut state = make_test_input_state(); let id = add_curved_arrow(&mut state, 0.0); state.set_selection(vec![id]); - let before = crate::draw::frame::ShapeSnapshot { + let before = ShapeSnapshot { shape: state .boards .active_frame() @@ -256,7 +262,7 @@ fn restyling_mid_gesture_ends_the_bend_instead_of_stacking_on_it() { let bent = arrow_bend(&state, id); assert!(bent.abs() > 0.1, "test setup should have bent the arrow"); - state.handle_action_with_resources(test_text_resources, crate::config::Action::CycleArrowStyle); + state.handle_action_with_resources(test_text_resources, Action::CycleArrowStyle); assert!( matches!(state.state, DrawingState::Idle), @@ -268,15 +274,15 @@ fn restyling_mid_gesture_ends_the_bend_instead_of_stacking_on_it() { // Undo the restyle, then the bend, and the arrow is back where it started // with nothing in between that was never drawn. - state.handle_action_with_resources(test_text_resources, crate::config::Action::Undo); + state.handle_action_with_resources(test_text_resources, Action::Undo); assert_eq!(arrow_style(&state, id), ArrowStyle::Curved); assert!((arrow_bend(&state, id) - bent).abs() < 1e-9); - state.handle_action_with_resources(test_text_resources, crate::config::Action::Undo); + state.handle_action_with_resources(test_text_resources, Action::Undo); assert_eq!(arrow_bend(&state, id), 0.0); assert_eq!(arrow_style(&state, id), ArrowStyle::Curved); } -fn arrow_style(state: &crate::input::InputState, id: crate::draw::ShapeId) -> ArrowStyle { +fn arrow_style(state: &InputState, id: ShapeId) -> ArrowStyle { match &state.boards.active_frame().shape(id).expect("arrow").shape { Shape::Arrow { style, .. } => *style, other => panic!("expected arrow, got {other:?}"), @@ -285,8 +291,8 @@ fn arrow_style(state: &crate::input::InputState, id: crate::draw::ShapeId) -> Ar #[test] fn any_selection_property_change_ends_a_live_bend_first() { - let route_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &test_ui_engine, @@ -302,7 +308,7 @@ fn any_selection_property_change_ends_a_live_bend_first() { state.set_selection(vec![id]); state.state = DrawingState::BendingArrow { shape_id: id, - snapshot: crate::draw::frame::ShapeSnapshot { + snapshot: ShapeSnapshot { shape: state .boards .active_frame() @@ -316,11 +322,7 @@ fn any_selection_property_change_ends_a_live_bend_first() { assert!(state.drag_arrow_bend_to_with(&route_measurer, 200, 20, false)); let bent = arrow_bend(&state, id); - state.adjust_selection_property_kind_with( - &route_measurer, - crate::input::state::core::properties::SelectionPropertyKind::Thickness, - 1, - ); + state.adjust_selection_property_kind_with(&route_measurer, SelectionPropertyKind::Thickness, 1); assert!( matches!(state.state, DrawingState::Idle), @@ -328,14 +330,14 @@ fn any_selection_property_change_ends_a_live_bend_first() { ); // Undoing the thickness change must leave the bend intact rather than // rolling the arrow back past a gesture that had already been committed. - state.handle_action_with_resources(test_text_resources, crate::config::Action::Undo); + state.handle_action_with_resources(test_text_resources, Action::Undo); assert!((arrow_bend(&state, id) - bent).abs() < 1e-9); } #[test] fn a_nudge_key_mid_gesture_ends_the_bend_instead_of_stacking_on_it() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -355,7 +357,7 @@ fn a_nudge_key_mid_gesture_ends_the_bend_instead_of_stacking_on_it() { state.set_selection(vec![id]); state.state = DrawingState::BendingArrow { shape_id: id, - snapshot: crate::draw::frame::ShapeSnapshot { + snapshot: ShapeSnapshot { shape: state .boards .active_frame() @@ -370,7 +372,7 @@ fn a_nudge_key_mid_gesture_ends_the_bend_instead_of_stacking_on_it() { let bent = arrow_bend(&state, id); assert!(bent.abs() > 0.1, "test setup should have bent the arrow"); - state.on_key_press(crate::input::Key::Right); + state.on_key_press(Key::Right); assert!( matches!(state.state, DrawingState::Idle), @@ -379,15 +381,15 @@ fn a_nudge_key_mid_gesture_ends_the_bend_instead_of_stacking_on_it() { assert_eq!(state.boards.active_frame().undo_stack_len(), 2); // Undo the nudge, then the bend. Neither step may resurrect the other. - state.handle_action_with_resources(test_text_resources, crate::config::Action::Undo); + state.handle_action_with_resources(test_text_resources, Action::Undo); assert!((arrow_bend(&state, id) - bent).abs() < 1e-9); - state.handle_action_with_resources(test_text_resources, crate::config::Action::Undo); + state.handle_action_with_resources(test_text_resources, Action::Undo); assert_eq!(arrow_bend(&state, id), 0.0); } #[test] fn escape_still_cancels_a_bend_rather_than_committing_it() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); // `route_action` ends a live bend for every action except Exit, whose whole // job is to back out of the gesture. Committing first would leave Escape @@ -398,7 +400,7 @@ fn escape_still_cancels_a_bend_rather_than_committing_it() { state.set_selection(vec![id]); state.state = DrawingState::BendingArrow { shape_id: id, - snapshot: crate::draw::frame::ShapeSnapshot { + snapshot: ShapeSnapshot { shape: state .boards .active_frame() @@ -411,7 +413,7 @@ fn escape_still_cancels_a_bend_rather_than_committing_it() { }; assert!(state.drag_arrow_bend_to_with(&test_text_measurer, 200, 20, false)); - state.on_key_press(crate::input::Key::Escape); + state.on_key_press(Key::Escape); assert!(matches!(state.state, DrawingState::Idle)); assert_eq!(arrow_bend(&state, id), 0.0, "Escape kept the bend"); @@ -448,8 +450,8 @@ fn an_action_with_no_bend_running_leaves_the_interaction_alone() { #[test] fn a_toolbar_event_mid_gesture_ends_the_bend_before_it_can_lose_the_arrow() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -464,7 +466,7 @@ fn a_toolbar_event_mid_gesture_ends_the_bend_before_it_can_lose_the_arrow() { state.set_selection(vec![id]); state.state = DrawingState::BendingArrow { shape_id: id, - snapshot: crate::draw::frame::ShapeSnapshot { + snapshot: ShapeSnapshot { shape: state .boards .active_frame() @@ -478,7 +480,7 @@ fn a_toolbar_event_mid_gesture_ends_the_bend_before_it_can_lose_the_arrow() { assert!(state.drag_arrow_bend_to_with(&test_text_measurer, 200, 20, false)); let bent = arrow_bend(&state, id); - state.apply_toolbar_event(crate::ui::toolbar::ToolbarEvent::UndoAll); + state.apply_toolbar_event(ToolbarEvent::UndoAll); assert!( matches!(state.state, DrawingState::Idle), @@ -486,7 +488,7 @@ fn a_toolbar_event_mid_gesture_ends_the_bend_before_it_can_lose_the_arrow() { ); // The bend was recorded before Undo All ran, so it is on the stack to be // undone rather than lost with the shape. - state.handle_action_with_resources(test_text_resources, crate::config::Action::RedoAll); + state.handle_action_with_resources(test_text_resources, Action::RedoAll); assert!( (arrow_bend(&state, id) - bent).abs() < 1e-9, "the bend was dropped instead of committed before the toolbar event" diff --git a/src/input/state/core/selection_actions/clipboard.rs b/src/input/state/core/selection_actions/clipboard.rs index 4f6b913a2..38d388d64 100644 --- a/src/input/state/core/selection_actions/clipboard.rs +++ b/src/input/state/core/selection_actions/clipboard.rs @@ -1,8 +1,7 @@ use super::super::base::{ClipboardPasteRequest, InputState, PasteAnchor}; use super::super::selection::LocalSelectionContext; -use crate::draw::Shape; -use crate::draw::TextMeasurer; use crate::draw::frame::UndoAction; +use crate::draw::{Shape, TextMeasurer}; use crate::i18n::{tr, trf}; use crate::input::state::{Toast, ToastPriority}; use crate::util::Rect; diff --git a/src/input/state/core/selection_actions/clipboard/image_paste.rs b/src/input/state/core/selection_actions/clipboard/image_paste.rs index 6a9a1a7a0..5cd5f134c 100644 --- a/src/input/state/core/selection_actions/clipboard/image_paste.rs +++ b/src/input/state/core/selection_actions/clipboard/image_paste.rs @@ -1,7 +1,6 @@ use super::super::super::base::{ClipboardPasteRequest, InputState}; -use crate::draw::TextMeasurer; use crate::draw::frame::UndoAction; -use crate::draw::{EmbeddedImage, Shape}; +use crate::draw::{EmbeddedImage, Shape, TextMeasurer}; use crate::i18n::tr; use crate::input::state::{Toast, ToastPriority}; diff --git a/src/input/state/core/selection_actions/delete.rs b/src/input/state/core/selection_actions/delete.rs index 192be71dc..10a7963a0 100644 --- a/src/input/state/core/selection_actions/delete.rs +++ b/src/input/state/core/selection_actions/delete.rs @@ -1,6 +1,5 @@ use super::super::base::InputState; -use crate::draw::ShapeId; -use crate::draw::TextMeasurer; +use crate::draw::{ShapeId, TextMeasurer}; use std::borrow::Cow; use std::collections::HashSet; diff --git a/src/input/state/core/selection_actions/delete/tests.rs b/src/input/state/core/selection_actions/delete/tests.rs index 8f4f1a89a..888dd6917 100644 --- a/src/input/state/core/selection_actions/delete/tests.rs +++ b/src/input/state/core/selection_actions/delete/tests.rs @@ -1,8 +1,10 @@ +use crate::input::state::test_support::TestInputStateBuilder; + use super::*; use crate::draw::FontDescriptor; fn create_test_input_state() -> InputState { - crate::input::state::test_support::TestInputStateBuilder::default() + TestInputStateBuilder::default() .thickness(3.0) .eraser_size(12.0) .font_descriptor(FontDescriptor { @@ -31,7 +33,7 @@ fn sample_eraser_path_points_returns_borrowed_for_single_point() { let points = vec![(5, 7)]; let sampled = state.sample_eraser_path_points(&points); - assert!(matches!(sampled, std::borrow::Cow::Borrowed(_))); + assert!(matches!(sampled, Cow::Borrowed(_))); assert_eq!(sampled.as_ref(), points.as_slice()); } @@ -41,7 +43,7 @@ fn sample_eraser_path_points_returns_borrowed_for_dense_segments() { let points = vec![(0, 0), (2, 0), (4, 0)]; let sampled = state.sample_eraser_path_points(&points); - assert!(matches!(sampled, std::borrow::Cow::Borrowed(_))); + assert!(matches!(sampled, Cow::Borrowed(_))); assert_eq!(sampled.as_ref(), points.as_slice()); } @@ -51,7 +53,7 @@ fn sample_eraser_path_points_avoids_duplicate_points_when_sampling() { let points = vec![(0, 0), (0, 20), (0, 20), (0, 40)]; let sampled = state.sample_eraser_path_points(&points); - assert!(matches!(sampled, std::borrow::Cow::Owned(_))); + assert!(matches!(sampled, Cow::Owned(_))); for pair in sampled.windows(2) { assert_ne!(pair[0], pair[1]); } diff --git a/src/input/state/core/selection_actions/geometry.rs b/src/input/state/core/selection_actions/geometry.rs index a283d8732..4ff76ed45 100644 --- a/src/input/state/core/selection_actions/geometry.rs +++ b/src/input/state/core/selection_actions/geometry.rs @@ -1,6 +1,5 @@ use super::super::base::InputState; -use crate::draw::ShapeId; -use crate::draw::TextMeasurer; +use crate::draw::{ShapeId, TextMeasurer}; use crate::util::Rect; fn selection_rect(start_x: i32, start_y: i32, end_x: i32, end_y: i32) -> Option { diff --git a/src/input/state/core/selection_actions/handles.rs b/src/input/state/core/selection_actions/handles.rs index 996a10b54..fe5904e29 100644 --- a/src/input/state/core/selection_actions/handles.rs +++ b/src/input/state/core/selection_actions/handles.rs @@ -11,8 +11,8 @@ //! those layers each kept their own list, they disagreed: the pointer showed a //! resize arrow over a grip that a click would bend. Both now ask this. -use crate::draw::ShapeId; use crate::draw::frame::ShapeSnapshot; +use crate::draw::{ShapeId, TextMeasurer}; use crate::input::InputState; use crate::input::state::core::base::SelectionHandle; @@ -39,7 +39,7 @@ impl InputState { /// loupe with the Spotlight tool still active. pub(crate) fn hit_idle_handle_with( &self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, x: i32, y: i32, ) -> Option { diff --git a/src/input/state/core/selection_actions/handles/tests.rs b/src/input/state/core/selection_actions/handles/tests.rs index 8514845da..fa077d7f1 100644 --- a/src/input/state/core/selection_actions/handles/tests.rs +++ b/src/input/state/core/selection_actions/handles/tests.rs @@ -1,3 +1,6 @@ +use crate::input::events::MouseButton; +use crate::ui_text::UiTextEngine; + use super::*; use crate::draw::{ArrowStyle, Color, Shape}; use crate::input::state::DrawingState; @@ -5,7 +8,7 @@ use crate::input::state::test_support::make_test_input_state; /// A curved arrow whose arc is shallow enough that its bend grip lands inside /// the selection box's top-edge handle. -fn add_shallow_curved_arrow(state: &mut crate::input::InputState) -> crate::draw::ShapeId { +fn add_shallow_curved_arrow(state: &mut InputState) -> ShapeId { state.boards.active_frame_mut().add_shape(Shape::Arrow { x1: 0, y1: 100, @@ -32,7 +35,7 @@ fn a_shallow_bend_grip_outranks_the_selection_edge_handle_it_overlaps() { // This is the collision the ordering exists for. At a shallow bend the grip // sits a couple of pixels off the chord, well inside the top edge handle's // tolerance, and whichever probe runs first wins the pixel. - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let id = add_shallow_curved_arrow(&mut state); state.set_selection(vec![id]); @@ -65,7 +68,7 @@ fn what_the_routing_reports_is_what_a_press_starts() { // keep matching the variants. This is what notices if one drifts, and it is // the bug the shared routing replaced: the pointer showed a resize arrow // over a grip that a click would bend. - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let id = add_shallow_curved_arrow(&mut state); state.set_selection(vec![id]); @@ -80,14 +83,14 @@ fn what_the_routing_reports_is_what_a_press_starts() { Some(IdleHandle::ArrowBend(id)) ); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let ui_engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, }; state.on_mouse_press_with_canvas_and_resources( resources, - crate::input::events::MouseButton::Left, + MouseButton::Left, center.0, center.1, center.0, @@ -102,7 +105,7 @@ fn what_the_routing_reports_is_what_a_press_starts() { #[test] fn empty_canvas_routes_to_no_handle() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let state = make_test_input_state(); assert_eq!(state.hit_idle_handle_with(&measurer, 50, 50), None); } diff --git a/src/input/state/core/selection_actions/measurement_tests.rs b/src/input/state/core/selection_actions/measurement_tests.rs index 0918df6c3..2698ac870 100644 --- a/src/input/state/core/selection_actions/measurement_tests.rs +++ b/src/input/state/core/selection_actions/measurement_tests.rs @@ -3,6 +3,7 @@ use crate::draw::{ }; use crate::input::InputState; use crate::input::state::core::base::SelectionHandle; +use crate::input::state::test_support; use crate::util::Rect; fn fixtures() -> Vec<(Shape, (i32, i32))> { @@ -70,7 +71,7 @@ fn fixtures() -> Vec<(Shape, (i32, i32))> { } fn state_with(shape: Shape) -> (InputState, ShapeId, ShapeId) { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.view.set_screen_dimensions(1000, 800); state.set_hit_test_threshold(1); state.set_hit_test_tolerance(1.0); diff --git a/src/input/state/core/selection_actions/measurement_tests/mutations.rs b/src/input/state/core/selection_actions/measurement_tests/mutations.rs index 7dcaa3405..a3be9ece8 100644 --- a/src/input/state/core/selection_actions/measurement_tests/mutations.rs +++ b/src/input/state/core/selection_actions/measurement_tests/mutations.rs @@ -1,3 +1,6 @@ +use crate::input::DrawingState; +use crate::input::state::SelectionGrab; + use super::*; use crate::draw::frame::UndoAction; use crate::input::state::core::properties::SelectionPropertyKind; @@ -167,8 +170,8 @@ fn explicit_cancellation_restores_decorated_move_bounds_and_hits() { state.hit_test_at_with(&owner, probe.0, probe.1 + 300), Some(id) ); - state.state = crate::input::DrawingState::MovingSelection { - grab: crate::input::state::SelectionGrab::new(id, probe.0, probe.1 + 300), + state.state = DrawingState::MovingSelection { + grab: SelectionGrab::new(id, probe.0, probe.1 + 300), last_x: probe.0, last_y: probe.1 + 300, snapshots, @@ -176,7 +179,7 @@ fn explicit_cancellation_restores_decorated_move_bounds_and_hits() { }; state.take_dirty_regions(); assert!(state.try_cancel_active_interaction_with(&owner)); - assert!(matches!(state.state, crate::input::DrawingState::Idle)); + assert!(matches!(state.state, DrawingState::Idle)); assert_eq!(bounds(&state, &owner, id), original); assert_eq!(bounds(&state, &owner, locked_id), locked); assert_eq!(state.hit_test_at_with(&owner, probe.0, probe.1), Some(id)); diff --git a/src/input/state/core/selection_actions/reorder.rs b/src/input/state/core/selection_actions/reorder.rs index 5e37ed772..aa1a69ca8 100644 --- a/src/input/state/core/selection_actions/reorder.rs +++ b/src/input/state/core/selection_actions/reorder.rs @@ -1,7 +1,6 @@ use super::super::base::InputState; -use crate::draw::ShapeId; -use crate::draw::TextMeasurer; use crate::draw::frame::UndoAction; +use crate::draw::{ShapeId, TextMeasurer}; use std::collections::HashSet; impl InputState { diff --git a/src/input/state/core/selection_actions/resize.rs b/src/input/state/core/selection_actions/resize.rs index 9953f6cb3..211e385d7 100644 --- a/src/input/state/core/selection_actions/resize.rs +++ b/src/input/state/core/selection_actions/resize.rs @@ -1,8 +1,7 @@ //! Selection resize functionality. -use crate::draw::ShapeId; -use crate::draw::TextMeasurer; use crate::draw::frame::ShapeSnapshot; +use crate::draw::{ShapeId, TextMeasurer}; use crate::input::InputState; use crate::input::state::core::base::SelectionHandle; use crate::util::Rect; diff --git a/src/input/state/core/selection_actions/resize/resize_helpers.rs b/src/input/state/core/selection_actions/resize/resize_helpers.rs index 3fd2c38b6..0fddcc457 100644 --- a/src/input/state/core/selection_actions/resize/resize_helpers.rs +++ b/src/input/state/core/selection_actions/resize/resize_helpers.rs @@ -1,7 +1,6 @@ use crate::util::Rect; -use super::super::super::base::InputState; -use super::super::super::base::SelectionHandle; +use super::super::super::base::{InputState, SelectionHandle}; #[derive(Debug, Clone, Copy)] pub(super) struct ResizeHandleProbe { diff --git a/src/input/state/core/selection_actions/spotlight.rs b/src/input/state/core/selection_actions/spotlight.rs index 435c80bca..fa1bcfa5c 100644 --- a/src/input/state/core/selection_actions/spotlight.rs +++ b/src/input/state/core/selection_actions/spotlight.rs @@ -1,6 +1,8 @@ -use crate::draw::TextMeasurer; -use crate::draw::{Shape, ShapeId}; +use crate::draw; +use crate::draw::{Shape, ShapeId, TextMeasurer}; use crate::input::InputState; +use crate::input::state::DrawingState; +use crate::ui::toolbar::model::ToolbarSliderSpec; use crate::util::Rect; /// Knob size in canvas pixels. @@ -48,10 +50,9 @@ impl SpotlightMagnificationTrack { let travelled = f64::from(x) - f64::from(self.track.x) - f64::from(MAGNIFICATION_HANDLE_SIZE / 2); let t = (travelled / span).clamp(0.0, 1.0); - let range = - crate::draw::MAX_SPOTLIGHT_MAGNIFICATION - crate::draw::MIN_SPOTLIGHT_MAGNIFICATION; - crate::ui::toolbar::model::ToolbarSliderSpec::SPOTLIGHT_MAGNIFICATION - .normalize_value(crate::draw::MIN_SPOTLIGHT_MAGNIFICATION + t * range) + let range = draw::MAX_SPOTLIGHT_MAGNIFICATION - draw::MIN_SPOTLIGHT_MAGNIFICATION; + ToolbarSliderSpec::SPOTLIGHT_MAGNIFICATION + .normalize_value(draw::MIN_SPOTLIGHT_MAGNIFICATION + t * range) } } @@ -124,10 +125,10 @@ pub(crate) fn spotlight_magnification_track( MAGNIFICATION_TRACK_LENGTH, MAGNIFICATION_HANDLE_SIZE, )?; - let range = crate::draw::MAX_SPOTLIGHT_MAGNIFICATION - crate::draw::MIN_SPOTLIGHT_MAGNIFICATION; + let range = draw::MAX_SPOTLIGHT_MAGNIFICATION - draw::MIN_SPOTLIGHT_MAGNIFICATION; let t = if range > f64::EPSILON { - ((crate::draw::normalize_spotlight_magnification(magnification) - - crate::draw::MIN_SPOTLIGHT_MAGNIFICATION) + ((draw::normalize_spotlight_magnification(magnification) + - draw::MIN_SPOTLIGHT_MAGNIFICATION) / range) .clamp(0.0, 1.0) } else { @@ -193,9 +194,7 @@ impl InputState { measurer: &TextMeasurer, x: i32, ) -> bool { - let crate::input::state::DrawingState::AdjustingSpotlightMagnification { shape_id, .. } = - self.state - else { + let DrawingState::AdjustingSpotlightMagnification { shape_id, .. } = self.state else { return false; }; let Some(control) = self.selected_spotlight_control_with(measurer) else { diff --git a/src/input/state/core/selection_actions/state.rs b/src/input/state/core/selection_actions/state.rs index a41559a6a..761634c18 100644 --- a/src/input/state/core/selection_actions/state.rs +++ b/src/input/state/core/selection_actions/state.rs @@ -1,7 +1,6 @@ use super::super::base::InputState; -use crate::draw::DirtyFullReason; -use crate::draw::TextMeasurer; use crate::draw::frame::{ShapeSnapshot, UndoAction}; +use crate::draw::{DirtyFullReason, TextMeasurer}; use crate::util::Rect; const SELECTION_DAMAGE_PADDING: i32 = 8; diff --git a/src/input/state/core/selection_actions/text/edit.rs b/src/input/state/core/selection_actions/text/edit.rs index 7df610503..c4c161383 100644 --- a/src/input/state/core/selection_actions/text/edit.rs +++ b/src/input/state/core/selection_actions/text/edit.rs @@ -1,4 +1,4 @@ -use crate::draw::TextMeasurer; +use crate::draw::{Shape, TextMeasurer}; use crate::input::{DrawingState, InputState}; use std::time::Instant; @@ -74,7 +74,7 @@ impl InputState { pub(crate) fn commit_text_edit_with( &mut self, measurer: &TextMeasurer, - new_shape: crate::draw::Shape, + new_shape: Shape, ) -> bool { let undo_limit = self.history_limits.undo_stack_limit(); let Some(change) = self.text_editing.commit_existing( diff --git a/src/input/state/core/selection_actions/text/handles.rs b/src/input/state/core/selection_actions/text/handles.rs index 968654e88..6c31a99cc 100644 --- a/src/input/state/core/selection_actions/text/handles.rs +++ b/src/input/state/core/selection_actions/text/handles.rs @@ -1,5 +1,4 @@ -use crate::draw::TextMeasurer; -use crate::draw::{Shape, ShapeId}; +use crate::draw::{Shape, ShapeId, TextMeasurer}; use crate::input::InputState; use crate::util::Rect; diff --git a/src/input/state/core/selection_actions/text/wrap.rs b/src/input/state/core/selection_actions/text/wrap.rs index 0dc68faeb..4e551b521 100644 --- a/src/input/state/core/selection_actions/text/wrap.rs +++ b/src/input/state/core/selection_actions/text/wrap.rs @@ -1,5 +1,4 @@ -use crate::draw::TextMeasurer; -use crate::draw::{Shape, ShapeId}; +use crate::draw::{Shape, ShapeId, TextMeasurer}; use crate::input::InputState; const TEXT_WRAP_MIN_WIDTH: i32 = 40; diff --git a/src/input/state/core/selection_actions/translation/mod.rs b/src/input/state/core/selection_actions/translation/mod.rs index efc652cd7..92ae6fb76 100644 --- a/src/input/state/core/selection_actions/translation/mod.rs +++ b/src/input/state/core/selection_actions/translation/mod.rs @@ -1,6 +1,5 @@ -use crate::draw::ShapeId; -use crate::draw::TextMeasurer; use crate::draw::frame::ShapeSnapshot; +use crate::draw::{ShapeId, TextMeasurer}; use crate::input::InputState; mod bounds; diff --git a/src/input/state/core/selection_actions/translation/restore.rs b/src/input/state/core/selection_actions/translation/restore.rs index ea94885e5..23501805b 100644 --- a/src/input/state/core/selection_actions/translation/restore.rs +++ b/src/input/state/core/selection_actions/translation/restore.rs @@ -1,6 +1,5 @@ -use crate::draw::ShapeId; -use crate::draw::TextMeasurer; use crate::draw::frame::ShapeSnapshot; +use crate::draw::{ShapeId, TextMeasurer}; use crate::input::InputState; impl InputState { diff --git a/src/input/state/core/selection_actions/translation/undo.rs b/src/input/state/core/selection_actions/translation/undo.rs index ebce8484d..55ee3950e 100644 --- a/src/input/state/core/selection_actions/translation/undo.rs +++ b/src/input/state/core/selection_actions/translation/undo.rs @@ -1,12 +1,12 @@ -use crate::draw::ShapeId; use crate::draw::frame::ShapeSnapshot; +use crate::draw::{ShapeId, TextMeasurer}; use crate::input::InputState; use crate::input::state::core::editing::CanvasEdit; impl InputState { pub(crate) fn push_translation_undo( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, before: Vec<(ShapeId, ShapeSnapshot)>, ) -> bool { let effects = CanvasEdit::from_snapshots(before).commit( diff --git a/src/input/state/core/session.rs b/src/input/state/core/session.rs index 48b528641..f2e15d989 100644 --- a/src/input/state/core/session.rs +++ b/src/input/state/core/session.rs @@ -1,3 +1,8 @@ +use crate::draw::{Color, TextMeasurer}; +use crate::input::MouseButton; +use crate::session; +use crate::session::{SessionOptions, SessionSnapshot}; + use super::base::{DrawingState, InputState}; use super::selection::PolygonClickState; use super::{ColorPickerPopupLayout, ColorPickerPopupState}; @@ -9,10 +14,7 @@ use std::path::{Path, PathBuf}; struct ActiveInteractionRollback { boards: BoardManager, state: DrawingState, - active_drag: ( - Option, - Option, - ), + active_drag: (Option, Option), style: super::DrawingStyle, text_editing: super::TextEditing, color_picker_popup_state: ColorPickerPopupState, @@ -166,7 +168,7 @@ impl InputState { pub(crate) fn with_active_interaction_canceled_for_capture_with( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, capture: impl FnOnce(&Self) -> T, ) -> T { if !self.has_cancelable_session_capture_interaction() { @@ -186,11 +188,11 @@ impl InputState { /// Snapshot boards for persistence without writing in-progress edits as empty text. pub(crate) fn snapshot_for_persistence_with( &mut self, - measurer: &crate::draw::TextMeasurer, - options: &crate::session::SessionOptions, - ) -> Option { + measurer: &TextMeasurer, + options: &SessionOptions, + ) -> Option { self.with_active_interaction_canceled_for_capture_with(measurer, |input| { - crate::session::snapshot_from_input(input, options) + session::snapshot_from_input(input, options) }) } } @@ -198,11 +200,14 @@ impl InputState { #[cfg(test)] mod tests { use crate::draw::frame::ShapeSnapshot; - use crate::draw::{BLACK, Shape}; + use crate::draw::{BLACK, Shape, TextMeasurer}; use crate::input::state::core::board_picker::{BoardPickerDrag, BoardPickerPageDrag}; use crate::input::state::core::text_editing::TextBlockDrag; use crate::input::state::test_support::make_test_input_state; + use crate::input::state::{PickerDrag, SelectionGrab}; use crate::input::{BOARD_ID_BLACKBOARD, DrawingState, MouseButton, SelectionHandle, Tool}; + use crate::session; + use crate::session::SessionSnapshot; use crate::util::Rect; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -244,7 +249,7 @@ mod tests { thick: 2.0, }, DrawingState::MovingSelection { - grab: crate::input::state::SelectionGrab::new(0, 10, 20), + grab: SelectionGrab::new(0, 10, 20), last_x: 10, last_y: 20, snapshots: Vec::new(), @@ -298,13 +303,13 @@ mod tests { let mut state = make_test_input_state(); state.open_color_picker_popup(); - state.color_picker_popup_set_dragging(Some(crate::input::state::PickerDrag::SatVal)); + state.color_picker_popup_set_dragging(Some(PickerDrag::SatVal)); assert!(state.has_active_pointer_interaction()); } #[test] fn session_capture_rollback_preserves_board_delete_confirmation_identity() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.switch_board(BOARD_ID_BLACKBOARD); assert_eq!(state.board_id(), BOARD_ID_BLACKBOARD); @@ -315,7 +320,7 @@ mod tests { state.begin_pointer_drag(MouseButton::Left, None); state.with_active_interaction_canceled_for_capture_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), |input| { assert!(!input.has_active_pointer_interaction()); }, @@ -342,7 +347,7 @@ mod tests { state.begin_pointer_drag(MouseButton::Left, None); state.with_active_interaction_canceled_for_capture_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), |input| { assert!(!input.has_active_pointer_interaction()); }, @@ -357,7 +362,7 @@ mod tests { #[test] fn persistence_snapshot_keeps_original_text_during_in_place_edit() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); use crate::session::SessionOptions; use std::path::PathBuf; @@ -383,11 +388,11 @@ mod tests { }; buffer.push_str(" unsaved edit"); - let live = crate::session::snapshot_from_input(&state, &options).expect("live snapshot"); + let live = session::snapshot_from_input(&state, &options).expect("live snapshot"); assert_eq!(first_snapshot_text(&live), ""); let persisted = state - .snapshot_for_persistence_with(&crate::draw::TextMeasurer::default(), &options) + .snapshot_for_persistence_with(&TextMeasurer::default(), &options) .expect("persistence snapshot"); assert_eq!(first_snapshot_text(&persisted), "Original"); @@ -407,7 +412,7 @@ mod tests { assert!(text.is_empty()); } - fn first_snapshot_text(snapshot: &crate::session::SessionSnapshot) -> &str { + fn first_snapshot_text(snapshot: &SessionSnapshot) -> &str { for board in &snapshot.boards { for page in &board.pages.pages { for drawn in &page.shapes { diff --git a/src/input/state/core/session_flags.rs b/src/input/state/core/session_flags.rs index 660f8398a..2b467ca71 100644 --- a/src/input/state/core/session_flags.rs +++ b/src/input/state/core/session_flags.rs @@ -1,5 +1,7 @@ //! Session persistence flags, preflight options, and path state. +use std::mem; + use crate::session::SessionOptions; use std::path::{Path, PathBuf}; @@ -48,7 +50,7 @@ impl SessionFlags { } pub(in crate::input::state) fn take_dirty(&mut self) -> bool { - std::mem::take(&mut self.dirty) + mem::take(&mut self.dirty) } pub(in crate::input::state) const fn preflight_options(&self) -> Option<&SessionOptions> { diff --git a/src/input/state/core/session_preflight.rs b/src/input/state/core/session_preflight.rs index 96e59387e..1363c48de 100644 --- a/src/input/state/core/session_preflight.rs +++ b/src/input/state/core/session_preflight.rs @@ -261,7 +261,7 @@ impl InputState { toast_key, action_label, action_name, - tr("session would exceed save limits.").to_string(), + tr("session would exceed save limits."), ); } None => { @@ -269,7 +269,7 @@ impl InputState { toast_key, action_label, action_name, - tr("session size check failed.").to_string(), + tr("session size check failed."), ); } } @@ -293,7 +293,7 @@ impl InputState { toast_key, action_label, action_name, - trf( + &trf( "session would exceed {}. Remove images or raise session.max_file_size_mb.", &[&limit], ), @@ -319,7 +319,7 @@ impl InputState { toast_key, action_label, action_name, - expanded_safety_limit_reason(&limit), + &expanded_safety_limit_reason(&limit), ); } true @@ -330,7 +330,7 @@ impl InputState { toast_key: &'static str, action_label: &str, action_name: &str, - reason: String, + reason: &str, ) -> bool { self.push_toast( ToastPriority::Info, @@ -439,7 +439,8 @@ fn estimate_frame_page_storage(page: &Frame) -> CloneStorageEstimate { if offset != (0, 0) { estimate.add_raw(32); } - for shape in page.shapes.iter().map(|drawn| &drawn.shape) { + let shapes = page.shapes.iter().map(|drawn| &drawn.shape); + for shape in shapes { estimate = estimate.saturating_add(estimate_shape_storage(shape)); } estimate diff --git a/src/input/state/core/session_preflight_exact.rs b/src/input/state/core/session_preflight_exact.rs index 9332db92b..03c65c448 100644 --- a/src/input/state/core/session_preflight_exact.rs +++ b/src/input/state/core/session_preflight_exact.rs @@ -1,3 +1,5 @@ +use crate::session::BoardAppearanceSnapshot; + use super::base::InputState; use super::session_preflight::board_should_persist_for_session; use crate::draw::{BoardPages, Frame}; @@ -111,9 +113,7 @@ fn duplicate_page_in_snapshot( let insert_at = (page_index + 1).min(pages.len()); pages.insert(insert_at, cloned_page); snapshot.boards.push(BoardSnapshot { - appearance: Some(crate::session::BoardAppearanceSnapshot::capture( - source_board, - )), + appearance: Some(BoardAppearanceSnapshot::capture(source_board)), id: source_board.spec.id.clone(), pages: BoardPagesSnapshot { active: insert_at, @@ -167,9 +167,7 @@ fn copy_page_between_boards_in_snapshot( let mut pages = pages_for_snapshot(&target_board.pages, history_limit); pages.push(cloned_page); snapshot.boards.push(BoardSnapshot { - appearance: Some(crate::session::BoardAppearanceSnapshot::capture( - target_board, - )), + appearance: Some(BoardAppearanceSnapshot::capture(target_board)), id: target_board.spec.id.clone(), pages: BoardPagesSnapshot { active: pages.len().saturating_sub(1), diff --git a/src/input/state/core/status_hud.rs b/src/input/state/core/status_hud.rs index 6ebe0335e..3e3fe7c2f 100644 --- a/src/input/state/core/status_hud.rs +++ b/src/input/state/core/status_hud.rs @@ -7,11 +7,18 @@ mod state; +use std::time::Instant; + use state::StatusHudRebuildInputs; pub use state::StatusHudState; use crate::config::{Action, StatusBarItem, StatusBarStyle, StatusPosition}; -use crate::ui::{StatusHudLayout, StatusHudSegmentKind}; +use crate::draw::TextMeasurer; +use crate::input::DrawingState; +use crate::input::state::InputTextResources; +use crate::ui; +use crate::ui::{StatusHudLayout, StatusHudSegmentKind, StatusHudTooltip}; +use crate::ui_text::UiTextEngine; use super::base::InputState; use super::board_picker::BoardPickerFocus; @@ -58,8 +65,8 @@ impl InputState { pub(crate) fn set_status_bar_item_visible_with_resources( &mut self, - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, item: StatusBarItem, visible: bool, ) -> bool { @@ -94,8 +101,8 @@ impl InputState { /// the render effect pass, which re-measures with that frame's inputs. pub(crate) fn refresh_status_hud_layout_with_resources( &mut self, - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, ) { let Some(inputs) = self.status_hud.rebuild_inputs() else { self.status_hud.layout = None; @@ -128,8 +135,8 @@ impl InputState { screen_width: u32, screen_height: u32, ) { - let engine = crate::ui_text::UiTextEngine::default(); - let measurer = crate::draw::TextMeasurer::default(); + let engine = UiTextEngine::default(); + let measurer = TextMeasurer::default(); self.update_status_hud_layout_for_pointer_with_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -145,7 +152,7 @@ impl InputState { pub(crate) fn update_status_hud_layout_for_pointer_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, position: StatusPosition, style: &StatusBarStyle, screen_width: u32, @@ -153,7 +160,7 @@ impl InputState { chrome_cursor_focused: bool, ) { let layout = if self.ui_visibility.show_status_bar { - crate::ui::compute_status_hud_layout_with_resources( + ui::compute_status_hud_layout_with_resources( resources.ui_engine, resources.measurer, self, @@ -212,15 +219,15 @@ impl InputState { /// once per frame after the HUD layout, so damage and paint agree. pub(crate) fn update_status_tooltip_with_engine( &mut self, - engine: &crate::ui_text::UiTextEngine, - now: std::time::Instant, + engine: &UiTextEngine, + now: Instant, ) -> Option<(f64, f64, f64, f64)> { let tooltip = self .status_hud .tooltip_segment(now) .zip(self.status_hud.layout.as_ref()) .and_then(|(kind, layout)| layout.tooltip_for(engine, kind)); - let bounds = tooltip.as_ref().map(crate::ui::StatusHudTooltip::bounds); + let bounds = tooltip.as_ref().map(StatusHudTooltip::bounds); self.status_hud.set_tooltip(tooltip); bounds @@ -228,7 +235,7 @@ impl InputState { /// Whether a hovered segment's tooltip just became due, so the next frame /// must paint it. - pub(crate) fn status_tooltip_due(&self, now: std::time::Instant) -> bool { + pub(crate) fn status_tooltip_due(&self, now: Instant) -> bool { self.status_hud.tooltip().is_none() && self.status_hud.tooltip_segment(now).is_some() } @@ -239,16 +246,15 @@ impl InputState { /// is requested on transitions only; the damage pass re-damages the /// pill footprint every rendered frame, so no region marking is needed. pub(crate) fn update_status_hud_hover_from_pointer(&mut self, x: i32, y: i32) { - let new_hover = if matches!(self.state, crate::input::DrawingState::Idle) - && self.status_hud_contains(x, y) - { - self.status_hud - .layout - .as_ref() - .and_then(|layout| layout.segment_at(x as f64, y as f64)) - } else { - None - }; + let new_hover = + if matches!(self.state, DrawingState::Idle) && self.status_hud_contains(x, y) { + self.status_hud + .layout + .as_ref() + .and_then(|layout| layout.segment_at(x as f64, y as f64)) + } else { + None + }; if self.status_hud.update_hover(new_hover) { self.needs_redraw = true; } @@ -325,7 +331,7 @@ impl InputState { /// `(hit, action)` mirroring toast release resolver. pub(crate) fn check_status_hud_click_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, x: i32, y: i32, ) -> (bool, Option) { diff --git a/src/input/state/core/status_hud/state.rs b/src/input/state/core/status_hud/state.rs index 152af96b8..c101619d0 100644 --- a/src/input/state/core/status_hud/state.rs +++ b/src/input/state/core/status_hud/state.rs @@ -1,3 +1,4 @@ +use std::mem; use std::time::{Duration, Instant}; use crate::config::{StatusBarStyle, StatusPosition}; @@ -115,7 +116,7 @@ impl StatusHudState { } pub(in crate::input::state) fn take_press_pending(&mut self) -> bool { - std::mem::take(&mut self.press_pending) + mem::take(&mut self.press_pending) } } diff --git a/src/input/state/core/style.rs b/src/input/state/core/style.rs index 28a48517d..3cb4eb95b 100644 --- a/src/input/state/core/style.rs +++ b/src/input/state/core/style.rs @@ -3,11 +3,14 @@ use crate::config::{ DrawingConfig, MouseDragToolsConfig, PresetToolStatesConfig, QuickColorPalette, SpotlightConfig, ToolPresetConfig, }; -use crate::draw::{ArrowStyle, BlurStyle, Color, EraserKind, FontDescriptor, clamp_regular_sides}; +use crate::draw::{ + ArrowStyle, BlurStyle, Color, EraserKind, FontDescriptor, clamp_regular_sides, shape, +}; use crate::input::state::{MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; use crate::input::{EraserMode, PerToolDrawingSettings, Tool}; use crate::session::ToolStateSnapshot; use crate::ui::toolbar::model::ToolbarSliderSpec; +use crate::{config, draw}; use super::{PressureThicknessEditMode, PressureThicknessEntryMode}; @@ -79,10 +82,10 @@ impl From<(&DrawingConfig, &ArrowConfig, &SpotlightConfig)> for DrawingStyle { eraser_kind: EraserKind::Circle, eraser_mode: drawing.default_eraser_mode, marker_opacity: drawing.marker_opacity, - pen_smoothing: crate::draw::shape::clamp_pen_smoothing(drawing.pen_smoothing), + pen_smoothing: shape::clamp_pen_smoothing(drawing.pen_smoothing), shape_recognition_sensitivity: drawing .shape_recognition_sensitivity - .min(crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY), + .min(config::MAX_SHAPE_RECOGNITION_SENSITIVITY), shape_recognition_grid_snap: drawing.shape_recognition_grid_snap, blur_style: drawing.default_blur_style, spotlight_dim_opacity: spotlight.dim_opacity, @@ -187,16 +190,16 @@ impl DrawingStyle { } pub(crate) fn nudge_pen_smoothing(&mut self, delta: i32) -> bool { - let next = crate::draw::shape::clamp_pen_smoothing( + let next = shape::clamp_pen_smoothing( i32::from(self.pen_smoothing) .saturating_add(delta) - .clamp(0, i32::from(crate::draw::shape::MAX_PEN_SMOOTHING)) as u8, + .clamp(0, i32::from(shape::MAX_PEN_SMOOTHING)) as u8, ); self.set_pen_smoothing(next) } pub(crate) fn set_pen_smoothing(&mut self, level: u8) -> bool { - let level = crate::draw::shape::clamp_pen_smoothing(level); + let level = shape::clamp_pen_smoothing(level); if level == self.pen_smoothing { return false; } @@ -207,15 +210,12 @@ impl DrawingStyle { pub(crate) fn nudge_shape_recognition_sensitivity(&mut self, delta: i32) -> bool { let next = i32::from(self.shape_recognition_sensitivity) .saturating_add(delta) - .clamp( - 0, - i32::from(crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY), - ); + .clamp(0, i32::from(config::MAX_SHAPE_RECOGNITION_SENSITIVITY)); self.set_shape_recognition_sensitivity(next as u8) } pub(crate) fn set_shape_recognition_sensitivity(&mut self, level: u8) -> bool { - let level = level.min(crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY); + let level = level.min(config::MAX_SHAPE_RECOGNITION_SENSITIVITY); if level == self.shape_recognition_sensitivity { return false; } @@ -224,9 +224,8 @@ impl DrawingStyle { } pub(crate) fn set_spotlight_magnification(&mut self, magnification: f64) -> bool { - let normalized = ToolbarSliderSpec::SPOTLIGHT_MAGNIFICATION.normalize_value( - crate::draw::normalize_spotlight_magnification(magnification), - ); + let normalized = ToolbarSliderSpec::SPOTLIGHT_MAGNIFICATION + .normalize_value(draw::normalize_spotlight_magnification(magnification)); if (normalized - self.spotlight_magnification).abs() < f64::EPSILON { return false; } @@ -439,8 +438,7 @@ impl DrawingStyle { let _ = self.set_marker_opacity(opacity); } if let Some(magnification) = snapshot.spotlight_magnification { - self.spotlight_magnification = - crate::draw::normalize_spotlight_magnification(magnification); + self.spotlight_magnification = draw::normalize_spotlight_magnification(magnification); } if let Some(fill_enabled) = snapshot.fill_enabled { let _ = self.set_fill_enabled(fill_enabled); @@ -540,7 +538,7 @@ mod tests { assert!(!style.set_marker_opacity(f64::INFINITY)); assert!(style.set_polygon_sides(u8::MAX)); - assert_eq!(style.polygon_sides, crate::draw::REGULAR_POLYGON_MAX_SIDES); + assert_eq!(style.polygon_sides, draw::REGULAR_POLYGON_MAX_SIDES); assert!(!style.set_polygon_sides(u8::MAX)); } diff --git a/src/input/state/core/text_editing.rs b/src/input/state/core/text_editing.rs index 949a6765e..f07c1c68e 100644 --- a/src/input/state/core/text_editing.rs +++ b/src/input/state/core/text_editing.rs @@ -1,3 +1,6 @@ +use std::mem; +use std::time::Instant; + use super::base::{DrawingState, TextInputMode}; use super::ime::ImeCompositionState; use crate::draw::frame::{Frame, ShapeSnapshot, UndoAction}; @@ -11,7 +14,7 @@ pub(crate) struct TextClickState { pub(crate) shape_id: ShapeId, pub(crate) x: i32, pub(crate) y: i32, - pub(crate) at: std::time::Instant, + pub(crate) at: Instant, } #[derive(Debug, Clone, Copy)] @@ -22,7 +25,7 @@ pub(crate) struct TextBlockDrag { #[derive(Debug, Clone)] pub(crate) struct TextEditEntryFeedback { - pub(crate) started: std::time::Instant, + pub(crate) started: Instant, } /// Text-editor mode, asynchronous identity, composition, and pointer state. @@ -118,7 +121,7 @@ impl TextEditing { self.text_edit_entry_feedback = feedback; } - pub(crate) fn expire_edit_entry_feedback(&mut self, now: std::time::Instant) -> bool { + pub(crate) fn expire_edit_entry_feedback(&mut self, now: Instant) -> bool { let Some(feedback) = &self.text_edit_entry_feedback else { return false; }; @@ -131,7 +134,7 @@ impl TextEditing { false } - pub(crate) fn edit_entry_progress(&self, now: std::time::Instant) -> Option { + pub(crate) fn edit_entry_progress(&self, now: Instant) -> Option { let feedback = self.text_edit_entry_feedback.as_ref()?; let elapsed = now.saturating_duration_since(feedback.started).as_millis() as f64; let total = TEXT_EDIT_ENTRY_DURATION_MS as f64; @@ -193,7 +196,7 @@ impl TextEditing { shape_id: ShapeId, x: i32, y: i32, - now: std::time::Instant, + now: Instant, max_delay_ms: u64, max_distance: i32, ) -> bool { @@ -217,7 +220,7 @@ impl TextEditing { } pub(crate) fn replace_preview_bounds(&mut self, bounds: Option) -> Option { - std::mem::replace(&mut self.last_text_preview_bounds, bounds) + mem::replace(&mut self.last_text_preview_bounds, bounds) } pub(crate) fn mark_cursor_rect_dirty(&mut self) { @@ -235,11 +238,11 @@ impl TextEditing { } pub(crate) fn take_cursor_rect_dirty(&mut self) -> bool { - std::mem::take(&mut self.text_input_cursor_rect_dirty) + mem::take(&mut self.text_input_cursor_rect_dirty) } pub(crate) fn take_external_change_dirty(&mut self) -> bool { - std::mem::take(&mut self.text_input_external_change_dirty) + mem::take(&mut self.text_input_external_change_dirty) } pub(crate) fn reset_composition_and_pointer(&mut self) { @@ -254,7 +257,7 @@ impl TextEditing { measurer: &TextMeasurer, frame: &mut Frame, shape_id: ShapeId, - now: std::time::Instant, + now: Instant, ) -> Option { let drawn = frame.shape(shape_id)?; if drawn.locked { @@ -390,6 +393,8 @@ impl TextEditing { #[cfg(test)] mod tests { + use crate::draw; + use super::*; #[test] @@ -416,7 +421,7 @@ mod tests { x: 20, y: 30, text: text.to_string(), - color: crate::draw::RED, + color: draw::RED, size: 24.0, font_descriptor: FontDescriptor::default(), background_enabled: false, @@ -432,7 +437,7 @@ mod tests { let mut editing = TextEditing::default(); let started = editing - .begin_existing(&measurer, &mut frame, shape_id, std::time::Instant::now()) + .begin_existing(&measurer, &mut frame, shape_id, Instant::now()) .expect("editable text shape"); assert_eq!(started.text, "before"); @@ -460,7 +465,7 @@ mod tests { let measurer = TextMeasurer::default(); let mut editing = TextEditing::default(); editing - .begin_existing(&measurer, &mut frame, shape_id, std::time::Instant::now()) + .begin_existing(&measurer, &mut frame, shape_id, Instant::now()) .expect("editable text shape"); editing diff --git a/src/input/state/core/text_font.rs b/src/input/state/core/text_font.rs index cd7034a18..fc8ef6589 100644 --- a/src/input/state/core/text_font.rs +++ b/src/input/state/core/text_font.rs @@ -7,8 +7,7 @@ //! applied change reports. use super::InputState; -use crate::draw::TextMeasurer; -use crate::draw::{DrawnShape, FontDescriptor, Shape, families_match}; +use crate::draw::{DrawnShape, FontDescriptor, Shape, TextMeasurer, families_match}; use crate::i18n::tr; fn text_font_descriptor(shape: &Shape) -> Option<&FontDescriptor> { @@ -92,7 +91,7 @@ impl InputState { if self.selection_has_text() { return self.apply_weight_to_selected_text(measurer, weight); } - let descriptor = crate::draw::FontDescriptor::new( + let descriptor = FontDescriptor::new( self.style.font_descriptor.family.clone(), weight.to_string(), self.style.font_descriptor.style.clone(), @@ -146,6 +145,6 @@ impl InputState { |shape| text_font_descriptor(shape).is_some(), move |shape| text_font_descriptor_mut(shape).is_some_and(&mut apply), ); - self.report_selection_apply_result(result, property) + self.report_selection_apply_result(&result, property) } } diff --git a/src/input/state/core/tool_controls/precision_entry.rs b/src/input/state/core/tool_controls/precision_entry.rs index 7334ed609..c2e6dc59a 100644 --- a/src/input/state/core/tool_controls/precision_entry.rs +++ b/src/input/state/core/tool_controls/precision_entry.rs @@ -6,6 +6,10 @@ //! toolbar apply arm clamps to the slider range), Esc cancels. Any other //! toolbar interaction dismisses it (see the backend dismissal rules). +use crate::input::state::InputTextResources; +use crate::input::state::core::modal::ModalSurface; +use crate::ui::toolbar::ToolbarEvent; + use super::super::base::InputState; use crate::input::events::Key; use crate::ui::toolbar::PrecisionEntryTarget; @@ -33,7 +37,7 @@ impl InputState { /// Open the precise-entry popup prefilled with the target's current /// value (selected, so the first keystroke replaces it). pub fn open_precision_entry(&mut self, target: PrecisionEntryTarget) { - self.close_modals_for_open(crate::input::state::core::modal::ModalSurface::PrecisionEntry); + self.close_modals_for_open(ModalSurface::PrecisionEntry); let value = match target { PrecisionEntryTarget::Thickness => { if self.active_tool().uses_eraser_size() @@ -111,7 +115,7 @@ impl InputState { /// `handle_color_picker_popup_key`): every key is consumed. pub(in crate::input::state) fn handle_precision_entry_key_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, ) -> bool { if !self.is_precision_entry_open() { @@ -123,8 +127,7 @@ impl InputState { } Key::Return => { if let Some((target, value)) = self.take_precision_entry_commit() { - let event = - crate::ui::toolbar::ToolbarEvent::CommitPrecisionEntry { target, value }; + let event = ToolbarEvent::CommitPrecisionEntry { target, value }; let _ = self.apply_toolbar_event_with_resources(resources, event); } } @@ -138,14 +141,19 @@ impl InputState { #[cfg(test)] mod tests { + use crate::draw::TextMeasurer; + use crate::input::MouseButton; + use crate::ui::toolbar::model::ToolbarSliderSpec; + use crate::ui_text::UiTextEngine; + use super::*; use crate::input::state::test_support::make_test_input_state; use crate::ui::toolbar::ToolbarEvent; #[test] fn open_prefills_the_selected_current_value_and_typing_replaces_it() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -178,8 +186,8 @@ mod tests { #[test] fn enter_commits_the_clamped_value_and_esc_cancels() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -195,7 +203,7 @@ mod tests { // Clamped to the shared thickness slider range. assert_eq!( state.style.current_thickness, - crate::ui::toolbar::model::ToolbarSliderSpec::THICKNESS.max + ToolbarSliderSpec::THICKNESS.max ); // Esc restores nothing and applies nothing. @@ -212,8 +220,8 @@ mod tests { #[test] fn font_size_target_commits_through_the_font_apply_arm() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -233,7 +241,7 @@ mod tests { assert!(!state.is_precision_entry_open()); assert_eq!( state.style.current_font_size, - crate::ui::toolbar::model::ToolbarSliderSpec::FONT_SIZE.max + ToolbarSliderSpec::FONT_SIZE.max ); // An unparseable buffer commits nothing. @@ -246,7 +254,7 @@ mod tests { fn overlay_pointer_press_cancels_the_popup() { let mut state = make_test_input_state(); state.open_precision_entry(PrecisionEntryTarget::Thickness); - state.on_mouse_press(crate::input::MouseButton::Left, 100, 100); + state.on_mouse_press(MouseButton::Left, 100, 100); assert!(!state.is_precision_entry_open()); } diff --git a/src/input/state/core/tool_controls/presets.rs b/src/input/state/core/tool_controls/presets.rs index 3c26e31da..a4d867394 100644 --- a/src/input/state/core/tool_controls/presets.rs +++ b/src/input/state/core/tool_controls/presets.rs @@ -1,3 +1,5 @@ +use crate::input::DragToolBindings; + use super::super::base::{ DrawingState, InputState, PRESET_FEEDBACK_DURATION_MS, PRESET_TOAST_DURATION_MS, PresetFeedbackKind, @@ -5,7 +7,8 @@ use super::super::base::{ use super::super::default_step_marker_size; use crate::config::{PresetSlotsConfig, PresetToolStatesConfig, ToolPresetConfig}; use crate::draw::TextMeasurer; -use crate::input::{DragModifier, tool::Tool}; +use crate::input::DragModifier; +use crate::input::tool::Tool; use std::time::{Duration, Instant}; impl InputState { @@ -95,8 +98,7 @@ impl InputState { let drag_tools = drag_tools .clone() .resolve_with_left_defaults(&left_defaults); - let _ = self - .set_drag_tool_bindings(crate::input::DragToolBindings::from_config(&drag_tools)); + let _ = self.set_drag_tool_bindings(DragToolBindings::from_config(&drag_tools)); } self.preset_slots.activate(slot); diff --git a/src/input/state/core/tool_controls/settings.rs b/src/input/state/core/tool_controls/settings.rs index 033b0d687..13e2878bd 100644 --- a/src/input/state/core/tool_controls/settings.rs +++ b/src/input/state/core/tool_controls/settings.rs @@ -1,13 +1,13 @@ +use crate::config::PresenterToolBehavior; +use crate::domain; + use super::super::base::{DrawingState, InputState, MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; -use crate::draw::TextMeasurer; -use crate::draw::{ArrowStyle, BlurStyle, Color, FontDescriptor}; +use crate::draw::{ArrowStyle, BlurStyle, Color, FontDescriptor, TextMeasurer}; use crate::i18n::tr; +use crate::input::modifiers::DragToolBindings; use crate::input::state::{Toast, ToastPriority}; -use crate::input::{ - DragBinding, MouseButton, - modifiers::DragToolBindings, - tool::{EraserMode, Tool}, -}; +use crate::input::tool::{EraserMode, Tool}; +use crate::input::{DragBinding, MouseButton}; impl InputState { /// Returns the stored drawing color for a tool. @@ -153,7 +153,7 @@ impl InputState { if self.presenter_mode_active() && matches!( self.presenter_mode_config().tool_behavior, - crate::config::PresenterToolBehavior::ForceHighlightLocked + PresenterToolBehavior::ForceHighlightLocked ) && !matches!(tool, Some(Tool::Highlight | Tool::Laser)) { @@ -358,7 +358,7 @@ impl InputState { let changed = if tool.uses_eraser_size() { self.set_eraser_size_with(measurer, self.style.eraser_size + delta) } else { - let next = crate::domain::step_stroke_thickness(self.thickness_for_tool(tool), delta); + let next = domain::step_stroke_thickness(self.thickness_for_tool(tool), delta); self.set_thickness_with(measurer, next) }; if changed { @@ -402,7 +402,10 @@ impl InputState { self.mark_current_provisional_dirty_full_with(measurer); let changed = self.style.set_thickness(tool, clamped); - debug_assert!(changed); + debug_assert!( + changed, + "thickness {clamped} differs from {current}, so the style must change" + ); self.mark_current_provisional_dirty_full_with(measurer); self.finish_preset_style_change(); true @@ -421,7 +424,10 @@ impl InputState { } self.mark_current_provisional_dirty_full_with(measurer); let changed = self.style.set_eraser_size(clamped); - debug_assert!(changed); + debug_assert!( + changed, + "eraser size {clamped} differs from the current size, so the style must change" + ); self.mark_current_provisional_dirty_full_with(measurer); self.finish_preset_style_change(); true diff --git a/src/input/state/core/tool_controls/toolbar.rs b/src/input/state/core/tool_controls/toolbar.rs index 527b0ea66..3ddab6992 100644 --- a/src/input/state/core/tool_controls/toolbar.rs +++ b/src/input/state/core/tool_controls/toolbar.rs @@ -1,3 +1,16 @@ +#[cfg(test)] +use crate::config::ToolbarItemsConfig; +#[cfg(test)] +use crate::config::ToolbarModeOverrides; +use crate::config::{ + ResolvedToolbarItems, ToolbarConfig, ToolbarLayoutMode, ToolbarSectionVisibility, + ToolbarStrokeControls, +}; +use crate::draw::TextMeasurer; +use crate::input::state::InputTextResources; +use crate::ui::toolbar::ToolbarItemCustomizeGroup; +use crate::ui_text::UiTextEngine; + use super::super::base::InputState; use crate::config::{ RadialMenuMouseBinding, ToolbarItemId, ToolbarItemOrderGroup, ToolbarItemVisibilitySetting, @@ -13,15 +26,15 @@ pub(crate) const CLEAR_UNDO_TOAST_MS: u64 = 2000; impl InputState { /// Sets toolbar visibility without changing its persisted pin. pub fn set_toolbar_visible(&mut self, visible: bool) -> bool { - let engine = crate::ui_text::UiTextEngine::default(); - let measurer = crate::draw::TextMeasurer::default(); + let engine = UiTextEngine::default(); + let measurer = TextMeasurer::default(); self.set_toolbar_visible_with_resources(&engine, &measurer, visible) } pub(crate) fn set_toolbar_visible_with_resources( &mut self, - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, visible: bool, ) -> bool { if !self.toolbar.set_visible(visible) { @@ -36,8 +49,8 @@ impl InputState { /// toolbar hidden by a transient chrome owner. pub(crate) fn derive_toolbar_visibility_from_pins_with_resources( &mut self, - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, ) { let visible = self.toolbar.top_pinned(); if self.modes.retarget_visibility_from_pin(visible) { @@ -118,21 +131,21 @@ impl InputState { self.toolbar.scale() } - pub(crate) fn toolbar_layout_mode(&self) -> crate::config::ToolbarLayoutMode { + pub(crate) fn toolbar_layout_mode(&self) -> ToolbarLayoutMode { self.toolbar.layout_mode() } #[cfg(test)] - pub(crate) fn toolbar_mode_overrides(&self) -> &crate::config::ToolbarModeOverrides { + pub(crate) fn toolbar_mode_overrides(&self) -> &ToolbarModeOverrides { self.toolbar.mode_overrides() } #[cfg(test)] - pub(crate) fn toolbar_items(&self) -> &crate::config::ToolbarItemsConfig { + pub(crate) fn toolbar_items(&self) -> &ToolbarItemsConfig { self.toolbar.items() } - pub(crate) fn resolved_toolbar_items(&self) -> &crate::config::ResolvedToolbarItems { + pub(crate) fn resolved_toolbar_items(&self) -> &ResolvedToolbarItems { self.toolbar.resolved_items() } @@ -140,9 +153,7 @@ impl InputState { self.toolbar.customize_items_open() } - pub(crate) fn toolbar_customize_items_group( - &self, - ) -> Option { + pub(crate) fn toolbar_customize_items_group(&self) -> Option { self.toolbar.customize_items_group() } @@ -171,7 +182,7 @@ impl InputState { } /// How the style pill shows pen smoothing and Shape Pen sensitivity. - pub(crate) fn toolbar_stroke_controls(&self) -> crate::config::ToolbarStrokeControls { + pub(crate) fn toolbar_stroke_controls(&self) -> ToolbarStrokeControls { self.toolbar.stroke_controls() } @@ -198,8 +209,8 @@ impl InputState { self.toolbar.set_use_icons(use_icons); } - pub fn init_toolbar_from_config(&mut self, config: &crate::config::ToolbarConfig) { - let legacy = crate::config::ToolbarSectionVisibility { + pub fn init_toolbar_from_config(&mut self, config: &ToolbarConfig) { + let legacy = ToolbarSectionVisibility { show_actions_section: self.ui_visibility.show_actions_section, show_actions_advanced: self.ui_visibility.show_actions_advanced, show_zoom_actions: self.ui_visibility.show_zoom_actions, @@ -239,8 +250,8 @@ impl InputState { pub(crate) fn set_top_display_mode_with_resources( &mut self, - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, mode: TopDisplayMode, ) { self.toolbar.set_top_display_mode(mode); @@ -249,15 +260,15 @@ impl InputState { } pub fn cycle_top_toolbar_display(&mut self) -> TopDisplayMode { - let engine = crate::ui_text::UiTextEngine::default(); - let measurer = crate::draw::TextMeasurer::default(); + let engine = UiTextEngine::default(); + let measurer = TextMeasurer::default(); self.cycle_top_toolbar_display_with_resources(&engine, &measurer) } pub(crate) fn cycle_top_toolbar_display_with_resources( &mut self, - engine: &crate::ui_text::UiTextEngine, - measurer: &crate::draw::TextMeasurer, + engine: &UiTextEngine, + measurer: &TextMeasurer, ) -> TopDisplayMode { let current = self.top_display_state(); let next = self.toolbar.cycle_top_display_mode(current); @@ -396,7 +407,7 @@ impl InputState { } #[cfg(test)] - pub(crate) fn test_set_toolbar_items(&mut self, items: crate::config::ToolbarItemsConfig) { + pub(crate) fn test_set_toolbar_items(&mut self, items: ToolbarItemsConfig) { self.toolbar.override_items_for_test(items); self.refresh_section_visibility(); } @@ -404,8 +415,8 @@ impl InputState { #[cfg(all(test, feature = "toolbar-gtk"))] pub(crate) fn test_set_toolbar_layout( &mut self, - mode: crate::config::ToolbarLayoutMode, - overrides: crate::config::ToolbarModeOverrides, + mode: ToolbarLayoutMode, + overrides: ToolbarModeOverrides, ) { self.toolbar.override_layout_for_test(mode, overrides); self.refresh_section_visibility(); @@ -415,7 +426,7 @@ impl InputState { pub(crate) fn test_set_toolbar_customization( &mut self, items_open: bool, - group: Option, + group: Option, status_bar_contents_open: bool, ) { self.toolbar @@ -423,61 +434,49 @@ impl InputState { } #[cfg(test)] - pub(crate) fn test_set_toolbar_stroke_controls( - &mut self, - style: crate::config::ToolbarStrokeControls, - ) { + pub(crate) fn test_set_toolbar_stroke_controls(&mut self, style: ToolbarStrokeControls) { self.toolbar.override_stroke_controls_for_test(style); } /// Wrapper for undo that preserves existing action plumbing. pub fn toolbar_undo(&mut self) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.toolbar_undo_with_resources(crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, }); } - pub(crate) fn toolbar_undo_with_resources( - &mut self, - resources: crate::input::state::InputTextResources<'_>, - ) { + pub(crate) fn toolbar_undo_with_resources(&mut self, resources: InputTextResources<'_>) { self.handle_action_with_resources(resources, Action::Undo); } /// Wrapper for redo that preserves existing action plumbing. pub fn toolbar_redo(&mut self) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.toolbar_redo_with_resources(crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, }); } - pub(crate) fn toolbar_redo_with_resources( - &mut self, - resources: crate::input::state::InputTextResources<'_>, - ) { + pub(crate) fn toolbar_redo_with_resources(&mut self, resources: InputTextResources<'_>) { self.handle_action_with_resources(resources, Action::Redo); } /// Wrapper for clear that preserves existing action plumbing. pub fn toolbar_clear(&mut self) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.toolbar_clear_with_resources(crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, }); } - pub(crate) fn toolbar_clear_with_resources( - &mut self, - resources: crate::input::state::InputTextResources<'_>, - ) { + pub(crate) fn toolbar_clear_with_resources(&mut self, resources: InputTextResources<'_>) { self.handle_action_with_resources(resources, Action::ClearCanvas); } @@ -485,8 +484,8 @@ impl InputState { /// were removed without a locked-shape warning, offers a short toast with /// an "Undo?" chip. The keyboard action and Shift+click stay instant. pub fn toolbar_clear_with_undo_toast(&mut self) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.toolbar_clear_with_undo_toast_with_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -497,7 +496,7 @@ impl InputState { pub(crate) fn toolbar_clear_with_undo_toast_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, ) { let (has_locked, has_unlocked) = { let frame = self.boards.active_frame(); @@ -522,8 +521,8 @@ impl InputState { /// Wrapper for entering text mode. pub fn toolbar_enter_text_mode(&mut self) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.toolbar_enter_text_mode_with_resources(crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, @@ -532,15 +531,15 @@ impl InputState { pub(crate) fn toolbar_enter_text_mode_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, ) { self.handle_action_with_resources(resources, Action::EnterTextMode); } /// Wrapper for entering sticky note mode. pub fn toolbar_enter_sticky_note_mode(&mut self) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.toolbar_enter_sticky_note_mode_with_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -551,7 +550,7 @@ impl InputState { pub(crate) fn toolbar_enter_sticky_note_mode_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, ) { self.handle_action_with_resources(resources, Action::EnterStickyNoteMode); } @@ -559,19 +558,24 @@ impl InputState { #[cfg(test)] mod tests { - use crate::config::{ToolbarItemsConfig, toolbar_item_ids as ids}; + use crate::config::{ + ToolbarConfig, ToolbarItemsConfig, ToolbarLayoutMode, ToolbarSectionFlag, UiConfig, + toolbar_item_ids as ids, + }; + use crate::input::state::UiVisibility; use crate::input::state::test_support::make_test_input_state; + use crate::ui::toolbar::ToolbarEvent; #[test] fn init_folds_legacy_section_booleans_into_explicit_overrides() { let mut state = make_test_input_state(); // A legacy Regular config where zoom actions were turned off and // everything else matches the baseline. - let config = crate::config::ToolbarConfig { + let config = ToolbarConfig { show_zoom_actions: false, ..Default::default() }; - state.ui_visibility = crate::input::state::UiVisibility::from(&crate::config::UiConfig { + state.ui_visibility = UiVisibility::from(&UiConfig { toolbar: config.clone(), ..Default::default() }); @@ -582,10 +586,10 @@ mod tests { assert!(state.ui_visibility.show_presets); // ...and the disagreement is now an explicit override that // survives mode switches. - let zoom_id = crate::config::ToolbarSectionFlag::ZoomActions.item_id(); + let zoom_id = ToolbarSectionFlag::ZoomActions.item_id(); assert!(state.resolved_toolbar_items().hidden.contains(&zoom_id)); - state.apply_toolbar_event(crate::ui::toolbar::ToolbarEvent::SetToolbarLayoutMode( - crate::config::ToolbarLayoutMode::Advanced, + state.apply_toolbar_event(ToolbarEvent::SetToolbarLayoutMode( + ToolbarLayoutMode::Advanced, )); assert!(!state.ui_visibility.show_zoom_actions); } @@ -593,7 +597,7 @@ mod tests { #[test] fn factory_visibility_reset_changes_only_the_centralized_eligible_set() { let mut state = make_test_input_state(); - let section = crate::config::ToolbarSectionFlag::Actions.item_id(); + let section = ToolbarSectionFlag::Actions.item_id(); let mut items = ToolbarItemsConfig::default(); items.set_hidden(ids::TOP_UTILITY_SCREENSHOT, true); items.set_hidden(ids::TOP_UTILITY_OCR, false); diff --git a/src/input/state/core/toolbar/apply/actions.rs b/src/input/state/core/toolbar/apply/actions.rs index 53d3b50cd..f106a5c6f 100644 --- a/src/input/state/core/toolbar/apply/actions.rs +++ b/src/input/state/core/toolbar/apply/actions.rs @@ -1,10 +1,12 @@ use crate::config::Action; +use crate::draw::TextMeasurer; +use crate::input::state::InputTextResources; use crate::input::{InputState, ZoomAction, ZoomAnchor}; impl InputState { pub(super) fn apply_toolbar_undo_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, ) -> bool { self.toolbar_undo_with_resources(resources); true @@ -12,24 +14,18 @@ impl InputState { pub(super) fn apply_toolbar_redo_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, ) -> bool { self.toolbar_redo_with_resources(resources); true } - pub(super) fn apply_toolbar_undo_all_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - ) -> bool { + pub(super) fn apply_toolbar_undo_all_with_measurer(&mut self, measurer: &TextMeasurer) -> bool { self.undo_all_immediate_with_measurer(measurer); true } - pub(super) fn apply_toolbar_redo_all_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - ) -> bool { + pub(super) fn apply_toolbar_redo_all_with_measurer(&mut self, measurer: &TextMeasurer) -> bool { self.redo_all_immediate_with_measurer(measurer); true } @@ -62,7 +58,7 @@ impl InputState { pub(super) fn apply_toolbar_clear_canvas_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, instant: bool, ) -> bool { if instant { @@ -75,7 +71,7 @@ impl InputState { pub(super) fn apply_toolbar_capture_screenshot_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, ) -> bool { // The interactive region capture: select, then choose Copy, Save, // Both, or Board. @@ -86,7 +82,7 @@ impl InputState { pub(super) fn apply_toolbar_copy_text_from_screen_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, ) -> bool { self.handle_action_with_resources(resources, Action::CopyTextFromScreen); self.close_top_toolbar_menus(); @@ -95,7 +91,7 @@ impl InputState { pub(super) fn apply_toolbar_open_command_palette_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, ) -> bool { self.handle_action_with_resources(resources, Action::ToggleCommandPalette); self.close_top_toolbar_menus(); diff --git a/src/input/state/core/toolbar/apply/delays.rs b/src/input/state/core/toolbar/apply/delays.rs index 54eed1c2b..b25495534 100644 --- a/src/input/state/core/toolbar/apply/delays.rs +++ b/src/input/state/core/toolbar/apply/delays.rs @@ -32,10 +32,12 @@ impl InputState { #[cfg(test)] mod tests { + use crate::input::state::test_support; + use super::*; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } #[test] diff --git a/src/input/state/core/toolbar/apply/layout.rs b/src/input/state/core/toolbar/apply/layout.rs index 67deef589..fd480221c 100644 --- a/src/input/state/core/toolbar/apply/layout.rs +++ b/src/input/state/core/toolbar/apply/layout.rs @@ -1,6 +1,11 @@ -use crate::config::{ToolbarItemId, ToolbarItemOrderGroup, ToolbarLayoutMode}; +use std::time::Instant; + +use crate::config::{ + StatusBarItem, ToolbarItemId, ToolbarItemOrderGroup, ToolbarItemVisibilitySetting, + ToolbarLayoutMode, ToolbarSectionFlag, TopDisplayMode, +}; use crate::input::InputState; -use crate::input::state::TopMenuState; +use crate::input::state::{InputTextResources, TopMenuState}; use crate::ui::toolbar::ToolbarItemCustomizeGroup; impl InputState { @@ -62,7 +67,7 @@ impl InputState { /// Whether a toolbar click at `now` is the tail of the double-click that /// just restored the strip from its minimized tab or micro chip. The /// restored controls sit under the pointer, so such a click is dropped. - pub(crate) fn toolbar_restore_guard_blocks(&self, now: std::time::Instant) -> bool { + pub(crate) fn toolbar_restore_guard_blocks(&self, now: Instant) -> bool { self.toolbar.restore_guard_blocks(now) } @@ -79,8 +84,8 @@ impl InputState { /// Set the top strip's display form (micro chip click → `Full`). pub(super) fn apply_toolbar_set_top_display_mode_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, - mode: crate::config::TopDisplayMode, + resources: InputTextResources<'_>, + mode: TopDisplayMode, ) -> bool { // Same presenter gate as Action::CycleToolbarDisplay: while presenter // mode owns toolbar visibility (e.g. the micro chip mapping), a chip @@ -123,35 +128,35 @@ impl InputState { } pub(super) fn apply_toolbar_toggle_actions_section(&mut self, show: bool) -> bool { - self.apply_section_flag(crate::config::ToolbarSectionFlag::Actions, show) + self.apply_section_flag(ToolbarSectionFlag::Actions, show) } pub(super) fn apply_toolbar_toggle_actions_advanced(&mut self, show: bool) -> bool { - self.apply_section_flag(crate::config::ToolbarSectionFlag::ActionsAdvanced, show) + self.apply_section_flag(ToolbarSectionFlag::ActionsAdvanced, show) } pub(super) fn apply_toolbar_toggle_zoom_actions(&mut self, show: bool) -> bool { - self.apply_section_flag(crate::config::ToolbarSectionFlag::ZoomActions, show) + self.apply_section_flag(ToolbarSectionFlag::ZoomActions, show) } pub(super) fn apply_toolbar_toggle_pages_section(&mut self, show: bool) -> bool { - self.apply_section_flag(crate::config::ToolbarSectionFlag::Pages, show) + self.apply_section_flag(ToolbarSectionFlag::Pages, show) } pub(super) fn apply_toolbar_toggle_boards_section(&mut self, show: bool) -> bool { - self.apply_section_flag(crate::config::ToolbarSectionFlag::Boards, show) + self.apply_section_flag(ToolbarSectionFlag::Boards, show) } pub(super) fn apply_toolbar_toggle_presets(&mut self, show: bool) -> bool { - self.apply_section_flag(crate::config::ToolbarSectionFlag::Presets, show) + self.apply_section_flag(ToolbarSectionFlag::Presets, show) } pub(super) fn apply_toolbar_toggle_step_section(&mut self, show: bool) -> bool { - self.apply_section_flag(crate::config::ToolbarSectionFlag::StepSection, show) + self.apply_section_flag(ToolbarSectionFlag::StepSection, show) } pub(super) fn apply_toolbar_toggle_text_controls(&mut self, show: bool) -> bool { - self.apply_section_flag(crate::config::ToolbarSectionFlag::TextControls, show) + self.apply_section_flag(ToolbarSectionFlag::TextControls, show) } /// Applies a section's visibility restored from runtime-UI state. @@ -161,8 +166,8 @@ impl InputState { /// than pinning it to whatever that mode happened to show. pub(crate) fn apply_section_visibility_runtime( &mut self, - flag: crate::config::ToolbarSectionFlag, - setting: crate::config::ToolbarItemVisibilitySetting, + flag: ToolbarSectionFlag, + setting: ToolbarItemVisibilitySetting, ) { self.set_toolbar_item_visibility_setting(flag.item_id(), setting); self.refresh_section_visibility(); @@ -171,7 +176,7 @@ impl InputState { /// Section toggles record an explicit override in the item store (the /// single source of truth) and re-derive the mirror booleans, so the /// choice survives layout-mode switches. - fn apply_section_flag(&mut self, flag: crate::config::ToolbarSectionFlag, show: bool) -> bool { + fn apply_section_flag(&mut self, flag: ToolbarSectionFlag, show: bool) -> bool { if !self.toolbar.set_section_visibility(flag, show) { return false; } @@ -222,7 +227,7 @@ impl InputState { pub(super) fn apply_toolbar_toggle_status_bar_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, show: bool, ) -> bool { if self.presenter_mode_active() && self.presenter_mode_config().hide_status_bar { @@ -253,8 +258,8 @@ impl InputState { pub(super) fn apply_toolbar_set_status_bar_item_visible_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, - item: crate::config::StatusBarItem, + resources: InputTextResources<'_>, + item: StatusBarItem, visible: bool, ) -> bool { self.set_status_bar_item_visible_with_resources( @@ -267,26 +272,26 @@ impl InputState { pub(super) fn apply_toolbar_toggle_status_board_badge_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, show: bool, ) -> bool { self.set_status_bar_item_visible_with_resources( resources.ui_engine, resources.measurer, - crate::config::StatusBarItem::Board, + StatusBarItem::Board, show, ) } pub(super) fn apply_toolbar_toggle_status_page_badge_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, show: bool, ) -> bool { self.set_status_bar_item_visible_with_resources( resources.ui_engine, resources.measurer, - crate::config::StatusBarItem::Page, + StatusBarItem::Page, show, ) } @@ -497,8 +502,13 @@ impl InputState { #[cfg(test)] mod tests { - use crate::config::{ToolbarLayoutMode, ToolbarSectionFlag}; - use crate::input::state::{TopMenuState, test_support::make_test_input_state}; + use std::time::{Duration, Instant}; + + use crate::config::{ToolbarLayoutMode, ToolbarSectionFlag, TopDisplayMode}; + use crate::draw::TextMeasurer; + use crate::input::state::TopMenuState; + use crate::input::state::test_support::make_test_input_state; + use crate::input::{InputState, Tool}; use crate::ui::toolbar::ToolbarEvent; #[test] @@ -637,30 +647,26 @@ mod tests { let mut state = make_test_input_state(); assert!(state.apply_toolbar_event(ToolbarEvent::SetTopMinimized(true))); assert!( - !state.toolbar_restore_guard_blocks(std::time::Instant::now()), + !state.toolbar_restore_guard_blocks(Instant::now()), "minimizing arms nothing" ); assert!(state.apply_toolbar_event(ToolbarEvent::SetTopMinimized(false))); - let now = std::time::Instant::now(); + let now = Instant::now(); assert!(state.toolbar_restore_guard_blocks(now)); - assert!(!state.toolbar_restore_guard_blocks(now + std::time::Duration::from_secs(1))); + assert!(!state.toolbar_restore_guard_blocks(now + Duration::from_secs(1))); } #[test] fn restoring_from_the_micro_chip_guards_the_next_click() { let mut state = make_test_input_state(); - state.apply_toolbar_event(ToolbarEvent::SetTopDisplayMode( - crate::config::TopDisplayMode::Micro, - )); - assert!(!state.toolbar_restore_guard_blocks(std::time::Instant::now())); + state.apply_toolbar_event(ToolbarEvent::SetTopDisplayMode(TopDisplayMode::Micro)); + assert!(!state.toolbar_restore_guard_blocks(Instant::now())); - assert!(state.apply_toolbar_event(ToolbarEvent::SetTopDisplayMode( - crate::config::TopDisplayMode::Full - ))); + assert!(state.apply_toolbar_event(ToolbarEvent::SetTopDisplayMode(TopDisplayMode::Full))); - assert!(state.toolbar_restore_guard_blocks(std::time::Instant::now())); + assert!(state.toolbar_restore_guard_blocks(Instant::now())); } /// The toolbar's Exit is an explicit request, so xdg stay-mode focus loss @@ -697,7 +703,7 @@ mod tests { let mut state = make_test_input_state(); state.apply_toolbar_event(ToolbarEvent::ToggleLayoutMenu(true)); - state.apply_toolbar_event(ToolbarEvent::SelectTool(crate::input::Tool::Marker)); + state.apply_toolbar_event(ToolbarEvent::SelectTool(Tool::Marker)); assert_eq!(state.toolbar_top_menu(), TopMenuState::Closed); } @@ -712,7 +718,7 @@ mod tests { assert_minimize_and_close_handle_canvas(&mut state); } - fn assert_canvas_open_closes_top_menus(state: &mut crate::input::InputState) { + fn assert_canvas_open_closes_top_menus(state: &mut InputState) { // Opening Canvas closes the overflow, the shapes picker, and resets // the shared internal scroll. state.apply_toolbar_event(ToolbarEvent::ToggleTopOverflow(true)); @@ -723,7 +729,7 @@ mod tests { assert_eq!(state.toolbar_top_popover_scroll(), 0.0); } - fn assert_canvas_is_exclusive_with_session_and_settings(state: &mut crate::input::InputState) { + fn assert_canvas_is_exclusive_with_session_and_settings(state: &mut InputState) { // Opening Session or Settings closes Canvas, and Canvas closes them. assert!(state.apply_toolbar_event(ToolbarEvent::ToggleSessionPopover(true))); assert_eq!(state.toolbar_top_menu(), TopMenuState::SessionPopover); @@ -735,7 +741,7 @@ mod tests { assert_eq!(state.toolbar_top_menu(), TopMenuState::CanvasPopover); } - fn assert_overflow_and_shapes_close_canvas(state: &mut crate::input::InputState) { + fn assert_overflow_and_shapes_close_canvas(state: &mut InputState) { // Re-opening the overflow (or the shapes picker) closes Canvas. assert!(state.apply_toolbar_event(ToolbarEvent::ToggleTopOverflow(true))); assert_eq!(state.toolbar_top_menu(), TopMenuState::TopOverflow); @@ -744,7 +750,7 @@ mod tests { assert_eq!(state.toolbar_top_menu(), TopMenuState::ShapePicker); } - fn assert_minimize_and_close_handle_canvas(state: &mut crate::input::InputState) { + fn assert_minimize_and_close_handle_canvas(state: &mut InputState) { // Minimizing the strip closes an open Canvas popover. state.apply_toolbar_event(ToolbarEvent::ToggleCanvasPopover(true)); state.apply_toolbar_event(ToolbarEvent::SetTopMinimized(true)); @@ -867,7 +873,7 @@ mod tests { state.toolbar_top_popover_scroll(), ); - state.apply_toolbar_event(ToolbarEvent::SelectTool(crate::input::Tool::Line)); + state.apply_toolbar_event(ToolbarEvent::SelectTool(Tool::Line)); assert_eq!(state.toolbar_top_menu(), TopMenuState::Closed); } @@ -901,7 +907,7 @@ mod tests { use crate::config::{StatusBarItem, StatusBarStyle, StatusPosition, TopDisplayMode}; use crate::ui_text::UiTextEngine; let engine = UiTextEngine::default(); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &engine, diff --git a/src/input/state/core/toolbar/apply/mod.rs b/src/input/state/core/toolbar/apply/mod.rs index 9a3f520f7..145ba280e 100644 --- a/src/input/state/core/toolbar/apply/mod.rs +++ b/src/input/state/core/toolbar/apply/mod.rs @@ -5,17 +5,24 @@ mod layout; mod pages; mod tools; +use std::time::Instant; + use crate::input::InputState; +use crate::config::{Action, TopDisplayMode}; +use crate::draw::TextMeasurer; +use crate::input::state::InputTextResources; use crate::ui::toolbar::ToolbarEvent; +use crate::ui::toolbar::model::StrokeSetting; +use crate::ui_text::UiTextEngine; impl InputState { /// Applies a toolbar-originated event to the input state. /// /// Returns true if the event resulted in a state change. pub fn apply_toolbar_event(&mut self, event: ToolbarEvent) -> bool { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.apply_toolbar_event_with_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -27,7 +34,7 @@ impl InputState { pub(crate) fn apply_toolbar_event_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, event: ToolbarEvent, ) -> bool { // Resolve the keyboard-action equivalent before the event is consumed @@ -55,11 +62,7 @@ impl InputState { /// pressed the key" case the command palette records. Only genuine state /// changes for actions that resolve to a shortcut count, so the coach can /// always name the key and no-op clicks never build a streak. - fn note_toolbar_shortcut_slow_path( - &mut self, - coach_action: Option, - changed: bool, - ) { + fn note_toolbar_shortcut_slow_path(&mut self, coach_action: Option, changed: bool) { if !changed { return; } @@ -73,7 +76,7 @@ impl InputState { fn apply_toolbar_event_inner_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, event: ToolbarEvent, ) -> bool { match event { @@ -97,18 +100,15 @@ impl InputState { self.apply_toolbar_set_spotlight_magnification(value) } ToolbarEvent::SetPenSmoothing(level) => self.apply_toolbar_set_pen_smoothing(level), - ToolbarEvent::NudgePenSmoothing(steps) => self.apply_toolbar_nudge_stroke_setting( - crate::ui::toolbar::model::StrokeSetting::Smoothing, - steps, - ), + ToolbarEvent::NudgePenSmoothing(steps) => { + self.apply_toolbar_nudge_stroke_setting(StrokeSetting::Smoothing, steps) + } ToolbarEvent::SetShapeRecognitionSensitivity(level) => { self.set_shape_recognition_sensitivity(level) } - ToolbarEvent::NudgeShapeRecognitionSensitivity(steps) => self - .apply_toolbar_nudge_stroke_setting( - crate::ui::toolbar::model::StrokeSetting::ShapeDetection, - steps, - ), + ToolbarEvent::NudgeShapeRecognitionSensitivity(steps) => { + self.apply_toolbar_nudge_stroke_setting(StrokeSetting::ShapeDetection, steps) + } ToolbarEvent::OpenFontPicker => self.apply_toolbar_open_font_picker(), ToolbarEvent::SetEraserMode(mode) => self.apply_toolbar_set_eraser_mode(mode), ToolbarEvent::SetFont(descriptor) => self.apply_toolbar_set_font(descriptor), @@ -244,16 +244,16 @@ impl InputState { ToolbarEvent::SetTopMinimized(minimized) => { let changed = self.apply_toolbar_set_top_minimized(minimized); if changed && !minimized { - self.toolbar.arm_restore_guard(std::time::Instant::now()); + self.toolbar.arm_restore_guard(Instant::now()); } changed } ToolbarEvent::SetTopDisplayMode(mode) => { - let from_micro = self.top_display_state() == crate::config::TopDisplayMode::Micro; + let from_micro = self.top_display_state() == TopDisplayMode::Micro; let changed = self.apply_toolbar_set_top_display_mode_with_resources(resources, mode); - if changed && from_micro && mode == crate::config::TopDisplayMode::Full { - self.toolbar.arm_restore_guard(std::time::Instant::now()); + if changed && from_micro && mode == TopDisplayMode::Full { + self.toolbar.arm_restore_guard(Instant::now()); } changed } @@ -377,10 +377,10 @@ impl InputState { mod coach_tests { use crate::config::{Action, Shortcut}; use crate::draw::{Color, Shape}; - use crate::input::InputState; use crate::input::state::test_support::{ make_test_input_state, make_test_input_state_with_action_bindings, }; + use crate::input::{InputState, ZoomAnchor}; use crate::ui::toolbar::ToolbarEvent; use std::collections::HashMap; @@ -451,7 +451,7 @@ mod coach_tests { state .take_pending_zoom_request() .map(|request| request.anchor), - Some(crate::input::ZoomAnchor::ScreenCenter), + Some(ZoomAnchor::ScreenCenter), "a toolbar zoom centres the screen, not the toolbar button" ); assert!(state.pending_onboarding_usage.used_zoom_control); diff --git a/src/input/state/core/toolbar/apply/pages.rs b/src/input/state/core/toolbar/apply/pages.rs index 3081b6c58..4eac8355b 100644 --- a/src/input/state/core/toolbar/apply/pages.rs +++ b/src/input/state/core/toolbar/apply/pages.rs @@ -1,5 +1,4 @@ -use crate::draw::PageDeleteOutcome; -use crate::draw::TextMeasurer; +use crate::draw::{PageDeleteOutcome, TextMeasurer}; use crate::i18n::tr; use crate::input::InputState; use crate::input::state::{Toast, ToastPriority}; diff --git a/src/input/state/core/toolbar/apply/tools.rs b/src/input/state/core/toolbar/apply/tools.rs index c525cda23..a63d1095d 100644 --- a/src/input/state/core/toolbar/apply/tools.rs +++ b/src/input/state/core/toolbar/apply/tools.rs @@ -1,10 +1,10 @@ -use crate::draw::TextMeasurer; -use crate::draw::{Color, FontDescriptor}; +use crate::draw::{Color, FontDescriptor, TextMeasurer}; use crate::input::state::TopMenuState; use crate::input::{DrawingState, EraserMode, InputState, Tool}; +use crate::input::state::InputTextResources; use crate::ui::toolbar::PrecisionEntryTarget; -use crate::ui::toolbar::model::ToolbarSliderSpec; +use crate::ui::toolbar::model::{StrokeSetting, ToolbarSliderSpec}; impl InputState { /// Open the precise-entry popup for a pill numeral. @@ -67,7 +67,7 @@ impl InputState { pub(super) fn apply_toolbar_set_color_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, color: Color, ) -> bool { self.apply_color_from_ui_with_measurer(measurer, color) @@ -95,7 +95,7 @@ impl InputState { pub(super) fn apply_toolbar_nudge_stroke_setting( &mut self, - setting: crate::ui::toolbar::model::StrokeSetting, + setting: StrokeSetting, steps: i32, ) -> bool { use crate::ui::toolbar::model::StrokeSetting; @@ -213,7 +213,7 @@ impl InputState { pub(super) fn apply_toolbar_enter_text_mode_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, ) -> bool { let _ = self.set_tool_override_with(resources.measurer, None); self.toolbar_enter_text_mode_with_resources(resources); @@ -223,7 +223,7 @@ impl InputState { pub(super) fn apply_toolbar_enter_sticky_note_mode_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, ) -> bool { let _ = self.set_tool_override_with(resources.measurer, None); self.toolbar_enter_sticky_note_mode_with_resources(resources); @@ -316,6 +316,8 @@ impl InputState { #[cfg(test)] mod tests { + use crate::draw::ArrowStyle; + use crate::input::Tool; use crate::input::state::TopMenuState; use crate::input::state::test_support::make_test_input_state; use crate::ui::toolbar::ToolbarEvent; @@ -360,14 +362,9 @@ mod tests { assert!(state.apply_toolbar_event(ToolbarEvent::ToggleArrowStyleMenu(true))); assert_eq!(state.toolbar_top_menu(), TopMenuState::ArrowStyleMenu); - assert!( - state.apply_toolbar_event(ToolbarEvent::SetArrowStyle(crate::draw::ArrowStyle::Curved)) - ); - assert_eq!(state.style.arrow_style, crate::draw::ArrowStyle::Curved); - assert!( - !state - .apply_toolbar_event(ToolbarEvent::SetArrowStyle(crate::draw::ArrowStyle::Curved)) - ); + assert!(state.apply_toolbar_event(ToolbarEvent::SetArrowStyle(ArrowStyle::Curved))); + assert_eq!(state.style.arrow_style, ArrowStyle::Curved); + assert!(!state.apply_toolbar_event(ToolbarEvent::SetArrowStyle(ArrowStyle::Curved))); } #[test] @@ -375,7 +372,7 @@ mod tests { let mut state = make_test_input_state(); state.apply_toolbar_event(ToolbarEvent::TogglePenFeelPanel(true)); - state.apply_toolbar_event(ToolbarEvent::SelectTool(crate::input::Tool::Marker)); + state.apply_toolbar_event(ToolbarEvent::SelectTool(Tool::Marker)); assert_eq!(state.toolbar_top_menu(), TopMenuState::Closed); } diff --git a/src/input/state/core/toolbar/state.rs b/src/input/state/core/toolbar/state.rs index 6c946138a..f9b7585b2 100644 --- a/src/input/state/core/toolbar/state.rs +++ b/src/input/state/core/toolbar/state.rs @@ -1,3 +1,6 @@ +use std::time::{Duration, Instant}; + +use crate::config; use crate::config::{ ResolvedToolbarItems, ToolbarConfig, ToolbarItemId, ToolbarItemOrderGroup, ToolbarItemVisibilitySetting, ToolbarItemsConfig, ToolbarLayoutMode, ToolbarModeOverrides, @@ -69,18 +72,17 @@ pub(in crate::input::state) struct ToolbarInteraction { /// enough that a deliberate next click still works. #[derive(Debug, Default, Clone, Copy)] pub(in crate::input::state) struct RestoreClickGuard { - until: Option, + until: Option, } impl RestoreClickGuard { - pub(in crate::input::state) const WINDOW: std::time::Duration = - std::time::Duration::from_millis(450); + pub(in crate::input::state) const WINDOW: Duration = Duration::from_millis(450); - fn arm(&mut self, now: std::time::Instant) { + fn arm(&mut self, now: Instant) { self.until = Some(now + Self::WINDOW); } - fn blocks(&self, now: std::time::Instant) -> bool { + fn blocks(&self, now: Instant) -> bool { self.until.is_some_and(|until| now < until) } } @@ -155,11 +157,11 @@ impl ToolbarInteraction { } /// Start the post-restore click guard (see [`RestoreClickGuard`]). - pub(in crate::input::state) fn arm_restore_guard(&mut self, now: std::time::Instant) { + pub(in crate::input::state) fn arm_restore_guard(&mut self, now: Instant) { self.restore_guard.arm(now); } - pub(in crate::input::state) fn restore_guard_blocks(&self, now: std::time::Instant) -> bool { + pub(in crate::input::state) fn restore_guard_blocks(&self, now: Instant) -> bool { self.restore_guard.blocks(now) } @@ -532,8 +534,7 @@ impl ToolbarInteraction { pub(in crate::input::state) fn reset_individual_item_visibility(&mut self) -> bool { let mut changed = false; - for (&id, &setting) in crate::config::factory_individual_toolbar_item_visibility_settings() - { + for (&id, &setting) in config::factory_individual_toolbar_item_visibility_settings() { changed |= self.items.set_visibility_setting(id, setting); } if changed { @@ -632,6 +633,8 @@ impl ToolbarInteraction { #[cfg(test)] mod tests { + use crate::config::toolbar_item_ids; + use super::*; fn legacy_visibility() -> ToolbarSectionVisibility { @@ -713,7 +716,7 @@ mod tests { let mut toolbar = ToolbarInteraction::default(); let drag = ( ToolbarItemOrderGroup::TopTools, - crate::config::toolbar_item_ids::TOP_TOOL_PEN, + toolbar_item_ids::TOP_TOOL_PEN, ); toolbar.begin_customize_drag(drag); @@ -728,7 +731,7 @@ mod tests { fn dragging_an_item_reorders_only_its_group() { let mut toolbar = ToolbarInteraction::default(); let group = ToolbarItemOrderGroup::TopTools; - let pen = crate::config::toolbar_item_ids::TOP_TOOL_PEN; + let pen = toolbar_item_ids::TOP_TOOL_PEN; let before = toolbar.resolved_items().order.ordered_ids(group).to_vec(); toolbar.begin_customize_drag((group, pen)); diff --git a/src/input/state/core/utility/capture_feedback.rs b/src/input/state/core/utility/capture_feedback.rs index febe30242..f58eaa850 100644 --- a/src/input/state/core/utility/capture_feedback.rs +++ b/src/input/state/core/utility/capture_feedback.rs @@ -1,5 +1,7 @@ //! Compact capture-outcome toasts with Open folder and Copy path actions. +use crate::paths; + use super::super::base::{InputState, TextClipboardRequest, Toast, ToastCommand, ToastPriority}; use crate::domain::Action; use crate::i18n::{tr, trf}; @@ -24,7 +26,7 @@ impl InputState { let message = capture_feedback_message( saved_path, copied_to_clipboard, - crate::paths::home_dir().as_deref(), + paths::home_dir().as_deref(), ); let (priority, toast) = if saved_path.is_some() { let open_folder = match self.action_binding_primary_label(Action::OpenCaptureFolder) { @@ -89,6 +91,10 @@ fn capture_display_name(path: &Path, home: Option<&Path>) -> String { #[cfg(test)] mod tests { + use std::collections::HashMap; + + use crate::config::Shortcut; + use super::*; use crate::input::state::test_support::make_test_input_state; use std::path::PathBuf; @@ -136,9 +142,9 @@ mod tests { #[test] fn saved_capture_offers_open_folder_with_its_shortcut_and_copy_path() { let mut state = make_test_input_state(); - state.set_action_bindings(std::collections::HashMap::from([( + state.set_action_bindings(HashMap::from([( Action::OpenCaptureFolder, - vec![crate::config::Shortcut::parse("Ctrl+Alt+O").unwrap()], + vec![Shortcut::parse("Ctrl+Alt+O").unwrap()], )])); state.set_capture_feedback(Some(Path::new(SAVED)), true); @@ -191,10 +197,7 @@ mod tests { #[test] fn unbound_open_folder_keeps_a_plain_button_label() { let mut state = make_test_input_state(); - state.set_action_bindings(std::collections::HashMap::from([( - Action::OpenCaptureFolder, - Vec::new(), - )])); + state.set_action_bindings(HashMap::from([(Action::OpenCaptureFolder, Vec::new())])); state.set_capture_feedback(Some(Path::new(SAVED)), false); diff --git a/src/input/state/core/utility/focus_mode.rs b/src/input/state/core/utility/focus_mode.rs index d7083140c..1415cb828 100644 --- a/src/input/state/core/utility/focus_mode.rs +++ b/src/input/state/core/utility/focus_mode.rs @@ -12,12 +12,13 @@ //! [`InputState::break_focus_mode`], so exiting later can never stomp an //! explicit user choice. +use crate::input::DrawingState; + use super::super::base::InputState; use super::super::modes::FocusModeRestore; use crate::domain::Action; use crate::i18n::{tr, trf}; -use crate::input::state::InputTextResources; -use crate::input::state::{Toast, ToastPriority}; +use crate::input::state::{InputTextResources, Toast, ToastPriority}; const FOCUS_MODE_TOAST_KEY: &str = "focus.mode"; @@ -46,7 +47,7 @@ impl InputState { || (self.boards.pan_enabled() && self.boards.show_pan_badge() && !self.board_is_transparent()) - || (matches!(self.state, crate::input::DrawingState::TextInput { .. }) + || (matches!(self.state, DrawingState::TextInput { .. }) && self.text_editing.edit_target().is_some())) } diff --git a/src/input/state/core/utility/frozen_zoom.rs b/src/input/state/core/utility/frozen_zoom.rs index 5bba99b8e..f2a4bfb8c 100644 --- a/src/input/state/core/utility/frozen_zoom.rs +++ b/src/input/state/core/utility/frozen_zoom.rs @@ -57,10 +57,12 @@ impl InputState { #[cfg(test)] mod tests { + use crate::input::state::test_support; + use super::*; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } #[test] diff --git a/src/input/state/core/utility/help_overlay.rs b/src/input/state/core/utility/help_overlay.rs index 7ddde867c..5bb893e61 100644 --- a/src/input/state/core/utility/help_overlay.rs +++ b/src/input/state/core/utility/help_overlay.rs @@ -1,3 +1,7 @@ +use crate::config::Action; +use crate::help_overlay_interaction::{HelpOverlayRegion, HelpRenderResult}; +use crate::input::state::core::modal::ModalSurface; + use super::super::base::InputState; /// Cursor hint for the help overlay. @@ -16,7 +20,7 @@ pub enum HelpOverlayCursorHint { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HelpOverlayClick { /// A clickable row (or the "Replay tour" footer) was hit; run this action. - Run(crate::config::Action), + Run(Action), /// The footer toggle that shows or hides actions without a binding. ToggleUnbound, /// Inside the overlay chrome but not on an interactive element (no-op). @@ -46,7 +50,7 @@ pub(crate) enum HelpOverlayPressSource { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HelpOverlayReleaseOutcome { /// Press and release landed on the SAME clickable row; run its action. - Run(crate::config::Action), + Run(Action), /// Press and release both landed on the unbound-actions toggle. ToggleUnbound, /// Press and release both landed outside the overlay box; dismiss it. @@ -59,15 +63,12 @@ pub enum HelpOverlayReleaseOutcome { impl InputState { /// Install the geometry returned by the public help result renderer for /// subsequent click and cursor queries on this input owner. - pub fn install_help_overlay_render_result( - &mut self, - result: crate::help_overlay_interaction::HelpRenderResult, - ) { + pub fn install_help_overlay_render_result(&mut self, result: HelpRenderResult) { self.help_overlay.install_render_result(result); } fn open_help_overlay_internal(&mut self, quick_mode: bool, track_usage: bool) { - self.close_modals_for_open(crate::input::state::core::modal::ModalSurface::HelpOverlay); + self.close_modals_for_open(ModalSurface::HelpOverlay); self.help_overlay.open(quick_mode); if track_usage { self.pending_onboarding_usage.used_help_overlay = true; @@ -116,12 +117,8 @@ impl InputState { /// help layout: a clickable row/footer action, inside chrome, or a dismiss. pub fn help_overlay_click_at(&self, x: i32, y: i32) -> HelpOverlayClick { match self.help_overlay.region_at(x as f64, y as f64) { - Some(crate::help_overlay_interaction::HelpOverlayRegion::Row(action)) => { - HelpOverlayClick::Run(action) - } - Some(crate::help_overlay_interaction::HelpOverlayRegion::ToggleUnbound) => { - HelpOverlayClick::ToggleUnbound - } + Some(HelpOverlayRegion::Row(action)) => HelpOverlayClick::Run(action), + Some(HelpOverlayRegion::ToggleUnbound) => HelpOverlayClick::ToggleUnbound, Some(_) => HelpOverlayClick::Inside, None => HelpOverlayClick::Outside, } @@ -178,16 +175,11 @@ impl InputState { } match self.help_overlay.region_at(x as f64, y as f64)? { - crate::help_overlay_interaction::HelpOverlayRegion::Search => { - Some(HelpOverlayCursorHint::Text) - } - crate::help_overlay_interaction::HelpOverlayRegion::Row(_) - | crate::help_overlay_interaction::HelpOverlayRegion::ToggleUnbound => { + HelpOverlayRegion::Search => Some(HelpOverlayCursorHint::Text), + HelpOverlayRegion::Row(_) | HelpOverlayRegion::ToggleUnbound => { Some(HelpOverlayCursorHint::Pointer) } - crate::help_overlay_interaction::HelpOverlayRegion::Inside => { - Some(HelpOverlayCursorHint::Default) - } + HelpOverlayRegion::Inside => Some(HelpOverlayCursorHint::Default), } } } diff --git a/src/input/state/core/utility/interaction.rs b/src/input/state/core/utility/interaction.rs index 340374aae..244d14874 100644 --- a/src/input/state/core/utility/interaction.rs +++ b/src/input/state/core/utility/interaction.rs @@ -1,6 +1,5 @@ use super::super::base::{DrawingState, InputState, PasteAnchor}; -use crate::draw::DirtyRegionReport; -use crate::draw::TextMeasurer; +use crate::draw::{DirtyRegionReport, TextMeasurer}; use crate::util::Rect; use std::time::Instant; @@ -265,6 +264,8 @@ impl InputState { #[cfg(test)] mod tests { + use crate::draw::DirtyFullReason; + use super::*; use crate::input::events::MouseButton; use crate::input::state::test_support::make_test_input_state; @@ -337,7 +338,7 @@ mod tests { #[test] fn cancel_text_input_clears_wrap_width_and_returns_to_idle() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.style.text_wrap_width = Some(240); state.state = DrawingState::text_input(10, 20, "hello".to_string()); @@ -352,7 +353,7 @@ mod tests { #[test] fn cancel_text_input_releases_an_active_block_drag() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.state = DrawingState::text_input(10, 20, "hello".to_string()); state.modifiers.alt = true; @@ -368,7 +369,7 @@ mod tests { #[test] fn try_cancel_active_interaction_reports_false_when_idle() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.needs_redraw = false; @@ -381,7 +382,7 @@ mod tests { #[test] fn try_cancel_active_interaction_cancels_drawing_and_ends_drag() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.state = DrawingState::Drawing { @@ -420,15 +421,12 @@ mod tests { state.update_screen_dimensions(100, 50); state .dirty_tracker - .mark_full_for(crate::draw::DirtyFullReason::CanvasClear); + .mark_full_for(DirtyFullReason::CanvasClear); let report = state.take_dirty_region_report(); assert_eq!(report.regions, vec![Rect::new(0, 0, 100, 50).unwrap()]); - assert_eq!( - report.full_reason, - Some(crate::draw::DirtyFullReason::CanvasClear) - ); + assert_eq!(report.full_reason, Some(DirtyFullReason::CanvasClear)); assert!(state.take_dirty_region_report().regions.is_empty()); } } diff --git a/src/input/state/core/utility/launcher.rs b/src/input/state/core/utility/launcher.rs index 7fdd3066b..da82a725b 100644 --- a/src/input/state/core/utility/launcher.rs +++ b/src/input/state/core/utility/launcher.rs @@ -1,3 +1,5 @@ +use crate::desktop_open::DesktopOpenRequest; + use super::super::base::InputState; use crate::config::Config; use crate::configurator_destination::ConfiguratorDestination; @@ -46,13 +48,13 @@ impl InputState { log::info!("Queued capture folder open at {}", folder.display()); self.set_pending_backend_action(PendingBackendAction::DesktopOpen( - crate::desktop_open::DesktopOpenRequest::CaptureFolder(folder), + DesktopOpenRequest::CaptureFolder(folder), )); } /// Opens the primary config file using the desktop default application. pub(crate) fn open_config_file_default(&mut self) -> bool { - let path = match Config::get_config_path() { + let path = match Config::config_path() { Ok(p) => p, Err(err) => { log::error!("Unable to resolve config path: {}", err); @@ -67,7 +69,7 @@ impl InputState { log::info!("Queued config file open at {}", path.display()); self.set_pending_backend_action(PendingBackendAction::DesktopOpen( - crate::desktop_open::DesktopOpenRequest::ConfigFile(path), + DesktopOpenRequest::ConfigFile(path), )); true } diff --git a/src/input/state/core/utility/light_mode.rs b/src/input/state/core/utility/light_mode.rs index 51abf284e..f01186d0d 100644 --- a/src/input/state/core/utility/light_mode.rs +++ b/src/input/state/core/utility/light_mode.rs @@ -1,10 +1,11 @@ +use crate::ui_text::UiTextEngine; + use super::super::base::{DesktopEnvironment, InputState, ShellMode}; use super::super::modes::LightModeRestore; use crate::domain::Action; use crate::draw::TextMeasurer; use crate::i18n::tr; -use crate::input::state::InputTextResources; -use crate::input::state::{Toast, ToastPriority}; +use crate::input::state::{InputTextResources, Toast, ToastPriority}; use crate::input::tool::Tool; impl InputState { @@ -78,7 +79,7 @@ impl InputState { pub fn toggle_light_mode_drawing(&mut self) -> bool { let measurer = TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let ui_engine = UiTextEngine::default(); self.toggle_light_mode_drawing_with_resources(InputTextResources { measurer: &measurer, ui_engine: &ui_engine, @@ -99,7 +100,7 @@ impl InputState { pub fn set_light_mode_drawing(&mut self, drawing: bool) -> bool { let measurer = TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let ui_engine = UiTextEngine::default(); self.set_light_mode_drawing_with_resources( InputTextResources { measurer: &measurer, diff --git a/src/input/state/core/utility/ocr_scan.rs b/src/input/state/core/utility/ocr_scan.rs index f211a5a86..a3f390057 100644 --- a/src/input/state/core/utility/ocr_scan.rs +++ b/src/input/state/core/utility/ocr_scan.rs @@ -11,6 +11,8 @@ use std::time::{Duration, Instant}; +use crate::ui::anim; + use super::super::base::InputState; use crate::i18n::{tr, trf}; use crate::ocr::OcrRequestId; @@ -107,7 +109,7 @@ impl OcrScan { /// under `[ui] reduced_motion`: the region is marked with a static /// indicator instead of a moving band (WCAG 2.3.3). pub(crate) fn sweep_progress(self, now: Instant) -> Option { - self.sweep_progress_for(now, crate::ui::anim::motion_enabled()) + self.sweep_progress_for(now, anim::motion_enabled()) } /// Split from the accessor so both motion settings can be exercised @@ -156,7 +158,7 @@ impl InputState { outcome: OcrScanOutcome, now: Instant, ) { - self.settle_ocr_scan_for(request, outcome, now, crate::ui::anim::motion_enabled()); + self.settle_ocr_scan_for(request, outcome, now, anim::motion_enabled()); } pub(crate) fn settle_ocr_scan_for( @@ -209,7 +211,7 @@ impl InputState { /// which wakes the loop itself. A still card is the one case that needs a /// deadline: nothing else will wake the loop to expire it. pub(crate) fn ocr_scan_wake_after(&self, now: Instant) -> Option { - self.ocr_scan_wake_after_for(now, crate::ui::anim::motion_enabled()) + self.ocr_scan_wake_after_for(now, anim::motion_enabled()) } pub(crate) fn ocr_scan_wake_after_for(&self, now: Instant, motion: bool) -> Option { @@ -234,7 +236,7 @@ impl InputState { /// pin a repaint at the animation frame rate for the length of a /// recognition; `ocr_scan_wake_after` supplies the one deadline it needs. pub fn advance_ocr_scan(&mut self, now: Instant) -> bool { - self.advance_ocr_scan_for(now, crate::ui::anim::motion_enabled()) + self.advance_ocr_scan_for(now, anim::motion_enabled()) } pub(crate) fn advance_ocr_scan_for(&mut self, now: Instant, motion: bool) -> bool { @@ -288,7 +290,7 @@ fn completed_sweep(elapsed: Duration, motion: bool) -> Duration { /// Opacity of the outcome card `shown` into its lifetime. pub(crate) fn result_opacity(shown: Duration) -> f64 { - result_opacity_for(shown, crate::ui::anim::motion_enabled()) + result_opacity_for(shown, anim::motion_enabled()) } /// Split from the accessor so both motion settings can be exercised without diff --git a/src/input/state/core/utility/pending.rs b/src/input/state/core/utility/pending.rs index f61e7a705..d5cd73cf0 100644 --- a/src/input/state/core/utility/pending.rs +++ b/src/input/state/core/utility/pending.rs @@ -260,15 +260,19 @@ impl InputState { #[cfg(test)] mod tests { + use crate::desktop_open::DesktopOpenRequest; + use crate::input::state::test_support; + use super::*; use crate::config::Action; use crate::draw::{BLACK, WHITE}; use crate::input::state::KeybindingEditOperation; - use crate::input::state::core::base::{InputEffect, InputEffectDrain}; - use crate::input::state::core::base::{KeybindingEditRequest, PresetAction, QuickColorEdit}; + use crate::input::state::core::base::{ + InputEffect, InputEffectDrain, KeybindingEditRequest, PresetAction, QuickColorEdit, + }; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } #[test] @@ -342,7 +346,7 @@ mod tests { #[test] fn pending_desktop_open_is_taken_without_requesting_early_exit() { let mut state = make_state(); - let request = crate::desktop_open::DesktopOpenRequest::CaptureFolder("/tmp/capture".into()); + let request = DesktopOpenRequest::CaptureFolder("/tmp/capture".into()); state.set_pending_backend_action(PendingBackendAction::DesktopOpen(request.clone())); assert!(!state.should_exit); diff --git a/src/input/state/core/utility/presenter_mode.rs b/src/input/state/core/utility/presenter_mode.rs index b043a85a6..0408f20a9 100644 --- a/src/input/state/core/utility/presenter_mode.rs +++ b/src/input/state/core/utility/presenter_mode.rs @@ -7,12 +7,15 @@ //! override marks it through `set_tool_override`, where the change actually //! reaches the snapshot. +use crate::config::{ + PresenterModeConfig, PresenterToolBehavior, PresenterToolbarMode, TopDisplayMode, +}; + use super::super::base::InputState; -use super::super::modes::PresenterRestore; +use super::super::modes::{PresenterChrome, PresenterRestore}; use crate::domain::Action; use crate::i18n::tr; -use crate::input::state::InputTextResources; -use crate::input::state::{Toast, ToastPriority}; +use crate::input::state::{InputTextResources, Toast, ToastPriority}; use crate::input::tool::Tool; impl InputState { @@ -20,7 +23,7 @@ impl InputState { self.modes.presenter_active() } - pub(crate) fn presenter_mode_config(&self) -> &crate::config::PresenterModeConfig { + pub(crate) fn presenter_mode_config(&self) -> &PresenterModeConfig { self.modes.presenter_config() } @@ -36,16 +39,11 @@ impl InputState { self.modes.presenter_restored_click_highlight() } - pub(crate) fn presenter_restored_top_display_mode( - &self, - ) -> Option { + pub(crate) fn presenter_restored_top_display_mode(&self) -> Option { self.modes.presenter_restored_top_display_mode() } - pub(crate) fn retarget_presenter_toolbar_display_mode( - &mut self, - mode: crate::config::TopDisplayMode, - ) -> bool { + pub(crate) fn retarget_presenter_toolbar_display_mode(&mut self, mode: TopDisplayMode) -> bool { self.modes.retarget_presenter_toolbar_display_mode(mode) } @@ -55,9 +53,7 @@ impl InputState { } #[cfg(test)] - pub(crate) fn presenter_mode_config_mut_for_test( - &mut self, - ) -> &mut crate::config::PresenterModeConfig { + pub(crate) fn presenter_mode_config_mut_for_test(&mut self) -> &mut PresenterModeConfig { self.modes.presenter_config_mut_for_test() } @@ -133,12 +129,14 @@ impl InputState { self.cancel_active_interaction_with(resources.measurer); let restore = PresenterRestore::capture( &config, - self.ui_visibility.show_status_bar, - self.ui_visibility.show_tool_preview, - self.toolbar_visibility_snapshot(), - self.click_highlight_enabled(), - self.input_hud_enabled(), - self.tool_override(), + PresenterChrome { + show_status_bar: self.ui_visibility.show_status_bar, + show_tool_preview: self.ui_visibility.show_tool_preview, + toolbar_visibility: self.toolbar_visibility_snapshot(), + click_highlight_enabled: self.click_highlight_enabled(), + input_hud_enabled: self.input_hud_enabled(), + tool_override: self.tool_override(), + }, ); if config.hide_status_bar { self.ui_visibility.show_status_bar = false; @@ -148,23 +146,20 @@ impl InputState { } if config.hide_toolbars { match config.toolbar_mode { - crate::config::PresenterToolbarMode::Hidden => { + PresenterToolbarMode::Hidden => { self.hide_toolbar_visibility(); } - crate::config::PresenterToolbarMode::Micro => { + PresenterToolbarMode::Micro => { // The top strip stays up as the micro chip. self.set_top_display_mode_with_resources( resources.ui_engine, resources.measurer, - crate::config::TopDisplayMode::Micro, + TopDisplayMode::Micro, ); } } } - if !matches!( - config.tool_behavior, - crate::config::PresenterToolBehavior::Keep - ) { + if !matches!(config.tool_behavior, PresenterToolBehavior::Keep) { self.set_tool_override_with(resources.measurer, Some(Tool::Highlight)); } if config.enable_click_highlight && !self.click_highlight_enabled() { diff --git a/src/input/state/core/utility/restore_notice.rs b/src/input/state/core/utility/restore_notice.rs index a10ce2b26..18ec715ac 100644 --- a/src/input/state/core/utility/restore_notice.rs +++ b/src/input/state/core/utility/restore_notice.rs @@ -88,6 +88,11 @@ fn restored_annotations_message(count: usize) -> String { #[cfg(test)] mod tests { + use std::path::PathBuf; + use std::time::{Duration, Instant}; + + use crate::draw::TextMeasurer; + use super::*; use crate::draw::Shape; use crate::input::BOARD_ID_WHITEBOARD; @@ -161,7 +166,7 @@ mod tests { /// would then erase that page instead. #[test] fn deleting_the_restored_page_retracts_the_notice() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.page_new(); add_rect(&mut state); @@ -169,11 +174,11 @@ mod tests { add_rect(&mut state); assert!(state.announce_restored_annotations()); - let requested_at = std::time::Instant::now(); + let requested_at = Instant::now(); state.delete_active_page_at_with_measurer(&measurer, requested_at); state.delete_active_page_at_with_measurer( &measurer, - requested_at + std::time::Duration::from_millis(1), + requested_at + Duration::from_millis(1), ); assert_eq!(state.boards.page_count(), 1); @@ -195,8 +200,8 @@ mod tests { SessionOptions, apply_snapshot, apply_snapshot_replacing_boards, snapshot_from_input, }; - let measurer = crate::draw::TextMeasurer::default(); - let mut options = SessionOptions::new(std::path::PathBuf::from("/tmp"), "restore-notice"); + let measurer = TextMeasurer::default(); + let mut options = SessionOptions::new(PathBuf::from("/tmp"), "restore-notice"); options.persist_transparent = true; options.restore_tool_state = false; diff --git a/src/input/state/core/utility/sequence.rs b/src/input/state/core/utility/sequence.rs index 56d82702e..e5632c62d 100644 --- a/src/input/state/core/utility/sequence.rs +++ b/src/input/state/core/utility/sequence.rs @@ -5,6 +5,7 @@ //! a one-second timeout, or an ownership boundary that clears pending state. use std::collections::HashMap; +use std::slice; use std::time::{Duration, Instant}; use crate::config::{KeyBinding, Shortcut}; @@ -109,12 +110,12 @@ impl SequenceTrie { match self.lookup_steps(&steps, pending, now) { SequenceMatch::None => { // Mismatch: cancel, then re-evaluate the new chord once. - self.dispatch_or_pending(std::slice::from_ref(chord), pending, now) + self.dispatch_or_pending(slice::from_ref(chord), pending, now) } other => other, } } else { - self.dispatch_or_pending(std::slice::from_ref(chord), pending, now) + self.dispatch_or_pending(slice::from_ref(chord), pending, now) } } @@ -148,7 +149,7 @@ impl SequenceTrie { } fn complete_single_match(&self, chord: &KeyBinding) -> SequenceMatch { - let Some(node) = self.node_for(std::slice::from_ref(chord)) else { + let Some(node) = self.node_for(slice::from_ref(chord)) else { return SequenceMatch::None; }; node.action diff --git a/src/input/state/core/utility/toasts.rs b/src/input/state/core/utility/toasts.rs index d5f67a5ea..df4643299 100644 --- a/src/input/state/core/utility/toasts.rs +++ b/src/input/state/core/utility/toasts.rs @@ -1,9 +1,12 @@ +use std::path::PathBuf; + use super::super::base::{ CompositorCapabilities, InputState, Toast, ToastCommand, ToastPress, ToastPriority, ToastPushOutcome, UiToastState, }; use super::super::feedback::ToastBounds; -use crate::capture::{ImageOperationKind, file::FileSaveConfig}; +use crate::capture::ImageOperationKind; +use crate::capture::file::FileSaveConfig; use crate::domain::Action; use crate::i18n::{tr, trf}; use std::time::Instant; @@ -190,7 +193,7 @@ impl InputState { pub(crate) fn complete_clipboard_fallback_save( &mut self, id: u64, - result: Result, + result: Result, ) { let Some(fallback) = self .selection_clipboard @@ -245,11 +248,12 @@ mod tests { TEXT_EDIT_ENTRY_DURATION_MS, TextEditEntryFeedback, }; + use crate::input::state::test_support; use crate::ui::toolbar::ToolbarEvent; use std::time::Duration; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } #[test] diff --git a/src/input/state/core/zoom_chip.rs b/src/input/state/core/zoom_chip.rs index 209b9acc0..3642e4217 100644 --- a/src/input/state/core/zoom_chip.rs +++ b/src/input/state/core/zoom_chip.rs @@ -20,7 +20,10 @@ mod state; pub use state::ZoomChipState; use crate::config::{Action, StatusBarStyle}; +use crate::input::DrawingState; +use crate::ui; use crate::ui::{ZoomChipButtonKind, ZoomChipLayout, ZoomChipPress}; +use crate::ui_text::UiTextEngine; use super::base::InputState; @@ -52,7 +55,7 @@ impl InputState { screen_width: u32, screen_height: u32, ) { - let engine = crate::ui_text::UiTextEngine::default(); + let engine = UiTextEngine::default(); self.update_zoom_chip_layout_for_pointer_with_engine( &engine, style, @@ -64,14 +67,14 @@ impl InputState { pub(crate) fn update_zoom_chip_layout_for_pointer_with_engine( &mut self, - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, style: &StatusBarStyle, screen_width: u32, screen_height: u32, chrome_cursor_focused: bool, ) { let layout = if self.zoom_chip_enabled() { - crate::ui::compute_zoom_chip_layout_with_engine( + ui::compute_zoom_chip_layout_with_engine( engine, self, style, @@ -111,8 +114,7 @@ impl InputState { /// contract as `update_status_hud_hover_from_pointer`: click gates + /// idle pointer, redraw on transitions only). pub(crate) fn update_zoom_chip_hover_from_pointer(&mut self, x: i32, y: i32) { - let new_hover = if matches!(self.state, crate::input::DrawingState::Idle) - && self.zoom_chip_contains(x, y) + let new_hover = if matches!(self.state, DrawingState::Idle) && self.zoom_chip_contains(x, y) { self.zoom_chip_button_at(x, y) .filter(|kind| self.zoom_chip_button_enabled(*kind)) diff --git a/src/input/state/core/zoom_chip/state.rs b/src/input/state/core/zoom_chip/state.rs index e2350545f..e42003a9d 100644 --- a/src/input/state/core/zoom_chip/state.rs +++ b/src/input/state/core/zoom_chip/state.rs @@ -1,3 +1,5 @@ +use std::mem; + use crate::config::ZoomChipDisplay; use crate::ui::{ZoomChipButtonKind, ZoomChipLayout, ZoomChipPress}; @@ -74,7 +76,7 @@ impl ZoomChipState { } pub(in crate::input::state) fn take_press_pending(&mut self) -> ZoomChipPress { - std::mem::replace(&mut self.press_pending, ZoomChipPress::None) + mem::replace(&mut self.press_pending, ZoomChipPress::None) } } diff --git a/src/input/state/from_config.rs b/src/input/state/from_config.rs index 832cb7909..eef3f0c20 100644 --- a/src/input/state/from_config.rs +++ b/src/input/state/from_config.rs @@ -1,11 +1,13 @@ use crate::config::Config; -use crate::input::state::{DrawingStyle, HistoryLimits}; +use crate::input::state::{DrawingStyle, HistoryLimits, UiVisibility}; use crate::input::{ClickHighlightSettings, InputHudSettings, InputState}; +use crate::render_profiles::RenderProfileSet; use super::core::{CanvasIndex, InputStateSeed, Keymap}; impl InputState { /// Build runtime input state from validated application configuration. + #[must_use] pub fn from_config(config: &Config) -> Self { let style = DrawingStyle::from((&config.drawing, &config.arrow, &config.spotlight)); let keymap = @@ -17,7 +19,7 @@ impl InputState { let mut input_state = InputState::from_seed(InputStateSeed { style, - ui_visibility: crate::input::state::UiVisibility::from(&config.ui), + ui_visibility: UiVisibility::from(&config.ui), boards_config: config.resolved_boards(), keymap, canvas_index, @@ -27,9 +29,7 @@ impl InputState { }); input_state.init_input_hud_from_config(InputHudSettings::from(&config.ui.input_hud)); input_state.init_laser_from_config(&config.laser); - input_state.set_render_profiles(crate::render_profiles::RenderProfileSet::from_config( - &config.render_profiles, - )); + input_state.set_render_profiles(RenderProfileSet::from_config(&config.render_profiles)); input_state.set_hit_test_threshold(config.drawing.hit_test_linear_threshold); input_state.set_shape_recognition_feedback(config.drawing.shape_recognition_feedback); @@ -60,6 +60,11 @@ impl InputState { #[cfg(test)] mod tests { + use crate::config::ColorSpec; + use crate::input::{ + DragBindableTool, DragBinding, DragButtonBindings, DragTool, DragToolBindings, Tool, + }; + use super::*; use crate::config::KeybindingsConfig; @@ -148,25 +153,25 @@ mod tests { #[test] fn build_input_state_applies_drag_tool_bindings() { let mut config = Config::default(); - config.drawing.drag_tool = crate::input::DragBindableTool::Arrow; - config.drawing.shift_drag_tool = crate::input::DragBindableTool::Eraser; - config.drawing.ctrl_drag_tool = crate::input::DragBindableTool::Pen; - config.drawing.ctrl_shift_drag_tool = crate::input::DragBindableTool::Rect; - config.drawing.tab_drag_tool = crate::input::DragBindableTool::Ellipse; + config.drawing.drag_tool = DragBindableTool::Arrow; + config.drawing.shift_drag_tool = DragBindableTool::Eraser; + config.drawing.ctrl_drag_tool = DragBindableTool::Pen; + config.drawing.ctrl_shift_drag_tool = DragBindableTool::Rect; + config.drawing.tab_drag_tool = DragBindableTool::Ellipse; let input = InputState::from_config(&config); assert_eq!( input.drag_tool_bindings(), - crate::input::DragToolBindings { - left: crate::input::DragButtonBindings { - drag: crate::input::DragBinding::from_tool(crate::input::Tool::Arrow), - shift_drag: crate::input::DragBinding::from_tool(crate::input::Tool::Eraser), - ctrl_drag: crate::input::DragBinding::from_tool(crate::input::Tool::Pen), - ctrl_shift_drag: crate::input::DragBinding::from_tool(crate::input::Tool::Rect,), - tab_drag: crate::input::DragBinding::from_tool(crate::input::Tool::Ellipse), + DragToolBindings { + left: DragButtonBindings { + drag: DragBinding::from_tool(Tool::Arrow), + shift_drag: DragBinding::from_tool(Tool::Eraser), + ctrl_drag: DragBinding::from_tool(Tool::Pen), + ctrl_shift_drag: DragBinding::from_tool(Tool::Rect,), + tab_drag: DragBinding::from_tool(Tool::Ellipse), }, - right: crate::input::DragButtonBindings::button_default(), - middle: crate::input::DragButtonBindings::button_default(), + right: DragButtonBindings::button_default(), + middle: DragButtonBindings::button_default(), }, ); } @@ -175,24 +180,18 @@ mod tests { fn build_input_state_applies_mouse_button_drag_tool_bindings() { let mut config = Config::default(); let mut drag_tools = config.drawing.effective_drag_tools(); - drag_tools.left.drag_tool = crate::input::DragTool::Line; - drag_tools.right.drag_tool = crate::input::DragTool::Pen; - drag_tools.right.drag_color = Some(crate::config::ColorSpec::Name("blue".to_string())); + drag_tools.left.drag_tool = DragTool::Line; + drag_tools.right.drag_tool = DragTool::Pen; + drag_tools.right.drag_color = Some(ColorSpec::Name("blue".to_string())); config.drawing.drag_tools = Some(drag_tools); let input = InputState::from_config(&config); - assert_eq!( - input.drag_tool_bindings().left.drag.tool, - crate::input::DragTool::Line - ); - assert_eq!( - input.drag_tool_bindings().right.drag.tool, - crate::input::DragTool::Pen - ); + assert_eq!(input.drag_tool_bindings().left.drag.tool, DragTool::Line); + assert_eq!(input.drag_tool_bindings().right.drag.tool, DragTool::Pen); assert_eq!( input.drag_tool_bindings().right.drag.color, - Some(crate::config::ColorSpec::Name("blue".to_string()).to_color()) + Some(ColorSpec::Name("blue".to_string()).to_color()) ); } } diff --git a/src/input/state/highlight/settings.rs b/src/input/state/highlight/settings.rs index 2793c81c8..dea8c54e7 100644 --- a/src/input/state/highlight/settings.rs +++ b/src/input/state/highlight/settings.rs @@ -21,6 +21,7 @@ pub struct ClickHighlightSettings { impl ClickHighlightSettings { #[cfg_attr(not(test), allow(dead_code))] + #[must_use] pub fn disabled() -> Self { let base_fill = Color { r: 1.0, diff --git a/src/input/state/highlight/state/mod.rs b/src/input/state/highlight/state/mod.rs index c051a7a4d..c0addce04 100644 --- a/src/input/state/highlight/state/mod.rs +++ b/src/input/state/highlight/state/mod.rs @@ -1,5 +1,6 @@ use std::time::Instant; +use crate::draw; use crate::draw::{Color, DirtyTracker}; use crate::util::Rect; @@ -192,7 +193,7 @@ impl ClickHighlightState { let progress = (elapsed.as_secs_f64() / total).clamp(0.0, 1.0); let fade = (1.0 - progress).clamp(0.0, 1.0); - crate::draw::render_click_highlight( + draw::render_click_highlight( ctx, highlight.x as f64, highlight.y as f64, @@ -210,7 +211,7 @@ impl ClickHighlightState { return; } - crate::draw::render_click_highlight( + draw::render_click_highlight( ctx, x as f64, y as f64, diff --git a/src/input/state/input_hud/label.rs b/src/input/state/input_hud/label.rs index ece6097b6..384cae745 100644 --- a/src/input/state/input_hud/label.rs +++ b/src/input/state/input_hud/label.rs @@ -5,6 +5,7 @@ //! other surface uses, so `ArrowUp` is the glyph on the HUD exactly as it is //! in the help overlay. +use crate::config::keybindings; use crate::input::events::Key; use crate::input::modifiers::Modifiers; @@ -29,6 +30,7 @@ pub(crate) fn modifier_prefix(modifiers: Modifiers) -> String { /// Whether a key is a bare modifier press (its own chip only when /// `show_bare_modifiers` is on). +#[must_use] pub fn is_bare_modifier(key: Key) -> bool { matches!(key, Key::Shift | Key::Ctrl | Key::Alt | Key::Super) } @@ -82,12 +84,13 @@ pub(crate) fn key_display_name(key: Key) -> Option { Key::F12 => "F12", Key::Unknown => return None, }; - Some(crate::config::keybindings::key_display_name(name).to_string()) + Some(keybindings::key_display_name(name).to_string()) } /// Chord label for a key press: the held modifiers in canonical order plus the /// key's display name. A bare modifier press reports only itself, so holding /// Ctrl never renders as `Ctrl+Ctrl`. +#[must_use] pub fn input_hud_key_label(key: Key, modifiers: Modifiers) -> Option { let name = key_display_name(key)?; if is_bare_modifier(key) { @@ -97,11 +100,13 @@ pub fn input_hud_key_label(key: Key, modifiers: Modifiers) -> Option { } /// Chord label for a pointer button press. +#[must_use] pub fn input_hud_mouse_label(button: &str, modifiers: Modifiers) -> String { format!("{}{}", modifier_prefix(modifiers), button) } /// Chord label for a scroll tick. +#[must_use] pub fn input_hud_scroll_label(up: bool, modifiers: Modifiers) -> String { let arrow = if up { "\u{2191}" } else { "\u{2193}" }; format!("{}Scroll {}", modifier_prefix(modifiers), arrow) @@ -111,36 +116,49 @@ pub fn input_hud_scroll_label(up: bool, modifiers: Modifiers) -> String { mod tests { use super::*; - fn mods(ctrl: bool, shift: bool, alt: bool, logo: bool) -> Modifiers { - Modifiers { - shift, - ctrl, - alt, - logo, - tab: false, - } - } + const NONE: Modifiers = Modifiers { + shift: false, + ctrl: false, + alt: false, + logo: false, + tab: false, + }; + const CTRL: Modifiers = Modifiers { ctrl: true, ..NONE }; + const SHIFT: Modifiers = Modifiers { + shift: true, + ..NONE + }; + const ALT: Modifiers = Modifiers { alt: true, ..NONE }; + const SUPER: Modifiers = Modifiers { logo: true, ..NONE }; + const CTRL_SHIFT: Modifiers = Modifiers { + ctrl: true, + ..SHIFT + }; + const SHIFT_SUPER: Modifiers = Modifiers { + logo: true, + ..SHIFT + }; #[test] fn chord_labels_use_the_canonical_modifier_order() { assert_eq!( - input_hud_key_label(Key::Char('z'), mods(true, true, false, false)).as_deref(), + input_hud_key_label(Key::Char('z'), CTRL_SHIFT).as_deref(), Some("Ctrl+Shift+Z") ); assert_eq!( - input_hud_key_label(Key::F10, mods(false, false, true, false)).as_deref(), + input_hud_key_label(Key::F10, ALT).as_deref(), Some("Alt+F10") ); assert_eq!( - input_hud_key_label(Key::Char('a'), mods(false, false, false, false)).as_deref(), + input_hud_key_label(Key::Char('a'), NONE).as_deref(), Some("A") ); assert_eq!( - input_hud_key_label(Key::Char('x'), mods(false, false, false, true)).as_deref(), + input_hud_key_label(Key::Char('x'), SUPER).as_deref(), Some("Super+X") ); assert_eq!( - input_hud_key_label(Key::F5, mods(false, true, false, true)).as_deref(), + input_hud_key_label(Key::F5, SHIFT_SUPER).as_deref(), Some("Shift+Super+F5") ); } @@ -148,19 +166,19 @@ mod tests { #[test] fn special_keys_use_the_shared_display_names_and_arrow_glyphs() { assert_eq!( - input_hud_key_label(Key::Space, mods(false, false, false, false)).as_deref(), + input_hud_key_label(Key::Space, NONE).as_deref(), Some("Space") ); assert_eq!( - input_hud_key_label(Key::Escape, mods(false, false, false, false)).as_deref(), + input_hud_key_label(Key::Escape, NONE).as_deref(), Some("Esc") ); assert_eq!( - input_hud_key_label(Key::Up, mods(false, false, false, false)).as_deref(), + input_hud_key_label(Key::Up, NONE).as_deref(), Some("\u{2191}") ); assert_eq!( - input_hud_key_label(Key::Left, mods(false, false, false, false)).as_deref(), + input_hud_key_label(Key::Left, NONE).as_deref(), Some("\u{2190}") ); // The chip agrees with what a binding on the same key would show. @@ -171,53 +189,42 @@ mod tests { (Key::Return, "Return"), ] { assert_eq!( - input_hud_key_label(key, mods(false, false, false, false)).as_deref(), - Some(crate::config::keybindings::key_display_name(name)) + input_hud_key_label(key, NONE).as_deref(), + Some(keybindings::key_display_name(name)) ); } // Tab is a HUD-only chip: no binding can carry it, so it stays a word. - assert_eq!( - input_hud_key_label(Key::Tab, mods(false, false, false, false)).as_deref(), - Some("Tab") - ); + assert_eq!(input_hud_key_label(Key::Tab, NONE).as_deref(), Some("Tab")); } #[test] fn bare_modifier_presses_do_not_prefix_themselves() { assert_eq!( - input_hud_key_label(Key::Ctrl, mods(true, false, false, false)).as_deref(), + input_hud_key_label(Key::Ctrl, CTRL).as_deref(), Some("Ctrl") ); assert_eq!( - input_hud_key_label(Key::Shift, mods(true, true, false, false)).as_deref(), + input_hud_key_label(Key::Shift, CTRL_SHIFT).as_deref(), Some("Shift") ); assert_eq!( - input_hud_key_label(Key::Super, mods(false, false, false, true)).as_deref(), + input_hud_key_label(Key::Super, SUPER).as_deref(), Some("Super") ); } #[test] fn unmapped_keys_are_skipped() { - assert!(input_hud_key_label(Key::Unknown, mods(false, false, false, false)).is_none()); - assert!( - input_hud_key_label(Key::Char('\u{1}'), mods(false, false, false, false)).is_none() - ); + assert!(input_hud_key_label(Key::Unknown, NONE).is_none()); + assert!(input_hud_key_label(Key::Char('\u{1}'), NONE).is_none()); } #[test] fn mouse_and_scroll_labels_carry_the_modifier_prefix() { + assert_eq!(input_hud_mouse_label("Click", CTRL), "Ctrl+Click"); + assert_eq!(input_hud_scroll_label(true, NONE), "Scroll \u{2191}"); assert_eq!( - input_hud_mouse_label("Click", mods(true, false, false, false)), - "Ctrl+Click" - ); - assert_eq!( - input_hud_scroll_label(true, mods(false, false, false, false)), - "Scroll \u{2191}" - ); - assert_eq!( - input_hud_scroll_label(false, mods(false, true, false, false)), + input_hud_scroll_label(false, SHIFT), "Shift+Scroll \u{2193}" ); } diff --git a/src/input/state/input_hud/state/mod.rs b/src/input/state/input_hud/state/mod.rs index d3ce0c514..bf0703f07 100644 --- a/src/input/state/input_hud/state/mod.rs +++ b/src/input/state/input_hud/state/mod.rs @@ -1,5 +1,7 @@ use std::collections::VecDeque; -use std::time::Instant; +use std::collections::vec_deque::Iter; +use std::mem; +use std::time::{Duration, Instant}; use crate::config::{InputHudMode, InputHudPosition}; use crate::draw::DirtyTracker; @@ -43,14 +45,17 @@ pub struct InputHudEntry { } impl InputHudEntry { + #[must_use] pub fn label(&self) -> &str { &self.label } + #[must_use] pub fn kind(&self) -> InputHudEntryKind { self.kind } + #[must_use] pub fn count(&self) -> u32 { self.count } @@ -77,6 +82,7 @@ pub struct InputHudState { } impl InputHudState { + #[must_use] pub fn new(settings: InputHudSettings) -> Self { let enabled = settings.enabled; Self { @@ -97,22 +103,27 @@ impl InputHudState { self.entries.clear(); } + #[must_use] pub fn enabled(&self) -> bool { self.enabled } + #[must_use] pub fn configured_mode(&self) -> InputHudMode { self.settings.mode } + #[must_use] pub fn position(&self) -> InputHudPosition { self.settings.position } + #[must_use] pub fn font_size(&self) -> f64 { self.settings.font_size } + #[must_use] pub fn active_source(&self) -> InputHudActiveSource { self.source } @@ -127,16 +138,19 @@ impl InputHudState { true } + #[must_use] pub fn has_entries(&self) -> bool { !self.entries.is_empty() } - pub fn entries(&self) -> std::collections::vec_deque::Iter<'_, InputHudEntry> { + #[must_use] + pub fn entries(&self) -> Iter<'_, InputHudEntry> { self.entries.iter() } /// Fade factor for one chip: fully opaque for `display`, then a linear /// ramp to zero over `fade`. + #[must_use] pub fn alpha(&self, entry: &InputHudEntry, now: Instant) -> f64 { let elapsed = now.saturating_duration_since(entry.refreshed_at); if elapsed <= self.settings.display { @@ -165,6 +179,7 @@ impl InputHudState { } /// Whether a source announcement is waiting, without consuming it. + #[must_use] pub fn has_source_announce(&self) -> bool { self.announce_source_pending } @@ -174,7 +189,7 @@ impl InputHudState { /// has reconciled the reader thread, so the announcement can never claim /// a source the HUD does not actually have. pub fn take_source_announce(&mut self) -> bool { - std::mem::take(&mut self.announce_source_pending) + mem::take(&mut self.announce_source_pending) } /// Force the HUD to a given enabled state (presenter mode entry/exit). @@ -281,7 +296,7 @@ impl InputHudState { let fade = self.settings.fade; self.entries.retain(|entry| { let elapsed = now.saturating_duration_since(entry.refreshed_at); - elapsed <= display || (fade > std::time::Duration::ZERO && elapsed < display + fade) + elapsed <= display || (fade > Duration::ZERO && elapsed < display + fade) }); !self.entries.is_empty() } diff --git a/src/input/state/interaction/actions.rs b/src/input/state/interaction/actions.rs index 156761c87..1af6bbae1 100644 --- a/src/input/state/interaction/actions.rs +++ b/src/input/state/interaction/actions.rs @@ -1,3 +1,5 @@ +use crate::input::state::InputTextResources; + use super::adapters; use super::outcome::{ActionRoute, RoutingOutcome}; use crate::domain::Action; @@ -190,7 +192,7 @@ pub(crate) fn classify_action(action: Action) -> ActionRoute { /// press, which is most of them, going around it. pub(crate) fn route_action_with_resources( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, action: Action, ) -> RoutingOutcome { // A wheel burst owns a snapshot from the frame where it started. Every diff --git a/src/input/state/interaction/adapters/actions.rs b/src/input/state/interaction/adapters/actions.rs index d5ac4aee2..8506fde81 100644 --- a/src/input/state/interaction/adapters/actions.rs +++ b/src/input/state/interaction/adapters/actions.rs @@ -1,3 +1,5 @@ +use crate::input::state::InputTextResources; + use super::super::outcome::ActionRoute; use crate::domain::Action; use crate::input::state::InputState; @@ -8,7 +10,7 @@ pub(crate) fn close_properties_panel_before_action(state: &mut InputState) { pub(crate) fn dispatch_action( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, action: Action, route: ActionRoute, ) { diff --git a/src/input/state/interaction/adapters/active_motion.rs b/src/input/state/interaction/adapters/active_motion.rs index 688f92d66..2db2597c5 100644 --- a/src/input/state/interaction/adapters/active_motion.rs +++ b/src/input/state/interaction/adapters/active_motion.rs @@ -1,3 +1,5 @@ +use crate::draw::{TextMeasurer, shape}; + use super::super::event::PointerPoints; use super::super::outcome::{ ActiveInteractionKind, InteractionSideEffect, NoRouteReason, PointerSideEffect, RoutingOutcome, @@ -10,7 +12,7 @@ use std::sync::Arc; pub(crate) fn handle_active_motion( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, points: PointerPoints, ) -> Option { let canvas = points.canvas(); @@ -176,7 +178,7 @@ pub(crate) fn handle_active_motion( pub(crate) fn handle_drawing_or_idle_motion( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, points: PointerPoints, ) -> RoutingOutcome { let canvas = points.canvas(); @@ -195,7 +197,7 @@ pub(crate) fn handle_drawing_or_idle_motion( { if let Some(thickness) = sample_size { if !matches!(tool, Tool::Pen | Tool::Marker | Tool::LiveShape) - || crate::draw::shape::keep_stroke_sample(points, canvas.position()) + || shape::keep_stroke_sample(points, canvas.position()) { points.push(canvas.position()); point_thicknesses.push(thickness as f32); diff --git a/src/input/state/interaction/adapters/keyboard.rs b/src/input/state/interaction/adapters/keyboard.rs index dc5bc7f73..fb16eae09 100644 --- a/src/input/state/interaction/adapters/keyboard.rs +++ b/src/input/state/interaction/adapters/keyboard.rs @@ -1,3 +1,6 @@ +use crate::draw::TextMeasurer; +use crate::input::state::InputTextResources; + use super::super::active::active_interaction_kind; use super::super::outcome::{ ActiveInteractionKind, CancelTarget, ConsumedBy, InteractionSideEffect, KeyboardSideEffect, @@ -12,7 +15,7 @@ use crate::input::state::{DrawingState, InputState}; pub(crate) fn handle_command_palette_key( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, ) -> Option { (state.command_palette_is_engaged() @@ -57,7 +60,7 @@ pub(crate) fn handle_radial_menu_key(state: &mut InputState, key: Key) -> Option pub(crate) fn handle_precision_entry_key( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, ) -> Option { (state.is_precision_entry_open() @@ -74,7 +77,7 @@ pub(crate) fn handle_color_picker_key(state: &mut InputState, key: Key) -> Optio /// produced rather than only the key itself. pub(crate) fn handle_font_picker_key( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, text: Option<&str>, ) -> Option { @@ -84,7 +87,7 @@ pub(crate) fn handle_font_picker_key( pub(crate) fn handle_context_menu_key( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, ) -> Option { (state.is_context_menu_open() && state.handle_context_menu_key_with_resources(resources, key)) @@ -93,7 +96,7 @@ pub(crate) fn handle_context_menu_key( pub(crate) fn handle_board_picker_key( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, ) -> Option { (state.is_board_picker_open() && state.handle_board_picker_key_with_measurer(measurer, key)) @@ -111,7 +114,7 @@ pub(crate) fn handle_global_modifier_key( pub(crate) fn handle_properties_panel_key( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, ) -> Option { if !state.is_properties_panel_open() { @@ -159,7 +162,7 @@ pub(crate) fn handle_pending_delete_cancel_key( pub(crate) fn handle_idle_selection_cancel_key( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, ) -> Option { if matches!(key, Key::Escape) @@ -175,7 +178,7 @@ pub(crate) fn handle_idle_selection_cancel_key( pub(crate) fn handle_text_input_key( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, ) -> Option { if matches!(&state.state, DrawingState::TextInput { .. }) { @@ -188,7 +191,7 @@ pub(crate) fn handle_text_input_key( pub(crate) fn handle_building_polygon_key( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, ) -> Option { if !matches!(state.state, DrawingState::BuildingPolygon { .. }) { @@ -220,7 +223,7 @@ pub(crate) fn handle_building_polygon_key( pub(crate) fn handle_drawing_escape_cancel_key( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, ) -> Option { if matches!(key, Key::Escape) @@ -259,7 +262,7 @@ pub(crate) fn action_for_key_binding( pub(crate) fn handle_return_edit_selected_text_key( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, key: Key, ) -> Option { if matches!(key, Key::Return) diff --git a/src/input/state/interaction/adapters/pointer.rs b/src/input/state/interaction/adapters/pointer.rs index b9b34923b..4168e5a31 100644 --- a/src/input/state/interaction/adapters/pointer.rs +++ b/src/input/state/interaction/adapters/pointer.rs @@ -1,3 +1,6 @@ +use crate::draw::TextMeasurer; +use crate::input::state::InputTextResources; + use super::super::active::active_interaction_kind; use super::super::event::PointerPoints; use super::super::outcome::{ @@ -17,7 +20,7 @@ pub(crate) fn update_pointer_positions(state: &mut InputState, points: PointerPo pub(crate) fn handle_radial_menu_press( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, button: MouseButton, points: PointerPoints, ) -> Option { @@ -37,7 +40,7 @@ pub(crate) fn handle_radial_menu_press( pub(crate) fn handle_building_polygon_non_left_press( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, button: MouseButton, points: PointerPoints, ) -> Option { @@ -92,7 +95,7 @@ pub(crate) fn handle_board_picker_press( pub(crate) fn handle_properties_panel_press( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, button: MouseButton, points: PointerPoints, ) -> Option { @@ -175,7 +178,7 @@ pub(crate) fn close_properties_panel_before_tool_routing(state: &mut InputState) pub(crate) fn handle_tool_button_press( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, button: MouseButton, points: PointerPoints, ) -> Option { @@ -216,7 +219,7 @@ pub(crate) fn handle_tool_button_press( pub(crate) fn handle_unbound_left_press( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, points: PointerPoints, ) -> RoutingOutcome { let screen = points.screen(); @@ -262,7 +265,7 @@ pub(crate) fn handle_unbound_left_press( pub(crate) fn handle_right_press( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, points: PointerPoints, ) -> RoutingOutcome { let screen = points.screen(); @@ -308,7 +311,7 @@ pub(crate) fn handle_middle_press(state: &mut InputState, points: PointerPoints) fn open_context_menu_from_right_click( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, screen_x: i32, screen_y: i32, canvas_x: i32, @@ -348,7 +351,7 @@ fn open_context_menu_from_right_click( state.update_context_menu_hover_from_pointer(screen_x, screen_y); if focus_edit { - state.focus_context_menu_command(MenuCommand::EditText); + state.focus_context_menu_command(&MenuCommand::EditText); } if state.is_context_menu_open() { state.pending_onboarding_usage.used_context_menu_right_click = true; @@ -358,7 +361,7 @@ fn open_context_menu_from_right_click( pub(crate) fn handle_radial_menu_motion( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, points: PointerPoints, ) -> Option { if !state.is_radial_menu_open() { @@ -413,7 +416,7 @@ pub(crate) fn handle_font_picker_motion( pub(crate) fn handle_font_picker_press( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, button: MouseButton, points: PointerPoints, ) -> Option { @@ -458,7 +461,7 @@ pub(crate) fn handle_board_picker_motion( pub(crate) fn handle_properties_panel_motion( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, points: PointerPoints, ) -> Option { if !state.is_properties_panel_open() { @@ -485,7 +488,7 @@ pub(crate) fn handle_context_menu_motion( pub(crate) fn handle_release_overlays( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, button: MouseButton, points: PointerPoints, ) -> Option { @@ -514,7 +517,7 @@ pub(crate) fn handle_release_overlays( pub(crate) fn handle_radial_menu_release( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, button: MouseButton, points: PointerPoints, ) -> Option { @@ -531,7 +534,7 @@ pub(crate) fn handle_radial_menu_release( pub(crate) fn finish_pointer_interaction( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, points: PointerPoints, ) { let canvas = points.canvas(); diff --git a/src/input/state/interaction/keyboard.rs b/src/input/state/interaction/keyboard.rs index 01a654885..223ff07d3 100644 --- a/src/input/state/interaction/keyboard.rs +++ b/src/input/state/interaction/keyboard.rs @@ -1,3 +1,5 @@ +use crate::input::state::InputTextResources; + use super::actions::route_action_with_resources; use super::adapters; use super::outcome::{ConsumedBy, NoRouteReason, RoutingOutcome}; @@ -12,7 +14,7 @@ use super::super::core::SequenceMatch; pub(crate) fn route_key_press_with_resources( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, ) -> RoutingOutcome { route_key_event(state, resources, key, false) @@ -20,7 +22,7 @@ pub(crate) fn route_key_press_with_resources( pub(crate) fn route_key_repeat_with_resources( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, ) -> RoutingOutcome { route_key_event(state, resources, key, true) @@ -28,7 +30,7 @@ pub(crate) fn route_key_repeat_with_resources( fn route_key_event( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, is_repeat: bool, ) -> RoutingOutcome { @@ -54,7 +56,7 @@ fn route_key_event( fn route_key_event_inner( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, key: Key, is_repeat: bool, ) -> RoutingOutcome { diff --git a/src/input/state/interaction/mod.rs b/src/input/state/interaction/mod.rs index b09f0906b..96f40abc5 100644 --- a/src/input/state/interaction/mod.rs +++ b/src/input/state/interaction/mod.rs @@ -17,6 +17,11 @@ pub(crate) use pointer::{route_pointer_motion, route_pointer_press, route_pointe #[cfg(test)] mod tests { + use crate::draw::{ShapeId, TextMeasurer}; + use crate::input::boards::PendingBoardRuntimeUiAction; + use crate::input::state::{DrawingState, InputState}; + use crate::ui_text::UiTextEngine; + use super::actions::classify_action; use super::active::active_interaction_kind; use super::outcome::{ @@ -26,12 +31,13 @@ mod tests { use super::*; use crate::config::Action; use crate::draw::Shape; - use crate::input::state::{TopMenuState, test_support::make_test_input_state}; + use crate::input::state::TopMenuState; + use crate::input::state::test_support::make_test_input_state; use crate::input::{BOARD_ID_BLACKBOARD, EraserMode, Key, MouseButton, Tool}; - fn route_key_press(state: &mut crate::input::state::InputState, key: Key) -> RoutingOutcome { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + fn route_key_press(state: &mut InputState, key: Key) -> RoutingOutcome { + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); route_key_press_with_resources( state, crate::input::state::InputTextResources { @@ -46,7 +52,7 @@ mod tests { PointerPoints::new(ScreenPoint::new(10, 20), CanvasPoint::new(10, 20)) } - fn add_rect(state: &mut crate::input::state::InputState) -> crate::draw::ShapeId { + fn add_rect(state: &mut InputState) -> ShapeId { state.boards.active_frame_mut().add_shape(Shape::Rect { x: 10, y: 20, @@ -65,7 +71,7 @@ mod tests { assert_eq!(active_interaction_kind(&state), None); - state.state = crate::input::state::DrawingState::Drawing { + state.state = DrawingState::Drawing { tool: Tool::Pen, start_x: 1, start_y: 2, @@ -77,7 +83,7 @@ mod tests { Some(ActiveInteractionKind::Drawing) ); - state.state = crate::input::state::DrawingState::text_input(1, 2, String::new()); + state.state = DrawingState::text_input(1, 2, String::new()); assert_eq!( active_interaction_kind(&state), Some(ActiveInteractionKind::TextInput) @@ -99,7 +105,7 @@ mod tests { #[test] fn properties_panel_unhandled_key_is_consumed() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); let id = add_rect(&mut state); state.set_selection(vec![id]); @@ -200,15 +206,15 @@ mod tests { #[test] fn right_click_cancels_active_interaction_before_context_menu_policy() { - let pointer_measurer = crate::draw::TextMeasurer::default(); - let pointer_ui_engine = crate::ui_text::UiTextEngine::default(); + let pointer_measurer = TextMeasurer::default(); + let pointer_ui_engine = UiTextEngine::default(); let pointer_resources = crate::input::state::InputTextResources { measurer: &pointer_measurer, ui_engine: &pointer_ui_engine, }; let mut state = make_test_input_state(); - state.state = crate::input::state::DrawingState::Drawing { + state.state = DrawingState::Drawing { tool: Tool::Pen, start_x: 1, start_y: 2, @@ -227,10 +233,7 @@ mod tests { ActiveInteractionKind::Drawing )) ); - assert!(matches!( - state.state, - crate::input::state::DrawingState::Idle - )); + assert!(matches!(state.state, DrawingState::Idle)); assert!(!state.pointer_drag_active()); } @@ -238,8 +241,8 @@ mod tests { /// view like anywhere else. #[test] fn right_click_opens_the_context_menu_while_zoomed() { - let pointer_measurer = crate::draw::TextMeasurer::default(); - let pointer_ui_engine = crate::ui_text::UiTextEngine::default(); + let pointer_measurer = TextMeasurer::default(); + let pointer_ui_engine = UiTextEngine::default(); let pointer_resources = crate::input::state::InputTextResources { measurer: &pointer_measurer, ui_engine: &pointer_ui_engine, @@ -260,8 +263,8 @@ mod tests { #[test] fn right_click_suppression_paths_return_named_side_effects() { - let pointer_measurer = crate::draw::TextMeasurer::default(); - let pointer_ui_engine = crate::ui_text::UiTextEngine::default(); + let pointer_measurer = TextMeasurer::default(); + let pointer_ui_engine = UiTextEngine::default(); let pointer_resources = crate::input::state::InputTextResources { measurer: &pointer_measurer, ui_engine: &pointer_ui_engine, @@ -283,8 +286,8 @@ mod tests { #[test] fn radial_menu_release_is_consumed() { - let pointer_measurer = crate::draw::TextMeasurer::default(); - let pointer_ui_engine = crate::ui_text::UiTextEngine::default(); + let pointer_measurer = TextMeasurer::default(); + let pointer_ui_engine = UiTextEngine::default(); let pointer_resources = crate::input::state::InputTextResources { measurer: &pointer_measurer, ui_engine: &pointer_ui_engine, @@ -306,7 +309,7 @@ mod tests { #[test] fn idle_eraser_hover_returns_named_pointer_side_effect() { - let pointer_measurer = crate::draw::TextMeasurer::default(); + let pointer_measurer = TextMeasurer::default(); let mut state = make_test_input_state(); state.style.eraser_mode = EraserMode::Stroke; @@ -340,8 +343,8 @@ mod tests { #[test] fn dismissing_picker_menus_consumes_release_without_activating_the_picker() { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, @@ -433,8 +436,8 @@ mod tests { fn board_picker_row_menu_gets_hover_and_clicks_before_the_picker() { use crate::input::state::core::ContextMenuState; - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, @@ -474,7 +477,7 @@ mod tests { (menu.origin_y + menu.height) as i32, ); // Entries: the board name, Edit Paper, Rename Board, then Pin Board. - let entry_at = |state: &crate::input::state::InputState, index: usize| { + let entry_at = |state: &InputState, index: usize| { let y = (menu_y..menu_bottom) .find(|&y| state.context_menu_index_at(menu_x, y) == Some(index)) .unwrap(); @@ -517,7 +520,7 @@ mod tests { assert!(state.is_board_picker_open() && !state.is_context_menu_open()); assert!(matches!( state.take_pending_board_runtime_ui_actions().as_slice(), - [crate::input::boards::PendingBoardRuntimeUiAction::TogglePin { board_id, .. }] + [PendingBoardRuntimeUiAction::TogglePin { board_id, .. }] if board_id == BOARD_ID_BLACKBOARD )); } diff --git a/src/input/state/interaction/outcome.rs b/src/input/state/interaction/outcome.rs index b7119c609..2ed8eabb3 100644 --- a/src/input/state/interaction/outcome.rs +++ b/src/input/state/interaction/outcome.rs @@ -1,3 +1,5 @@ +use crate::input::InputState; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum RoutingOutcome { Consumed(ConsumedBy), @@ -98,7 +100,7 @@ pub(crate) enum ActionRoute { } impl RoutingOutcome { - pub(crate) fn owns_pointer_motion(self, state: &crate::input::InputState) -> bool { + pub(crate) fn owns_pointer_motion(self, state: &InputState) -> bool { match self { Self::Started(_) | Self::Continued(_) => state.has_active_pointer_interaction(), Self::Consumed(ConsumedBy::RadialMenu | ConsumedBy::RadialMenuToggle) => { @@ -117,7 +119,7 @@ impl RoutingOutcome { /// A popup button can own a release without owning motion. Its ownership /// ends if that popup closes, so a late Up cannot finish a later stroke. - pub(crate) fn owns_pointer_release(self, state: &crate::input::InputState) -> bool { + pub(crate) fn owns_pointer_release(self, state: &InputState) -> bool { match self { Self::Started(_) | Self::Continued(_) => state.has_active_pointer_interaction(), Self::Consumed(ConsumedBy::RadialMenu | ConsumedBy::RadialMenuToggle) => { diff --git a/src/input/state/interaction/pointer.rs b/src/input/state/interaction/pointer.rs index 966c70669..88b5d699e 100644 --- a/src/input/state/interaction/pointer.rs +++ b/src/input/state/interaction/pointer.rs @@ -1,3 +1,6 @@ +use crate::draw::TextMeasurer; +use crate::input::state::{InputTextResources, ZoomAnchor}; + use super::adapters; use super::event::{PointerMotion, PointerPress, PointerRelease}; use super::outcome::{ConsumedBy, NoRouteReason, RoutingOutcome}; @@ -7,7 +10,7 @@ use crate::ui::ZoomChipPress; pub(crate) fn route_pointer_press( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, event: PointerPress, ) -> RoutingOutcome { let points = event.points(); @@ -88,7 +91,7 @@ pub(crate) fn route_pointer_press( pub(crate) fn route_pointer_motion( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, event: PointerMotion, ) -> RoutingOutcome { let points = event.points(); @@ -131,7 +134,7 @@ pub(crate) fn route_pointer_motion( pub(crate) fn route_pointer_release( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, event: PointerRelease, ) -> RoutingOutcome { let points = event.points(); @@ -179,11 +182,7 @@ pub(crate) fn route_pointer_release( let screen = points.screen(); let (_, action) = state.check_zoom_chip_click(kind, screen.x(), screen.y()); if let Some(action) = action { - state.handle_action_anchored( - resources, - action, - crate::input::state::ZoomAnchor::ScreenCenter, - ); + state.handle_action_anchored(resources, action, ZoomAnchor::ScreenCenter); } } state.needs_redraw = true; diff --git a/src/input/state/laser/mod.rs b/src/input/state/laser/mod.rs index 0dcc4d2e0..075bfb818 100644 --- a/src/input/state/laser/mod.rs +++ b/src/input/state/laser/mod.rs @@ -12,6 +12,7 @@ use std::collections::VecDeque; use std::time::{Duration, Instant}; use crate::config::LaserConfig; +use crate::draw; use crate::draw::{Color, DirtyTracker, LaserStyle}; use crate::util::Rect; @@ -211,7 +212,7 @@ impl LaserInk { /// Paints every finished stroke at the opacity the last advance chose. pub(crate) fn render(&self, ctx: &cairo::Context) { for stroke in &self.strokes { - crate::draw::render_laser_stroke( + draw::render_laser_stroke( ctx, &stroke.points, self.settings.style, diff --git a/src/input/state/mod.rs b/src/input/state/mod.rs index 4a323243f..56f4bca25 100644 --- a/src/input/state/mod.rs +++ b/src/input/state/mod.rs @@ -9,14 +9,12 @@ mod mouse; mod render; mod spotlight; mod text_resources; -pub(crate) use core::{ClipboardFallbackSaveRequest, InputEffect, InputEffectDrain}; pub(crate) use core::{ - DrawingStyle, HistoryLimits, IdleHandle, SpotlightMagnificationTrack, TopMenuState, -}; -pub(crate) use core::{ - LevelRange, PanelAction, PanelRect, PanelScroll, PropertiesPanelHit, PropertiesPanelLayout, - PropertiesPanelLock, PropertiesPanelSwatch, PropertiesRowControl, PropertiesRowGeometry, - ShapePropertiesPanel, properties_panel_metrics, + ClipboardFallbackSaveRequest, DrawingStyle, HistoryLimits, IdleHandle, InputEffect, + InputEffectDrain, LevelRange, PanelAction, PanelRect, PanelScroll, PropertiesPanelHit, + PropertiesPanelLayout, PropertiesPanelLock, PropertiesPanelSwatch, PropertiesRowControl, + PropertiesRowGeometry, ShapePropertiesPanel, SpotlightMagnificationTrack, TopMenuState, + properties_panel_metrics, }; pub(in crate::input::state) use spotlight::SpotlightWheelGesture; pub(crate) use spotlight::{SpotlightFrameRegions, SpotlightWheelClaim, SpotlightWheelOutcome}; @@ -26,11 +24,10 @@ mod tests; #[cfg(test)] pub(crate) use actions::key_press::bindings::key_to_action_label_for_test; -pub(crate) use core::board_picker::BoardPickerEditMode; -pub(crate) use core::board_picker::BoardPickerFocus; pub(crate) use core::board_picker::appearance::AppearanceField; pub(crate) use core::board_picker::{ - PAGE_DELETE_ICON_MARGIN, PAGE_DELETE_ICON_SIZE, PAGE_NAME_HEIGHT, PAGE_NAME_PADDING, + BoardPickerEditMode, BoardPickerFocus, PAGE_DELETE_ICON_MARGIN, PAGE_DELETE_ICON_SIZE, + PAGE_NAME_HEIGHT, PAGE_NAME_PADDING, }; #[cfg(test)] pub(crate) use core::build_text_input_preview; @@ -87,6 +84,13 @@ pub use input_hud::{ #[cfg(test)] pub(crate) mod test_support { + use crate::config::{ + ArrowConfig, DrawingConfig, HistoryConfig, MouseDragToolsConfig, SpotlightConfig, UiConfig, + }; + use crate::draw; + use crate::input::PerToolDrawingSettings; + use crate::input::tool::ToolSettingsSlot; + use super::core::{CanvasIndex, InputStateSeed, Keymap}; use crate::config::{Action, BoardsConfig, KeybindingsConfig, PresenterModeConfig, Shortcut}; use crate::draw::FontDescriptor; @@ -99,22 +103,19 @@ pub(crate) mod test_support { impl Default for TestInputStateBuilder { fn default() -> Self { - Self::with_keybindings(KeybindingsConfig::default()) + Self::with_keybindings(&KeybindingsConfig::default()) } } impl TestInputStateBuilder { - pub(crate) fn with_keybindings(keybindings: KeybindingsConfig) -> Self { - let keymap = Keymap::from_config( - &keybindings, - &crate::config::MouseDragToolsConfig::default(), - ); + pub(crate) fn with_keybindings(keybindings: &KeybindingsConfig) -> Self { + let keymap = Keymap::from_config(keybindings, &MouseDragToolsConfig::default()); let mut style = super::DrawingStyle::from(( - &crate::config::DrawingConfig::default(), - &crate::config::ArrowConfig::default(), - &crate::config::SpotlightConfig::default(), + &DrawingConfig::default(), + &ArrowConfig::default(), + &SpotlightConfig::default(), )); - style.current_color = crate::draw::RED; + style.current_color = draw::RED; style.current_thickness = 4.0; style.eraser_size = 4.0; style.marker_opacity = 0.32; @@ -123,13 +124,11 @@ pub(crate) mod test_support { style.arrow_length = 20.0; style.arrow_angle = 30.0; style.arrow_head_at_end = false; - style.tool_settings = crate::input::PerToolDrawingSettings::new( - style.current_color, - style.current_thickness, - ); + style.tool_settings = + PerToolDrawingSettings::new(style.current_color, style.current_thickness); style.tool_settings.step_marker.thickness = super::core::utility::default_step_marker_size(style.current_font_size); - let history_limits = super::HistoryLimits::from(&crate::config::HistoryConfig { + let history_limits = super::HistoryLimits::from(&HistoryConfig { undo_all_delay_ms: 0, redo_all_delay_ms: 0, custom_section_enabled: true, @@ -142,7 +141,7 @@ pub(crate) mod test_support { Self { seed: InputStateSeed { style, - ui_visibility: super::UiVisibility::from(&crate::config::UiConfig::default()), + ui_visibility: super::UiVisibility::from(&UiConfig::default()), boards_config: BoardsConfig::default(), keymap, canvas_index: CanvasIndex::from_config(6.0, usize::MAX), @@ -168,8 +167,8 @@ pub(crate) mod test_support { pub(crate) fn thickness(mut self, thickness: f64) -> Self { self.seed.style.current_thickness = thickness; - for tool in crate::input::tool::ToolSettingsSlot::ALL { - self.seed.style.tool_settings.get_slot_mut(tool).thickness = thickness; + for tool in ToolSettingsSlot::ALL { + self.seed.style.tool_settings.slot_mut(tool).thickness = thickness; } self } diff --git a/src/input/state/mouse/motion.rs b/src/input/state/mouse/motion.rs index 67435bab5..3110ecaf7 100644 --- a/src/input/state/mouse/motion.rs +++ b/src/input/state/mouse/motion.rs @@ -1,6 +1,10 @@ -use super::super::{ - InputState, - interaction::{CanvasPoint, PointerMotion, PointerPoints, ScreenPoint, route_pointer_motion}, +use crate::draw::TextMeasurer; +use crate::input::state::InputTextResources; +use crate::ui_text::UiTextEngine; + +use super::super::InputState; +use super::super::interaction::{ + CanvasPoint, PointerMotion, PointerPoints, ScreenPoint, route_pointer_motion, }; impl InputState { @@ -26,8 +30,8 @@ impl InputState { canvas_x: impl Into, canvas_y: impl Into, ) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.on_mouse_motion_with_canvas_and_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -42,7 +46,7 @@ impl InputState { pub(crate) fn on_mouse_motion_with_canvas_and_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, screen_x: i32, screen_y: i32, canvas_x: impl Into, diff --git a/src/input/state/mouse/press.rs b/src/input/state/mouse/press.rs index 08d870bf5..ceecd92ff 100644 --- a/src/input/state/mouse/press.rs +++ b/src/input/state/mouse/press.rs @@ -1,14 +1,17 @@ -use crate::draw::Shape; -use crate::draw::TextMeasurer; +use crate::config::PresenterToolBehavior; +use crate::draw::{Color, Shape, TextMeasurer}; +use crate::input::events::MouseButton; +use crate::input::state::InputTextResources; use crate::input::tool::ToolPressBehavior; -use crate::input::{DragTool, Tool, events::MouseButton}; +use crate::input::{DragTool, Tool}; +use crate::ui_text::UiTextEngine; use std::sync::Arc; use super::super::core::{IdleHandle, MenuCommand}; -use super::super::{ - ContextMenuKind, DrawingState, InputState, SelectionGrab, - interaction::{CanvasPoint, PointerPoints, PointerPress, ScreenPoint, route_pointer_press}, +use super::super::interaction::{ + CanvasPoint, PointerPoints, PointerPress, ScreenPoint, route_pointer_press, }; +use super::super::{ContextMenuKind, DrawingState, InputState, SelectionGrab}; mod panels; mod polygon; @@ -44,7 +47,7 @@ impl InputState { pub(in crate::input::state) fn handle_right_click_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, screen_x: i32, screen_y: i32, canvas_x: i32, @@ -95,7 +98,7 @@ impl InputState { self.update_context_menu_hover_from_pointer(screen_x, screen_y); if focus_edit { - self.focus_context_menu_command(MenuCommand::EditText); + self.focus_context_menu_command(&MenuCommand::EditText); } if self.is_context_menu_open() { self.pending_onboarding_usage.used_context_menu_right_click = true; @@ -128,8 +131,8 @@ impl InputState { canvas_x: impl Into, canvas_y: impl Into, ) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.on_mouse_press_with_canvas_and_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -145,7 +148,7 @@ impl InputState { pub(crate) fn on_mouse_press_with_canvas_and_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, button: MouseButton, screen_x: i32, screen_y: i32, @@ -175,7 +178,7 @@ impl InputState { && self.presenter_mode_active() && matches!( self.presenter_mode_config().tool_behavior, - crate::config::PresenterToolBehavior::ForceHighlightLocked + PresenterToolBehavior::ForceHighlightLocked ) { // Locked presenting still allows the laser, which leaves nothing @@ -204,10 +207,10 @@ impl InputState { fn handle_tool_button_press_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, button: MouseButton, tool: Tool, - color: Option, + color: Option, coords: PressCoords, ) { self.update_pointer_positions( @@ -273,7 +276,7 @@ impl InputState { measurer: &TextMeasurer, canvas_x: i32, canvas_y: i32, - color: Option, + color: Option, ) -> bool { if !matches!(self.state, DrawingState::TextInput { .. }) { return false; @@ -373,12 +376,7 @@ impl InputState { /// Begin an Alt+left-drag that moves the whole active text block. Records /// the grab offset so the block tracks the cursor exactly under the grabbed /// point, and arms the pointer drag so motion/release route back here. - fn begin_text_block_drag( - &mut self, - canvas_x: i32, - canvas_y: i32, - color: Option, - ) { + fn begin_text_block_drag(&mut self, canvas_x: i32, canvas_y: i32, color: Option) { if self .text_editing .begin_block_drag(&self.state, canvas_x, canvas_y) @@ -414,10 +412,10 @@ impl InputState { pub(in crate::input::state) fn handle_tool_button_press_at_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, button: MouseButton, tool: Tool, - color: Option, + color: Option, screen: (i32, i32), canvas: CanvasPoint, ) { @@ -436,10 +434,10 @@ impl InputState { fn handle_idle_tool_click_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, button: MouseButton, tool: Tool, - color: Option, + color: Option, canvas: CanvasPoint, ) { let (x, y) = (canvas.x(), canvas.y()); diff --git a/src/input/state/mouse/press/panels.rs b/src/input/state/mouse/press/panels.rs index d57d78b19..d5a0661a9 100644 --- a/src/input/state/mouse/press/panels.rs +++ b/src/input/state/mouse/press/panels.rs @@ -1,4 +1,6 @@ +use crate::draw::TextMeasurer; use crate::input::events::MouseButton; +use crate::input::state::InputTextResources; use crate::input::state::core::PickerDrag; use super::super::super::InputState; @@ -29,7 +31,7 @@ impl InputState { pub(in crate::input::state) fn handle_radial_menu_press_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, button: MouseButton, screen_x: i32, screen_y: i32, @@ -185,17 +187,12 @@ impl InputState { x: i32, y: i32, ) -> bool { - self.handle_properties_panel_press_with_measurer( - &crate::draw::TextMeasurer::default(), - button, - x, - y, - ) + self.handle_properties_panel_press_with_measurer(&TextMeasurer::default(), button, x, y) } pub(in crate::input::state) fn handle_properties_panel_press_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, button: MouseButton, x: i32, y: i32, diff --git a/src/input/state/mouse/press/polygon.rs b/src/input/state/mouse/press/polygon.rs index d6d29fd1c..1a25a74f9 100644 --- a/src/input/state/mouse/press/polygon.rs +++ b/src/input/state/mouse/press/polygon.rs @@ -1,6 +1,8 @@ -use crate::draw::Shape; +use std::mem; + use crate::draw::frame::UndoAction; use crate::draw::shape::{PolygonKind, has_minimum_distinct_points}; +use crate::draw::{Shape, TextMeasurer}; use crate::i18n::tr; use crate::input::Tool; use crate::input::state::{Toast, ToastPriority}; @@ -12,7 +14,7 @@ use super::super::{TEXT_DOUBLE_CLICK_DISTANCE, TEXT_DOUBLE_CLICK_MS}; impl InputState { pub(crate) fn start_building_polygon_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, x: i32, y: i32, ) { @@ -58,7 +60,7 @@ impl InputState { pub(crate) fn handle_building_polygon_left_click_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, x: i32, y: i32, ) { @@ -71,7 +73,7 @@ impl InputState { pub(crate) fn append_building_polygon_point_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, x: i32, y: i32, ) { @@ -89,10 +91,7 @@ impl InputState { self.needs_redraw = true; } - pub(crate) fn pop_building_polygon_point_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - ) { + pub(crate) fn pop_building_polygon_point_with_measurer(&mut self, measurer: &TextMeasurer) { let DrawingState::BuildingPolygon { points, .. } = &mut self.state else { return; }; @@ -109,11 +108,8 @@ impl InputState { self.needs_redraw = true; } - pub(crate) fn finish_building_polygon_with_measurer( - &mut self, - measurer: &crate::draw::TextMeasurer, - ) { - let state = std::mem::replace(&mut self.state, DrawingState::Idle); + pub(crate) fn finish_building_polygon_with_measurer(&mut self, measurer: &TextMeasurer) { + let state = mem::replace(&mut self.state, DrawingState::Idle); let DrawingState::BuildingPolygon { points, fill, diff --git a/src/input/state/mouse/release/drawing.rs b/src/input/state/mouse/release/drawing.rs index 5a57807da..7f70c037a 100644 --- a/src/input/state/mouse/release/drawing.rs +++ b/src/input/state/mouse/release/drawing.rs @@ -1,8 +1,11 @@ +use std::time::Instant; + use log::warn; -use crate::draw::Shape; +use crate::draw; use crate::draw::frame::{ShapeSnapshot, UndoAction}; use crate::draw::shape::bounding_box_for_points; +use crate::draw::{Shape, TextMeasurer}; use crate::input::tool::{ FinishedToolStroke, PolygonStrokeSnapshot, ToolStrokeSnapshot, ToolUsage, }; @@ -20,7 +23,7 @@ pub(super) struct DrawingRelease { pub(super) fn finish_drawing( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, tool: Tool, release: DrawingRelease, ) { @@ -59,7 +62,7 @@ pub(super) fn finish_drawing( fill_enabled: state.style.fill_enabled, regular_sides: state.style.polygon_sides, }; - tool.finish_polygon_stroke(snapshot) + tool.finish_polygon_stroke(&snapshot) } else { let snapshot = ToolStrokeSnapshot { tool, @@ -226,12 +229,12 @@ fn follow_committed_stroke( ) { state.record_first_stroke_done_for_onboarding(); if recognized { - state.show_recognition_chip(shape, bounds, std::time::Instant::now()); + state.show_recognition_chip(shape, bounds, Instant::now()); } if matches!( shape, Shape::Spotlight { magnification, .. } - if crate::draw::spotlight_magnification_is_active(*magnification) + if draw::spotlight_magnification_is_active(*magnification) ) { state.request_spotlight_magnifier_feedback(); } @@ -363,6 +366,10 @@ fn append_segment_damage_regions( #[cfg(test)] mod tests { + use crate::draw::shape; + use crate::input::MouseButton; + use crate::util::Rect; + use super::raw_preview_damage_regions; use crate::draw::Shape; use crate::input::Tool; @@ -407,11 +414,11 @@ mod tests { let path = shaky_path(); let first = path[0]; let last = *path.last().unwrap(); - state.on_mouse_press(crate::input::MouseButton::Left, first.0, first.1); + state.on_mouse_press(MouseButton::Left, first.0, first.1); for &(x, y) in &path[1..] { state.on_mouse_motion(x, y); } - state.on_mouse_release(crate::input::MouseButton::Left, last.0, last.1); + state.on_mouse_release(MouseButton::Left, last.0, last.1); let shape = state .boards @@ -467,23 +474,23 @@ mod tests { } /// Draw the shaky path at `level` and return the damage the release left. - fn damage_after_drawing(level: u8) -> Vec { + fn damage_after_drawing(level: u8) -> Vec { let mut state = make_test_input_state(); state.set_pen_smoothing(level); state.set_tool_override(Some(Tool::Pen)); let path = shaky_path(); let first = path[0]; let last = *path.last().unwrap(); - state.on_mouse_press(crate::input::MouseButton::Left, first.0, first.1); + state.on_mouse_press(MouseButton::Left, first.0, first.1); for &(x, y) in &path[1..] { state.on_mouse_motion(x, y); } let _ = state.take_dirty_regions(); - state.on_mouse_release(crate::input::MouseButton::Left, last.0, last.1); + state.on_mouse_release(MouseButton::Left, last.0, last.1); state.take_dirty_regions() } - fn covers(regions: &[crate::util::Rect], x: i32, y: i32) -> bool { + fn covers(regions: &[Rect], x: i32, y: i32) -> bool { regions.iter().any(|rect| rect.contains(x, y)) } @@ -507,7 +514,7 @@ mod tests { // on a long diagonal stroke is most of the screen. let regions = damage_after_drawing(6); let path = shaky_path(); - let full = crate::draw::shape::bounding_box_for_points(&path, 64.0).unwrap(); + let full = shape::bounding_box_for_points(&path, 64.0).unwrap(); assert!( !regions.iter().any(|rect| rect.width >= full.width diff --git a/src/input/state/mouse/release/mod.rs b/src/input/state/mouse/release/mod.rs index 4c116216b..8595da007 100644 --- a/src/input/state/mouse/release/mod.rs +++ b/src/input/state/mouse/release/mod.rs @@ -1,9 +1,14 @@ +use std::mem; + +use crate::draw::TextMeasurer; use crate::input::events::MouseButton; +use crate::input::state::InputTextResources; +use crate::ui_text::UiTextEngine; -use super::super::{ - DrawingState, InputState, - interaction::{CanvasPoint, PointerPoints, PointerRelease, ScreenPoint, route_pointer_release}, +use super::super::interaction::{ + CanvasPoint, PointerPoints, PointerRelease, ScreenPoint, route_pointer_release, }; +use super::super::{DrawingState, InputState}; mod drawing; mod panels; @@ -45,8 +50,8 @@ impl InputState { canvas_x: impl Into, canvas_y: impl Into, ) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); self.on_mouse_release_with_canvas_and_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -62,7 +67,7 @@ impl InputState { pub(crate) fn on_mouse_release_with_canvas_and_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, button: MouseButton, screen_x: i32, screen_y: i32, @@ -88,7 +93,7 @@ impl InputState { pub(in crate::input::state) fn handle_context_menu_release_at_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, x: i32, y: i32, ) -> bool { @@ -97,7 +102,7 @@ impl InputState { pub(in crate::input::state) fn handle_board_picker_release_at_with_resources( &mut self, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, x: i32, y: i32, ) -> bool { @@ -106,7 +111,7 @@ impl InputState { pub(in crate::input::state) fn handle_properties_panel_release_at_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, x: i32, y: i32, ) -> bool { @@ -115,7 +120,7 @@ impl InputState { pub(in crate::input::state) fn finish_pointer_interaction_at_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, canvas_x: i32, canvas_y: i32, ) { @@ -132,7 +137,7 @@ impl InputState { /// gesture first. The later release then finds nothing left to finish. pub(in crate::input::state) fn settle_snapshot_gesture_with_measurer( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, ) -> bool { if !matches!( self.state, @@ -152,11 +157,11 @@ impl InputState { fn end_pointer_interaction_with( &mut self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, (canvas_x, canvas_y): (i32, i32), end: GestureEnd, ) { - let state = std::mem::replace(&mut self.state, DrawingState::Idle); + let state = mem::replace(&mut self.state, DrawingState::Idle); match state { DrawingState::MovingSelection { grab, diff --git a/src/input/state/mouse/release/panels.rs b/src/input/state/mouse/release/panels.rs index 7b2261086..64ce29af5 100644 --- a/src/input/state/mouse/release/panels.rs +++ b/src/input/state/mouse/release/panels.rs @@ -1,6 +1,8 @@ use std::time::Instant; +use crate::draw::TextMeasurer; use crate::input::InputState; +use crate::input::state::InputTextResources; use crate::input::state::core::color_picker_popup::ColorPickerPopupAction; use crate::input::state::core::{BoardPickerClickState, MenuCommand}; @@ -106,7 +108,7 @@ pub(super) fn handle_color_picker_popup_release(state: &mut InputState, x: i32, pub(super) fn handle_board_picker_release( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, x: i32, y: i32, ) -> bool { @@ -225,7 +227,7 @@ pub(super) fn handle_board_picker_release( pub(super) fn handle_properties_panel_release( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, x: i32, y: i32, ) -> bool { @@ -242,7 +244,7 @@ pub(super) fn handle_properties_panel_release( pub(super) fn handle_context_menu_release( state: &mut InputState, - resources: crate::input::state::InputTextResources<'_>, + resources: InputTextResources<'_>, x: i32, y: i32, ) -> bool { diff --git a/src/input/state/mouse/release/selection.rs b/src/input/state/mouse/release/selection.rs index e1291e9d1..1f8e71924 100644 --- a/src/input/state/mouse/release/selection.rs +++ b/src/input/state/mouse/release/selection.rs @@ -1,8 +1,9 @@ use std::time::Instant; use crate::draw::frame::{ShapeSnapshot, UndoAction}; -use crate::draw::{Shape, ShapeId}; -use crate::input::{InputState, state::SelectionGrab}; +use crate::draw::{Shape, ShapeId, TextMeasurer}; +use crate::input::InputState; +use crate::input::state::SelectionGrab; use super::super::{SELECTION_DRAG_THRESHOLD, TEXT_DOUBLE_CLICK_DISTANCE, TEXT_DOUBLE_CLICK_MS}; @@ -13,7 +14,7 @@ use super::super::{SELECTION_DRAG_THRESHOLD, TEXT_DOUBLE_CLICK_DISTANCE, TEXT_DO /// (`release` is `None`) records its move but is never a click. pub(super) fn finish_moving_selection( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, mut grab: SelectionGrab, release: Option<(i32, i32)>, snapshots: Vec<(ShapeId, ShapeSnapshot)>, @@ -44,11 +45,7 @@ pub(super) fn finish_moving_selection( /// Shift keeps the rest of the selection, so its shared properties open /// together; otherwise the double-click narrows the selection to the shape. -fn open_double_clicked_shape( - state: &mut InputState, - measurer: &crate::draw::TextMeasurer, - shape_id: ShapeId, -) { +fn open_double_clicked_shape(state: &mut InputState, measurer: &TextMeasurer, shape_id: ShapeId) { if !state.modifiers.shift { state.set_selection_with(measurer, vec![shape_id]); } @@ -59,7 +56,7 @@ fn open_double_clicked_shape( pub(super) fn finish_selection_drag( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, start_x: i32, start_y: i32, end_x: i32, @@ -158,7 +155,7 @@ pub(super) fn finish_text_resize( pub(super) fn finish_selection_resize( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, snapshots: &[(ShapeId, ShapeSnapshot)], ) { let effects = @@ -172,12 +169,16 @@ pub(super) fn finish_selection_resize( #[cfg(test)] mod tests { + use crate::draw; + use crate::input::state::SelectionHandle; + use crate::input::state::test_support::TestInputStateBuilder; + use super::*; #[test] fn multi_shape_resize_undo_and_redo_are_one_gesture() { - let mut state = crate::input::state::test_support::TestInputStateBuilder::default().build(); - let measurer = crate::draw::TextMeasurer::default(); + let mut state = TestInputStateBuilder::default().build(); + let measurer = TextMeasurer::default(); let ids: Vec<_> = [10, 50] .into_iter() .map(|x| { @@ -188,7 +189,7 @@ mod tests { h: 20, fill: false, fill_color: None, - color: crate::draw::WHITE, + color: draw::WHITE, thick: 2.0, }) }) @@ -202,7 +203,7 @@ mod tests { .collect(); state.apply_selection_resize_with( &measurer, - crate::input::state::SelectionHandle::BottomRight, + SelectionHandle::BottomRight, &bounds, bounds.width, bounds.height, diff --git a/src/input/state/mouse/release/text.rs b/src/input/state/mouse/release/text.rs index 9288c3f9d..6db30cd08 100644 --- a/src/input/state/mouse/release/text.rs +++ b/src/input/state/mouse/release/text.rs @@ -1,15 +1,16 @@ use std::time::Instant; +use crate::draw::{ShapeId, TextMeasurer}; use crate::input::InputState; use super::super::{TEXT_DOUBLE_CLICK_DISTANCE, TEXT_DOUBLE_CLICK_MS}; pub(super) fn handle_pending_text_click( state: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, x: i32, y: i32, - shape_id: crate::draw::ShapeId, + shape_id: ShapeId, ) { let is_double = state.text_editing.register_click( shape_id, diff --git a/src/input/state/render.rs b/src/input/state/render.rs index 43a1d36fb..daee0d974 100644 --- a/src/input/state/render.rs +++ b/src/input/state/render.rs @@ -1,8 +1,11 @@ +use std::iter; + +use crate::domain::BoardGrid; use crate::draw::render::{render_freehand_pressure_preview_borrowed, render_polygon_preview}; use crate::draw::shape::bounding_box_for_points; use crate::draw::{ - Color, RenderCaches, RenderCtx, Shape, render_freehand_borrowed, render_laser_stroke, - render_marker_stroke_borrowed, + Color, RenderCaches, RenderCtx, Shape, TextMeasurer, render_freehand_borrowed, + render_laser_stroke, render_marker_stroke_borrowed, }; use crate::input::Tool; use crate::input::tool::{ @@ -66,7 +69,7 @@ impl InputState { } /// The board paper Shape Pen snaps to: none when snapping is turned off. - pub(crate) fn shape_pen_grid(&self) -> crate::domain::BoardGrid { + pub(crate) fn shape_pen_grid(&self) -> BoardGrid { let grid = self.boards.active_board().spec.grid; if self.style.shape_recognition_grid_snap { grid @@ -101,7 +104,7 @@ impl InputState { fill_enabled: self.style.fill_enabled, regular_sides: self.style.polygon_sides, }; - return tool.provisional_polygon_stroke(snapshot); + return tool.provisional_polygon_stroke(&snapshot); } let snapshot = ProvisionalToolSnapshot { @@ -137,7 +140,7 @@ impl InputState { pub(crate) fn render_provisional_tool_stroke( &self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, render: &mut RenderCtx<'_, '_>, stroke: ProvisionalToolStroke<'_>, text_halo_enabled: bool, @@ -219,7 +222,7 @@ impl InputState { pub(crate) fn render_provisional_tool_stroke_for_damage( &self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, render: &mut RenderCtx<'_, '_>, stroke: ProvisionalToolStroke<'_>, damage_regions: &[Rect], @@ -335,7 +338,7 @@ impl InputState { current_y: i32, ) -> bool { let mut caches = RenderCaches::default(); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); self.render_provisional_shape_with_halo( &measurer, &mut RenderCtx::new(ctx, &mut caches), @@ -347,7 +350,7 @@ impl InputState { pub(crate) fn render_provisional_shape_with_halo( &self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, render: &mut RenderCtx<'_, '_>, current_x: i32, current_y: i32, @@ -409,7 +412,7 @@ impl InputState { pub(crate) fn render_provisional_shape_for_damage( &self, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, render: &mut RenderCtx<'_, '_>, current_x: i32, current_y: i32, @@ -479,7 +482,7 @@ fn path_damage_ranges>( } fn single_range(range: Range) -> Vec> { - std::iter::once(range).collect() + iter::once(range).collect() } fn push_merged_range(ranges: &mut Vec>, next: Range) { @@ -516,13 +519,19 @@ fn pressure_preview_needs_full_mask_render(color: Color, ranges: &[Range] #[cfg(test)] mod tests { + use std::io::Cursor; + + use crate::draw; + use crate::draw::{ArrowLabel, ArrowStyle, EmbeddedImage, FontDescriptor, StepMarkerLabel}; + use crate::input::state::test_support::TestInputStateBuilder; + use super::*; #[test] fn persistent_preview_dispatch_matches_shape_rendering() { - let measurer = crate::draw::TextMeasurer::default(); - let input = crate::input::state::test_support::TestInputStateBuilder::default().build(); - let mut png = std::io::Cursor::new(Vec::new()); + let measurer = TextMeasurer::default(); + let input = TestInputStateBuilder::default().build(); + let mut png = Cursor::new(Vec::new()); let source = cairo::ImageSurface::create(cairo::Format::ARgb32, 2, 2).unwrap(); let context = cairo::Context::new(&source).unwrap(); context.set_source_rgb(240.0 / 255.0, 50.0 / 255.0, 20.0 / 255.0); @@ -534,7 +543,7 @@ mod tests { y: 2, w: 20, h: 20, - data: crate::draw::EmbeddedImage { + data: EmbeddedImage { mime_type: "image/png".into(), width: 2, height: 2, @@ -545,9 +554,9 @@ mod tests { x: 2, y: 42, text: "Preview".into(), - color: crate::draw::RED, + color: draw::RED, size: 14.0, - font_descriptor: crate::draw::FontDescriptor::default(), + font_descriptor: FontDescriptor::default(), background_enabled: false, wrap_width: None, }, @@ -556,14 +565,14 @@ mod tests { y1: 80, x2: 210, y2: 80, - color: crate::draw::RED, + color: draw::RED, thick: 3.0, arrow_length: 12.0, arrow_angle: 30.0, head_at_end: true, - style: crate::draw::ArrowStyle::Standard, + style: ArrowStyle::Standard, bend: 0.0, - label: Some(crate::draw::ArrowLabel { + label: Some(ArrowLabel { value: 72, size: 18.0, font_descriptor: Default::default(), @@ -572,8 +581,8 @@ mod tests { Shape::StepMarker { x: 55, y: 105, - color: crate::draw::RED, - label: crate::draw::StepMarkerLabel { + color: draw::RED, + label: StepMarkerLabel { value: 108, size: 18.0, font_descriptor: Default::default(), diff --git a/src/input/state/spotlight.rs b/src/input/state/spotlight.rs index 8a8d64d4e..bea76dac2 100644 --- a/src/input/state/spotlight.rs +++ b/src/input/state/spotlight.rs @@ -4,8 +4,12 @@ //! layer and punches all the openings out of it. That makes spotlights the only //! shape kind the renderer collects up front instead of drawing in z-order. +use crate::draw::frame::{ShapeSnapshot, UndoAction}; use crate::draw::{Shape, ShapeId, SpotlightRegion, TextMeasurer, spotlight_regions_for_frame}; -use crate::input::Tool; +use crate::input::boards::{BoardIdentityGeneration, BoardManager}; +use crate::input::{Tool, hit_test}; +use crate::ui::toolbar::model::ToolbarSliderSpec; +use crate::{draw, util}; use super::{DrawingState, InputState}; @@ -32,7 +36,7 @@ pub(crate) struct SpotlightFrameRegions { pub(crate) struct FrameIdentity { /// Bumped whenever the set of boards is replaced, so a restored session /// cannot present itself as the same board list. - board_identity: crate::input::boards::BoardIdentityGeneration, + board_identity: BoardIdentityGeneration, board_id: String, page_index: usize, /// Bumped whenever pages are added or removed, so deleting a page and @@ -42,7 +46,7 @@ pub(crate) struct FrameIdentity { } impl FrameIdentity { - pub(crate) fn of(boards: &crate::input::boards::BoardManager) -> Self { + pub(crate) fn of(boards: &BoardManager) -> Self { Self { board_identity: boards.board_identity_generation(), board_id: boards.active_board_id().to_string(), @@ -59,7 +63,7 @@ struct SpotlightMagnificationGesture { frame: FrameIdentity, shape_id: ShapeId, /// Factor before the first tick, so the whole burst is one undo entry. - before: crate::draw::frame::ShapeSnapshot, + before: ShapeSnapshot, } #[derive(Debug, Clone, Default)] @@ -153,7 +157,7 @@ enum SpotlightWheelTarget { /// /// Compared rather than the whole `Shape`, which has no `PartialEq`, and the /// factor is the only field either gesture touches. -fn snapshot_magnification(snapshot: &crate::draw::frame::ShapeSnapshot) -> Option { +fn snapshot_magnification(snapshot: &ShapeSnapshot) -> Option { match snapshot.shape { Shape::Spotlight { magnification, .. } => Some(magnification.to_bits()), _ => None, @@ -209,7 +213,7 @@ impl InputState { let mut regions = spotlight_regions_for_frame(self.boards.active_frame()); let committed_magnified = regions .iter() - .any(|region| crate::draw::spotlight_magnification_is_active(region.magnification)); + .any(|region| draw::spotlight_magnification_is_active(region.magnification)); regions.extend(cursor.and_then(|cursor| self.provisional_spotlight_region(cursor))); SpotlightFrameRegions { @@ -231,7 +235,7 @@ impl InputState { // Use the same predicate as ordinary hit testing. A former copy // disagreed about degenerate radii and the boundary epsilon, so a // loupe could be clickable and yet not answer the wheel. - if !crate::input::hit_test::ellipse_fill_hit(cx, cy, rx, ry, (x, y)) { + if !hit_test::ellipse_fill_hit(cx, cy, rx, ry, (x, y)) { return None; } Some(if drawn.locked { @@ -313,7 +317,7 @@ impl InputState { shape_id: ShapeId, magnification: f64, ) -> bool { - let normalized = crate::draw::normalize_spotlight_magnification(magnification); + let normalized = draw::normalize_spotlight_magnification(magnification); let frame = self.boards.active_frame_mut(); let Some(drawn) = frame.shape_mut(shape_id) else { return false; @@ -377,17 +381,15 @@ impl InputState { let Shape::Spotlight { magnification, .. } = drawn.shape else { return SpotlightWheelOutcome::NotOverLoupe; }; - let before = crate::draw::frame::ShapeSnapshot { + let before = ShapeSnapshot { shape: drawn.shape.clone(), locked: drawn.locked, }; // Snapped, not just stepped: a shape that somehow sits between steps // — an older session, a hand-edited file — is pulled back onto the // grid by the first tick instead of carrying its offset forever. - let target = crate::ui::toolbar::model::ToolbarSliderSpec::SPOTLIGHT_MAGNIFICATION - .normalize_value( - magnification + crate::draw::SPOTLIGHT_MAGNIFICATION_STEP * f64::from(steps), - ); + let target = ToolbarSliderSpec::SPOTLIGHT_MAGNIFICATION + .normalize_value(magnification + draw::SPOTLIGHT_MAGNIFICATION_STEP * f64::from(steps)); if !self.set_spotlight_shape_magnification_with(measurer, shape_id, target) { // An end of the range. The wheel still belongs to this loupe, so // the caller must not fall through to thickness: the pointer is @@ -402,7 +404,7 @@ impl InputState { shape_id, before, }); - if crate::draw::spotlight_magnification_is_active(target) { + if draw::spotlight_magnification_is_active(target) { self.request_spotlight_magnifier_feedback(); } SpotlightWheelOutcome::Adjusted @@ -451,7 +453,7 @@ impl InputState { let Some(drawn) = self.boards.active_frame().shape(shape_id) else { return; }; - let after = crate::draw::frame::ShapeSnapshot { + let after = ShapeSnapshot { shape: drawn.shape.clone(), locked: drawn.locked, }; @@ -467,8 +469,8 @@ impl InputState { pub(crate) fn record_spotlight_magnification_change( &mut self, shape_id: ShapeId, - before: crate::draw::frame::ShapeSnapshot, - after: crate::draw::frame::ShapeSnapshot, + before: ShapeSnapshot, + after: ShapeSnapshot, ) { let after_factor = snapshot_magnification(&after); if snapshot_magnification(&before) == after_factor { @@ -476,7 +478,7 @@ impl InputState { } let limit = self.history_limits.undo_stack_limit(); self.boards.active_frame_mut().push_undo_action( - crate::draw::frame::UndoAction::Modify { + UndoAction::Modify { shape_id, before, after, @@ -486,7 +488,7 @@ impl InputState { self.mark_session_dirty(); if after_factor .map(f64::from_bits) - .is_some_and(crate::draw::spotlight_magnification_is_active) + .is_some_and(draw::spotlight_magnification_is_active) { self.request_spotlight_magnifier_feedback(); } @@ -510,13 +512,13 @@ impl InputState { return None; } - let (cx, cy, rx, ry) = crate::util::ellipse_bounds(*start_x, *start_y, cursor.0, cursor.1); + let (cx, cy, rx, ry) = util::ellipse_bounds(*start_x, *start_y, cursor.0, cursor.1); Some(SpotlightRegion { cx: f64::from(cx), cy: f64::from(cy), rx: f64::from(rx), ry: f64::from(ry), - magnification: crate::draw::normalize_spotlight_magnification( + magnification: draw::normalize_spotlight_magnification( self.style.spotlight_magnification, ), }) @@ -530,13 +532,13 @@ mod wheel_gesture_tests { fn gesture(shape_id: ShapeId) -> SpotlightMagnificationGesture { SpotlightMagnificationGesture { frame: FrameIdentity { - board_identity: crate::input::boards::BoardIdentityGeneration(1), + board_identity: BoardIdentityGeneration(1), board_id: "transparent".to_string(), page_index: 0, page_generation: 0, }, shape_id, - before: crate::draw::frame::ShapeSnapshot { + before: ShapeSnapshot { shape: Shape::Spotlight { cx: 20, cy: 30, diff --git a/src/input/state/spotlight/selection.rs b/src/input/state/spotlight/selection.rs index bbf8d8df0..a5a16941b 100644 --- a/src/input/state/spotlight/selection.rs +++ b/src/input/state/spotlight/selection.rs @@ -1,4 +1,6 @@ //! Spotlight queries for selection controls. +use crate::draw; + use super::InputState; use crate::draw::{DrawnShape, Shape}; @@ -19,9 +21,9 @@ impl InputState { selected .iter() .filter_map(|drawn| match drawn.shape { - Shape::Spotlight { magnification, .. } => Some( - crate::draw::normalize_spotlight_magnification(magnification), - ), + Shape::Spotlight { magnification, .. } => { + Some(draw::normalize_spotlight_magnification(magnification)) + } _ => None, }) .reduce(f64::max) diff --git a/src/input/state/tests/action_bindings.rs b/src/input/state/tests/action_bindings.rs index 65f4c2140..246cbd633 100644 --- a/src/input/state/tests/action_bindings.rs +++ b/src/input/state/tests/action_bindings.rs @@ -1,3 +1,9 @@ +use std::time::{Duration, Instant}; + +use crate::config::KeybindingsConfig; +use crate::input::state::core::SequenceMatch; +use crate::input::state::core::modal::ModalSurface; + use super::*; use crate::config::{PointerButton, Shortcut}; use std::collections::HashMap; @@ -96,9 +102,9 @@ fn find_action_respects_exact_modifier_matches() { #[test] fn find_action_treats_super_as_a_distinct_modifier() { - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); keybindings.core.exit = vec!["Super+X".to_string()]; - let mut state = create_test_input_state_with_keybindings(keybindings); + let mut state = create_test_input_state_with_keybindings(&keybindings); state.modifiers.logo = true; assert_eq!(state.find_action("x"), Some(Action::Exit)); @@ -118,10 +124,10 @@ fn find_action_treats_super_as_a_distinct_modifier() { #[test] fn pointer_shortcuts_match_current_modifiers_and_ignore_unbound_buttons() { - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); keybindings.core.undo = vec!["Ctrl+MouseBack".to_string()]; keybindings.core.redo = vec!["MouseForward".to_string()]; - let mut state = create_test_input_state_with_keybindings(keybindings); + let mut state = create_test_input_state_with_keybindings(&keybindings); state.modifiers.ctrl = true; assert_eq!( @@ -156,48 +162,48 @@ fn consumed_pointer_shortcut_buttons_clear_on_focus_loss() { #[test] fn keyboard_sequences_dispatch_after_the_last_step() { - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); keybindings.ui.toggle_floating_badge = vec!["Ctrl+Alt+Shift+K > Ctrl+Alt+Shift+C".to_string()]; - let mut state = create_test_input_state_with_keybindings(keybindings); - let now = std::time::Instant::now(); + let mut state = create_test_input_state_with_keybindings(&keybindings); + let now = Instant::now(); state.modifiers.ctrl = true; state.modifiers.alt = true; state.modifiers.shift = true; assert_eq!( state.match_keyboard_chord("k", false, now), - crate::input::state::core::SequenceMatch::Pending + SequenceMatch::Pending ); assert_eq!( state.match_keyboard_chord("c", false, now), - crate::input::state::core::SequenceMatch::Dispatched(Action::ToggleFloatingBadge) + SequenceMatch::Dispatched(Action::ToggleFloatingBadge) ); } #[test] fn sequence_timeout_and_focus_loss_clear_pending_without_dispatch() { - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); keybindings.ui.toggle_floating_badge = vec!["Ctrl+Alt+Shift+K > Ctrl+Alt+Shift+C".to_string()]; - let mut state = create_test_input_state_with_keybindings(keybindings); - let now = std::time::Instant::now(); + let mut state = create_test_input_state_with_keybindings(&keybindings); + let now = Instant::now(); state.modifiers.ctrl = true; state.modifiers.alt = true; state.modifiers.shift = true; assert_eq!( state.match_keyboard_chord("k", false, now), - crate::input::state::core::SequenceMatch::Pending + SequenceMatch::Pending ); - assert!(state.expire_pending_sequence(now + std::time::Duration::from_secs(1))); + assert!(state.expire_pending_sequence(now + Duration::from_secs(1))); assert_eq!( - state.match_keyboard_chord("c", false, now + std::time::Duration::from_secs(1)), - crate::input::state::core::SequenceMatch::None + state.match_keyboard_chord("c", false, now + Duration::from_secs(1)), + SequenceMatch::None ); assert_eq!( state.match_keyboard_chord("k", false, now), - crate::input::state::core::SequenceMatch::Pending + SequenceMatch::Pending ); state.reset_modifiers(); state.modifiers.ctrl = true; @@ -205,22 +211,22 @@ fn sequence_timeout_and_focus_loss_clear_pending_without_dispatch() { state.modifiers.shift = true; assert_eq!( state.match_keyboard_chord("c", false, now), - crate::input::state::core::SequenceMatch::None + SequenceMatch::None ); } #[test] fn keymap_reload_and_modal_open_clear_pending_sequences() { - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); keybindings.ui.toggle_floating_badge = vec!["Ctrl+Alt+Shift+K > Ctrl+Alt+Shift+C".to_string()]; - let mut state = create_test_input_state_with_keybindings(keybindings.clone()); - let now = std::time::Instant::now(); + let mut state = create_test_input_state_with_keybindings(&keybindings); + let now = Instant::now(); state.modifiers.ctrl = true; state.modifiers.alt = true; state.modifiers.shift = true; assert_eq!( state.match_keyboard_chord("k", false, now), - crate::input::state::core::SequenceMatch::Pending + SequenceMatch::Pending ); let action_map = keybindings.build_action_map().unwrap(); @@ -228,56 +234,56 @@ fn keymap_reload_and_modal_open_clear_pending_sequences() { state.set_keybinding_maps(action_map, action_bindings); assert_eq!( state.match_keyboard_chord("c", false, now), - crate::input::state::core::SequenceMatch::None + SequenceMatch::None ); assert_eq!( state.match_keyboard_chord("k", false, now), - crate::input::state::core::SequenceMatch::Pending + SequenceMatch::Pending ); - state.close_modals_for_open(crate::input::state::core::modal::ModalSurface::HelpOverlay); + state.close_modals_for_open(ModalSurface::HelpOverlay); assert_eq!( state.match_keyboard_chord("c", false, now), - crate::input::state::core::SequenceMatch::None + SequenceMatch::None ); } #[test] fn on_key_press_completes_a_sequence_and_repeat_does_not() { - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); keybindings.ui.toggle_floating_badge = vec!["Ctrl+Alt+Shift+K > Ctrl+Alt+Shift+C".to_string()]; - let mut state = create_test_input_state_with_keybindings(keybindings); + let mut state = create_test_input_state_with_keybindings(&keybindings); assert!(state.ui_visibility.show_floating_badge); state.modifiers.ctrl = true; state.modifiers.alt = true; state.modifiers.shift = true; - state.on_key_press(crate::input::Key::Char('k')); + state.on_key_press(Key::Char('k')); assert!(state.ui_visibility.show_floating_badge); - state.on_key_repeat(crate::input::Key::Char('c')); + state.on_key_repeat(Key::Char('c')); assert!(state.ui_visibility.show_floating_badge); - state.on_key_press(crate::input::Key::Char('c')); + state.on_key_press(Key::Char('c')); assert!(!state.ui_visibility.show_floating_badge); } #[test] fn shifted_punctuation_fallback_completes_a_pending_sequence() { - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); keybindings.ui.toggle_floating_badge = vec!["Ctrl+Alt+Shift+K > Shift+/".to_string()]; - let mut state = create_test_input_state_with_keybindings(keybindings); + let mut state = create_test_input_state_with_keybindings(&keybindings); assert!(state.ui_visibility.show_floating_badge); state.modifiers.ctrl = true; state.modifiers.alt = true; state.modifiers.shift = true; - state.on_key_press(crate::input::Key::Char('k')); + state.on_key_press(Key::Char('k')); assert!(state.ui_visibility.show_floating_badge); state.modifiers.ctrl = false; state.modifiers.alt = false; state.modifiers.shift = true; - state.on_key_press(crate::input::Key::Char('?')); + state.on_key_press(Key::Char('?')); assert!(!state.ui_visibility.show_floating_badge); } diff --git a/src/input/state/tests/basics.rs b/src/input/state/tests/basics.rs index fe23b5c47..dab8114bb 100644 --- a/src/input/state/tests/basics.rs +++ b/src/input/state/tests/basics.rs @@ -1,11 +1,14 @@ +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::*; use crate::config::{DragButtonConfig, MouseDragToolsConfig}; use crate::input::{DragBinding, DragTool, DragToolBindings}; #[test] fn toggle_floating_badge_action_flips_runtime_visibility() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -17,10 +20,7 @@ fn toggle_floating_badge_action_flips_runtime_visibility() { "badge visible by default" ); - state.handle_action_with_resources( - test_text_resources, - crate::config::Action::ToggleFloatingBadge, - ); + state.handle_action_with_resources(test_text_resources, Action::ToggleFloatingBadge); assert!(!state.ui_visibility.show_floating_badge); assert!(state.needs_redraw); // Persisting is the backend's job: it diffs the state before and after @@ -28,10 +28,7 @@ fn toggle_floating_badge_action_flips_runtime_visibility() { assert!(!state.has_pending_backend_actions()); assert!(state.take_pending_backend_action().is_none()); - state.handle_action_with_resources( - test_text_resources, - crate::config::Action::ToggleFloatingBadge, - ); + state.handle_action_with_resources(test_text_resources, Action::ToggleFloatingBadge); assert!(state.ui_visibility.show_floating_badge); } @@ -39,8 +36,8 @@ fn toggle_floating_badge_action_flips_runtime_visibility() { /// through a queued request, however many times they are pressed. #[test] fn repeated_chrome_visibility_toggles_queue_no_backend_work() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -48,16 +45,10 @@ fn repeated_chrome_visibility_toggles_queue_no_backend_work() { let mut state = create_test_input_state(); - state.handle_action_with_resources( - test_text_resources, - crate::config::Action::ToggleFloatingBadge, - ); - state.handle_action_with_resources( - test_text_resources, - crate::config::Action::ToggleFloatingBadge, - ); - state.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleZoomChip); - state.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleZoomChip); + state.handle_action_with_resources(test_text_resources, Action::ToggleFloatingBadge); + state.handle_action_with_resources(test_text_resources, Action::ToggleFloatingBadge); + state.handle_action_with_resources(test_text_resources, Action::ToggleZoomChip); + state.handle_action_with_resources(test_text_resources, Action::ToggleZoomChip); assert!(state.ui_visibility.show_floating_badge); assert!(state.ui_visibility.show_zoom_chip); @@ -265,8 +256,8 @@ fn test_adjust_font_size_multiple_adjustments() { /// queue through `handle_action`. #[test] fn chrome_actions_queue_their_own_durable_entry() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -276,17 +267,14 @@ fn chrome_actions_queue_their_own_durable_entry() { for (action, expected) in [ ( - crate::config::Action::ToggleStatusBar, + Action::ToggleStatusBar, Pending::StatusBar { previous: true }, ), ( - crate::config::Action::ToggleFloatingBadge, + Action::ToggleFloatingBadge, Pending::FloatingBadge { previous: true }, ), - ( - crate::config::Action::ToggleZoomChip, - Pending::ZoomChip { previous: true }, - ), + (Action::ToggleZoomChip, Pending::ZoomChip { previous: true }), ] { let mut state = create_test_input_state(); state.handle_action_with_resources(test_text_resources, action); @@ -303,8 +291,8 @@ fn chrome_actions_queue_their_own_durable_entry() { /// which a before/after read of the focus flag would get exactly backwards. #[test] fn a_chrome_toggle_that_breaks_focus_mode_still_persists() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -313,13 +301,13 @@ fn a_chrome_toggle_that_breaks_focus_mode_still_persists() { use crate::input::state::PendingToolbarPersistence as Pending; let mut state = create_test_input_state(); - state.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleFocusMode); + state.handle_action_with_resources(test_text_resources, Action::ToggleFocusMode); assert!(state.focus_mode_active()); // Focus mode taking chrome over is not a preference. assert!(state.take_pending_toolbar_persistence().is_empty()); let previous = state.ui_visibility.show_status_bar; - state.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleStatusBar); + state.handle_action_with_resources(test_text_resources, Action::ToggleStatusBar); assert!(!state.focus_mode_active(), "the toggle breaks focus mode"); assert_eq!( state.take_pending_toolbar_persistence(), @@ -331,8 +319,8 @@ fn a_chrome_toggle_that_breaks_focus_mode_still_persists() { /// neither is the user choosing to live without it. #[test] fn focus_mode_transitions_queue_no_durable_chrome_change() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -340,11 +328,11 @@ fn focus_mode_transitions_queue_no_durable_chrome_change() { let mut state = create_test_input_state(); - state.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleFocusMode); + state.handle_action_with_resources(test_text_resources, Action::ToggleFocusMode); assert!(!state.ui_visibility.show_status_bar); assert!(state.take_pending_toolbar_persistence().is_empty()); - state.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleFocusMode); + state.handle_action_with_resources(test_text_resources, Action::ToggleFocusMode); assert!(state.ui_visibility.show_status_bar); assert!(state.take_pending_toolbar_persistence().is_empty()); } @@ -353,8 +341,8 @@ fn focus_mode_transitions_queue_no_durable_chrome_change() { /// preferences keeps one entry each rather than the first swallowing the rest. #[test] fn a_burst_across_chrome_kinds_keeps_one_entry_each() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -363,12 +351,9 @@ fn a_burst_across_chrome_kinds_keeps_one_entry_each() { use crate::input::state::PendingToolbarPersistence as Pending; let mut state = create_test_input_state(); - state.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleStatusBar); - state.handle_action_with_resources( - test_text_resources, - crate::config::Action::ToggleFloatingBadge, - ); - state.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleZoomChip); + state.handle_action_with_resources(test_text_resources, Action::ToggleStatusBar); + state.handle_action_with_resources(test_text_resources, Action::ToggleFloatingBadge); + state.handle_action_with_resources(test_text_resources, Action::ToggleZoomChip); assert_eq!( state.take_pending_toolbar_persistence(), @@ -383,16 +368,16 @@ fn a_burst_across_chrome_kinds_keeps_one_entry_each() { /// A burst that lands where it started is dropped: nothing durable changed. #[test] fn a_chrome_toggle_pressed_twice_queues_nothing() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, }; let mut state = create_test_input_state(); - state.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleStatusBar); - state.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleStatusBar); + state.handle_action_with_resources(test_text_resources, Action::ToggleStatusBar); + state.handle_action_with_resources(test_text_resources, Action::ToggleStatusBar); assert!(state.ui_visibility.show_status_bar); assert!(state.take_pending_toolbar_persistence().is_empty()); @@ -402,8 +387,8 @@ fn a_chrome_toggle_pressed_twice_queues_nothing() { /// effect, and the explicit toggles move it directly; all of them persist. #[test] fn highlight_actions_queue_the_click_highlight() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -412,9 +397,9 @@ fn highlight_actions_queue_the_click_highlight() { use crate::input::state::PendingToolbarPersistence as Pending; for action in [ - crate::config::Action::ToggleClickHighlight, - crate::config::Action::ToggleHighlightTool, - crate::config::Action::SelectHighlightTool, + Action::ToggleClickHighlight, + Action::ToggleHighlightTool, + Action::SelectHighlightTool, ] { let mut state = create_test_input_state(); let previous_enabled = state.click_highlight_enabled(); diff --git a/src/input/state/tests/board_picker.rs b/src/input/state/tests/board_picker.rs index 2456290cf..5d3b84d8c 100644 --- a/src/input/state/tests/board_picker.rs +++ b/src/input/state/tests/board_picker.rs @@ -1,3 +1,8 @@ +use crate::config::BoardsConfig; +use crate::draw::TextMeasurer; +use crate::input::boards::BoardManager; +use crate::input::state::{BoardPickerLayout, InputState}; + use super::create_test_input_state; use crate::draw::{Frame, PageDeleteOutcome}; use crate::input::BoardBackground; @@ -11,7 +16,7 @@ use crate::input::state::{ PAGE_DELETE_ICON_MARGIN, PAGE_DELETE_ICON_SIZE, PAGE_NAME_HEIGHT, PAGE_NAME_PADDING, }; -fn apply_pending_board_pin(input: &mut crate::input::state::InputState) { +fn apply_pending_board_pin(input: &mut InputState) { let actions = input.take_pending_board_runtime_ui_actions(); assert_eq!(actions.len(), 1); let PendingBoardRuntimeUiAction::TogglePin { @@ -40,7 +45,7 @@ fn apply_pending_board_pin(input: &mut crate::input::state::InputState) { #[test] fn board_picker_search_selects_transposed_match() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); for ch in "balckboard".chars() { input.board_picker_append_search(ch); } @@ -52,7 +57,7 @@ fn board_picker_search_selects_transposed_match() { #[test] fn board_picker_search_selects_prefix_match() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); for ch in "blue".chars() { input.board_picker_append_search(ch); } @@ -66,7 +71,7 @@ fn board_picker_selects_recent_board() { let mut input = create_test_input_state(); input.switch_board("blackboard"); input.switch_board("whiteboard"); - input.open_board_picker_quick_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_quick_with_measurer(&TextMeasurer::default()); let selected = input.board_picker_selected_index().expect("selection"); let name = &input.boards.board_states()[selected].spec.name; assert_eq!(name, "Blackboard"); @@ -76,7 +81,7 @@ fn board_picker_selects_recent_board() { fn board_picker_quick_mode_hides_new_row() { let mut input = create_test_input_state(); let board_count = input.boards.board_count(); - input.open_board_picker_quick_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_quick_with_measurer(&TextMeasurer::default()); assert_eq!(input.board_picker_row_count(), board_count); if board_count > 0 { assert!(!input.board_picker_is_new_row(board_count - 1)); @@ -93,12 +98,12 @@ fn board_picker_quick_mode_pins_board_to_top() { .position(|board| board.spec.id == "blackboard") .expect("blackboard board"); input.switch_board("whiteboard"); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected(blackboard_index); input.board_picker_toggle_pin_selected(); apply_pending_board_pin(&mut input); - input.open_board_picker_quick_with_measurer(&crate::draw::TextMeasurer::default()); - input.board_picker_activate_row_with_measurer(&crate::draw::TextMeasurer::default(), 0); + input.open_board_picker_quick_with_measurer(&TextMeasurer::default()); + input.board_picker_activate_row_with_measurer(&TextMeasurer::default(), 0); assert_eq!(input.board_id(), "blackboard"); } @@ -112,19 +117,19 @@ fn board_picker_full_mode_pins_board_to_top() { .position(|board| board.spec.id == "blackboard") .expect("blackboard board"); input.switch_board("whiteboard"); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected(blackboard_index); input.board_picker_toggle_pin_selected(); apply_pending_board_pin(&mut input); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); - input.board_picker_activate_row_with_measurer(&crate::draw::TextMeasurer::default(), 0); + input.open_board_picker_with_measurer(&TextMeasurer::default()); + input.board_picker_activate_row_with_measurer(&TextMeasurer::default(), 0); assert_eq!(input.board_id(), "blackboard"); } #[test] fn board_picker_drag_pinned_clamped_to_pinned_section() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let blackboard_index = input .boards .board_states() @@ -156,7 +161,7 @@ fn board_picker_drag_pinned_clamped_to_pinned_section() { #[test] fn board_picker_drag_unpinned_clamped_after_pinned() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let blackboard_index = input .boards .board_states() @@ -190,18 +195,14 @@ fn board_picker_drag_unpinned_clamped_after_pinned() { assert!(row_after >= 1); } -fn update_picker_layout(state: &mut crate::input::state::InputState, width: i32, height: i32) { +fn update_picker_layout(state: &mut InputState, width: i32, height: i32) { let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height).expect("image surface"); let ctx = cairo::Context::new(&surface).expect("cairo context"); state.update_board_picker_layout(&ctx, width as u32, height as u32); } -fn set_board_page_count( - state: &mut crate::input::state::InputState, - board_index: usize, - page_count: usize, -) { +fn set_board_page_count(state: &mut InputState, board_index: usize, page_count: usize) { let pages = state.boards.board_states_mut()[board_index] .pages .pages_mut(); @@ -209,11 +210,7 @@ fn set_board_page_count( pages.extend((0..page_count.max(1)).map(|_| Frame::new())); } -fn set_board_page_names( - state: &mut crate::input::state::InputState, - board_index: usize, - names: &[&str], -) { +fn set_board_page_names(state: &mut InputState, board_index: usize, names: &[&str]) { set_board_page_count(state, board_index, names.len()); for (index, name) in names.iter().enumerate() { assert!( @@ -224,10 +221,7 @@ fn set_board_page_names( } } -fn page_thumb_origin( - layout: crate::input::state::BoardPickerLayout, - page_index: usize, -) -> Option<(f64, f64)> { +fn page_thumb_origin(layout: BoardPickerLayout, page_index: usize) -> Option<(f64, f64)> { if page_index < layout.page_first_visible_index { return None; } @@ -251,7 +245,7 @@ fn page_thumb_origin( )) } -fn assert_page_visible(layout: crate::input::state::BoardPickerLayout, page_index: usize) { +fn assert_page_visible(layout: BoardPickerLayout, page_index: usize) { assert!( page_thumb_origin(layout, page_index).is_some(), "page {} should be visible in {:?}", @@ -263,7 +257,7 @@ fn assert_page_visible(layout: crate::input::state::BoardPickerLayout, page_inde #[test] fn board_picker_page_hit_testing_uses_rendered_thumbnail_positions() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); update_picker_layout(&mut input, 1280, 720); let layout = *input.board_picker_layout().expect("layout"); @@ -300,7 +294,7 @@ fn board_picker_page_hit_testing_uses_rendered_thumbnail_positions() { #[test] fn board_picker_empty_page_list_has_no_page_hit() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -322,7 +316,7 @@ fn board_picker_empty_page_list_has_no_page_hit() { #[test] fn board_picker_add_card_clickable_when_pages_exactly_fill_rows() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); update_picker_layout(&mut input, 1280, 720); let board_index = input @@ -364,7 +358,7 @@ fn board_picker_add_card_clickable_when_pages_exactly_fill_rows() { #[test] fn board_picker_overflow_hitbox_matches_rendered_hint_position() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); update_picker_layout(&mut input, 1280, 720); let board_index = input @@ -394,7 +388,7 @@ fn board_picker_overflow_hitbox_matches_rendered_hint_position() { #[test] fn board_picker_page_ten_hit_testing_returns_absolute_index() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -438,7 +432,7 @@ fn board_picker_page_ten_hit_testing_returns_absolute_index() { #[test] fn board_picker_page_ten_operations_use_absolute_index() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -452,20 +446,20 @@ fn board_picker_page_ten_operations_use_absolute_index() { } update_picker_layout(&mut input, 1280, 720); - input.board_picker_duplicate_page_with_measurer(&crate::draw::TextMeasurer::default(), 9); + input.board_picker_duplicate_page_with_measurer(&TextMeasurer::default(), 9); let pages = &input.boards.board_states()[board_index].pages; assert_eq!(pages.active_index(), 10); assert_eq!(pages.page_name(10), Some("Page 10")); assert_eq!(pages.page_name(9), Some("Page 10")); - input.board_picker_delete_page_with_measurer(&crate::draw::TextMeasurer::default(), 9); + input.board_picker_delete_page_with_measurer(&TextMeasurer::default(), 9); assert_eq!( input.boards.board_states()[board_index].pages.page_count(), 13, "first delete should only request confirmation" ); - input.board_picker_delete_page_with_measurer(&crate::draw::TextMeasurer::default(), 9); + input.board_picker_delete_page_with_measurer(&TextMeasurer::default(), 9); let pages = &input.boards.board_states()[board_index].pages; assert_eq!(pages.page_count(), 12); @@ -481,7 +475,7 @@ fn board_picker_active_page_ten_visible_on_open_and_focus() { .pages .switch_to_page(9); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); update_picker_layout(&mut input, 1280, 720); let layout = *input.board_picker_layout().expect("layout"); assert_page_visible(layout, 9); @@ -496,7 +490,7 @@ fn board_picker_active_page_ten_visible_on_open_and_focus() { #[test] fn board_picker_keyboard_focus_scrolls_absolute_page_into_view() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -515,7 +509,7 @@ fn board_picker_keyboard_focus_scrolls_absolute_page_into_view() { #[test] fn board_picker_sticky_add_works_when_visible_grid_is_full() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -529,7 +523,7 @@ fn board_picker_sticky_add_works_when_visible_grid_is_full() { assert!(input.board_picker_page_add_card_at(add_x, add_y)); - input.board_picker_add_page_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_add_page_with_measurer(&TextMeasurer::default()); update_picker_layout(&mut input, 1280, 720); let pages = &input.boards.board_states()[board_index].pages; @@ -540,9 +534,9 @@ fn board_picker_sticky_add_works_when_visible_grid_is_full() { #[test] fn board_picker_ctrl_n_adds_page_while_page_panel_focused() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -568,18 +562,18 @@ fn board_picker_ctrl_n_adds_page_while_page_panel_focused() { #[test] fn board_picker_add_and_duplicate_scroll_to_newly_active_page() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); set_board_page_count(&mut input, board_index, 15); update_picker_layout(&mut input, 1280, 720); - input.board_picker_add_page_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_add_page_with_measurer(&TextMeasurer::default()); update_picker_layout(&mut input, 1280, 720); assert_page_visible(*input.board_picker_layout().expect("layout"), 15); - input.board_picker_duplicate_page_with_measurer(&crate::draw::TextMeasurer::default(), 15); + input.board_picker_duplicate_page_with_measurer(&TextMeasurer::default(), 15); update_picker_layout(&mut input, 1280, 720); assert_page_visible(*input.board_picker_layout().expect("layout"), 16); } @@ -587,7 +581,7 @@ fn board_picker_add_and_duplicate_scroll_to_newly_active_page() { #[test] fn board_picker_wheel_scroll_changes_visible_page_window() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -619,7 +613,7 @@ fn board_picker_wheel_scroll_changes_visible_page_window() { #[test] fn board_picker_repeated_wheel_scroll_uses_state_between_layouts() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -641,7 +635,7 @@ fn board_picker_repeated_wheel_scroll_uses_state_between_layouts() { #[test] fn board_picker_wheel_scroll_up_clamps_focus_to_last_visible_page() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -679,9 +673,9 @@ fn board_picker_wheel_scroll_up_clamps_focus_to_last_visible_page() { #[test] fn board_picker_page_search_wheel_scroll_syncs_cursor_with_visible_focus() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -735,9 +729,9 @@ fn board_picker_page_search_wheel_scroll_syncs_cursor_with_visible_focus() { #[test] fn board_picker_page_search_wheel_scroll_without_visible_match_clears_cursor() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -775,7 +769,7 @@ fn board_picker_page_search_wheel_scroll_without_visible_match_clears_cursor() { #[test] fn board_picker_column_change_keeps_focused_absolute_page_visible() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -796,9 +790,9 @@ fn board_picker_column_change_keeps_focused_absolute_page_visible() { #[test] fn board_picker_page_jump_focuses_absolute_page_and_scrolls_visible() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -826,9 +820,9 @@ fn board_picker_page_jump_focuses_absolute_page_and_scrolls_visible() { #[test] fn board_picker_page_jump_edges_keep_picker_open() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -872,9 +866,9 @@ fn board_picker_page_jump_edges_keep_picker_open() { #[test] fn board_picker_page_search_slash_starts_without_inserting_slash() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); update_picker_layout(&mut input, 1280, 720); input.board_picker_set_focus(BoardPickerFocus::PagePanel); @@ -889,9 +883,9 @@ fn board_picker_page_search_slash_starts_without_inserting_slash() { #[test] fn board_picker_selecting_current_board_row_clears_page_nav_mode() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); update_picker_layout(&mut input, 1280, 720); input.board_picker_set_focus(BoardPickerFocus::PagePanel); @@ -917,9 +911,9 @@ fn board_picker_selecting_current_board_row_clears_page_nav_mode() { #[test] fn board_picker_page_search_finds_named_page_beyond_visible_window() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -942,9 +936,9 @@ fn board_picker_page_search_finds_named_page_beyond_visible_window() { #[test] fn board_picker_page_search_numeric_is_exact_page_number() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -966,9 +960,9 @@ fn board_picker_page_search_numeric_is_exact_page_number() { #[test] fn board_picker_page_search_no_match_enter_is_noop() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -1000,9 +994,9 @@ fn board_picker_page_search_no_match_enter_is_noop() { #[test] fn board_picker_page_search_f3_cycles_matches() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -1030,9 +1024,9 @@ fn board_picker_page_search_f3_cycles_matches() { #[test] fn board_picker_page_search_enter_opens_absolute_page_beyond_nine() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -1060,9 +1054,9 @@ fn board_picker_page_search_enter_opens_absolute_page_beyond_nine() { #[test] fn board_picker_page_search_rename_updates_derived_match() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -1081,9 +1075,7 @@ fn board_picker_page_search_rename_updates_derived_match() { page_index: 9, buffer: "Target".to_string(), }); - assert!( - input.board_picker_commit_page_edit_with_measurer(&crate::draw::TextMeasurer::default()) - ); + assert!(input.board_picker_commit_page_edit_with_measurer(&TextMeasurer::default())); assert_eq!(input.board_picker_page_search_active_match(), Some(9)); assert_eq!(input.board_picker_page_focus_page_index(), Some(9)); @@ -1091,9 +1083,9 @@ fn board_picker_page_search_rename_updates_derived_match() { #[test] fn board_picker_page_search_pending_delete_preserves_confirmed_delete_clamps() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let board_index = input .board_picker_page_panel_board_index() .expect("page panel board index"); @@ -1114,14 +1106,14 @@ fn board_picker_page_search_pending_delete_preserves_confirmed_delete_clamps() { assert_eq!(input.board_picker_page_search_active_match(), Some(1)); assert_eq!( - input.board_picker_delete_page_with_measurer(&crate::draw::TextMeasurer::default(), 1), + input.board_picker_delete_page_with_measurer(&TextMeasurer::default(), 1), PageDeleteOutcome::Pending ); assert_eq!(input.board_picker_page_search_cursor(), Some(1)); assert_eq!(input.board_picker_page_search_active_match(), Some(1)); assert_eq!( - input.board_picker_delete_page_with_measurer(&crate::draw::TextMeasurer::default(), 1), + input.board_picker_delete_page_with_measurer(&TextMeasurer::default(), 1), PageDeleteOutcome::Removed ); assert_eq!(input.board_picker_page_search_cursor(), Some(0)); @@ -1132,7 +1124,7 @@ fn board_picker_page_search_pending_delete_preserves_confirmed_delete_clamps() { #[test] fn board_picker_row_action_hitboxes_match_rendered_positions() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); update_picker_layout(&mut input, 1280, 720); let layout = *input.board_picker_layout().expect("layout"); @@ -1164,7 +1156,7 @@ fn board_picker_row_action_hitboxes_match_rendered_positions() { #[test] fn board_picker_palette_hit_testing_uses_rendered_coordinates() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let solid_board_index = input .boards @@ -1176,7 +1168,7 @@ fn board_picker_palette_hit_testing_uses_rendered_coordinates() { .board_picker_row_for_board(solid_board_index) .expect("solid row"); input.board_picker_set_selected(solid_row); - input.board_picker_edit_color_selected_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_edit_color_selected_with_measurer(&TextMeasurer::default()); update_picker_layout(&mut input, 1280, 720); let layout = *input.board_picker_layout().expect("layout"); @@ -1195,7 +1187,7 @@ fn board_picker_palette_hit_testing_uses_rendered_coordinates() { #[test] fn board_picker_page_focus_clamps_to_existing_pages() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); update_picker_layout(&mut input, 1280, 720); let board_index = input @@ -1216,7 +1208,7 @@ fn board_picker_page_focus_clamps_to_existing_pages() { #[test] fn board_picker_footer_text_prefers_active_search_query() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker.search = "blue".to_string(); assert_eq!( @@ -1227,15 +1219,15 @@ fn board_picker_footer_text_prefers_active_search_query() { #[test] fn board_picker_footer_text_changes_for_quick_and_page_panel_modes() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); - input.open_board_picker_quick_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_quick_with_measurer(&TextMeasurer::default()); assert_eq!( input.board_picker_footer_text(), "Enter: switch Type: jump Esc: close" ); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); assert_eq!( input.board_picker_footer_text(), "Enter: open F2: rename Ctrl+C: color Ctrl+N: new Del: delete" @@ -1280,14 +1272,14 @@ fn board_picker_title_and_recent_label_reflect_mode_and_recent_boards() { "transparent".to_string(), ]); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); assert_eq!(input.board_picker_title(3, 8), "Boards (3 of 8 max)"); assert_eq!( input.board_picker_recent_label(), Some("Recent: Blackboard, Overlay".to_string()) ); - input.open_board_picker_quick_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_quick_with_measurer(&TextMeasurer::default()); assert_eq!(input.board_picker_title(3, 8), "Switch board"); } @@ -1301,12 +1293,12 @@ fn board_picker_rename_selected_promotes_quick_mode_to_full_editing() { .position(|board| board.spec.id == "blackboard") .expect("blackboard board"); - input.open_board_picker_quick_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_quick_with_measurer(&TextMeasurer::default()); let selected_row = input .board_picker_row_for_board(blackboard_index) .expect("blackboard row"); input.board_picker_set_selected(selected_row); - input.board_picker_rename_selected_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_rename_selected_with_measurer(&TextMeasurer::default()); assert_eq!(input.board_picker_mode(), BoardPickerMode::Full); assert_eq!( @@ -1317,7 +1309,7 @@ fn board_picker_rename_selected_promotes_quick_mode_to_full_editing() { #[test] fn board_picker_f2_starts_board_name_edit_not_color_edit() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); let blackboard_index = input .boards @@ -1326,7 +1318,7 @@ fn board_picker_f2_starts_board_name_edit_not_color_edit() { .position(|board| board.spec.id == "blackboard") .expect("blackboard board"); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let selected_row = input .board_picker_row_for_board(blackboard_index) .expect("blackboard row"); @@ -1349,7 +1341,7 @@ fn board_picker_f2_key_route_starts_board_name_edit_not_color_edit() { .position(|board| board.spec.id == "blackboard") .expect("blackboard board"); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let selected_row = input .board_picker_row_for_board(blackboard_index) .expect("blackboard row"); @@ -1364,7 +1356,7 @@ fn board_picker_f2_key_route_starts_board_name_edit_not_color_edit() { #[test] fn board_picker_f2_switches_color_edit_back_to_name_edit() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); let blackboard_index = input .boards @@ -1373,12 +1365,12 @@ fn board_picker_f2_switches_color_edit_back_to_name_edit() { .position(|board| board.spec.id == "blackboard") .expect("blackboard board"); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let selected_row = input .board_picker_row_for_board(blackboard_index) .expect("blackboard row"); input.board_picker_set_selected(selected_row); - input.board_picker_edit_color_selected_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_edit_color_selected_with_measurer(&TextMeasurer::default()); assert_eq!( input.board_picker_edit_state(), Some((BoardPickerEditMode::Color, selected_row, "#111111")) @@ -1394,7 +1386,7 @@ fn board_picker_f2_switches_color_edit_back_to_name_edit() { #[test] fn board_picker_ctrl_c_starts_board_color_edit() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); let blackboard_index = input .boards @@ -1403,7 +1395,7 @@ fn board_picker_ctrl_c_starts_board_color_edit() { .position(|board| board.spec.id == "blackboard") .expect("blackboard board"); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let selected_row = input .board_picker_row_for_board(blackboard_index) .expect("blackboard row"); @@ -1427,13 +1419,13 @@ fn board_picker_edit_color_selected_shows_info_toast_for_transparent_board() { .position(|board| board.spec.background.is_transparent()) .expect("transparent board"); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected( input .board_picker_row_for_board(transparent_index) .expect("transparent row"), ); - input.board_picker_edit_color_selected_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_edit_color_selected_with_measurer(&TextMeasurer::default()); assert!(input.board_picker_edit_state().is_none()); assert_eq!( @@ -1452,7 +1444,7 @@ fn board_picker_commit_edit_rejects_invalid_colors_and_keeps_edit_open() { .position(|board| board.spec.id == "blackboard") .expect("blackboard board"); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let selected_row = input .board_picker_row_for_board(blackboard_index) .expect("blackboard row"); @@ -1494,7 +1486,7 @@ fn open_board_picker_closes_help_and_clears_transient_picker_state() { buffer: "Draft".to_string(), }); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); assert!(input.pending_onboarding_usage.used_board_picker); assert!(input.is_board_picker_open()); @@ -1514,7 +1506,7 @@ fn open_board_picker_closes_help_and_clears_transient_picker_state() { #[test] fn close_board_picker_clears_transient_picker_state() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker.search = "blue".to_string(); input.board_picker.drag = Some(BoardPickerDrag { source_row: 0, @@ -1547,7 +1539,7 @@ fn close_board_picker_clears_transient_picker_state() { #[test] fn board_picker_active_index_prefers_hover_over_selected_row() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected(0); if let BoardPickerState::Open { hover_index, .. } = &mut input.board_picker.state { @@ -1560,7 +1552,7 @@ fn board_picker_active_index_prefers_hover_over_selected_row() { #[test] fn board_picker_page_panel_board_index_falls_back_to_active_board_for_new_row() { let mut input = create_test_input_state(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected(input.boards.board_count()); assert_eq!( @@ -1573,11 +1565,11 @@ fn board_picker_page_panel_board_index_falls_back_to_active_board_for_new_row() fn toggle_board_picker_quick_opens_quick_mode_and_closes_on_second_toggle() { let mut input = create_test_input_state(); - input.toggle_board_picker_quick_with(&crate::draw::TextMeasurer::default()); + input.toggle_board_picker_quick_with(&TextMeasurer::default()); assert!(input.is_board_picker_open()); assert_eq!(input.board_picker_mode(), BoardPickerMode::Quick); - input.toggle_board_picker_quick_with(&crate::draw::TextMeasurer::default()); + input.toggle_board_picker_quick_with(&TextMeasurer::default()); assert!(!input.is_board_picker_open()); } @@ -1585,12 +1577,9 @@ fn toggle_board_picker_quick_opens_quick_mode_and_closes_on_second_toggle() { fn board_picker_activate_row_on_new_row_creates_board_and_starts_editing() { let mut input = create_test_input_state(); let initial_count = input.boards.board_count(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); - input.board_picker_activate_row_with_measurer( - &crate::draw::TextMeasurer::default(), - initial_count, - ); + input.board_picker_activate_row_with_measurer(&TextMeasurer::default(), initial_count); let active_row = input .board_picker_row_for_board(input.boards.active_index()) @@ -1616,14 +1605,14 @@ fn board_picker_activate_page_switches_board_page_and_closes_picker() { .position(|board| board.spec.id == "whiteboard") .expect("whiteboard board"); set_board_page_count(&mut input, whiteboard_index, 2); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected( input .board_picker_row_for_board(whiteboard_index) .expect("whiteboard row"), ); - input.board_picker_activate_page_with_measurer(&crate::draw::TextMeasurer::default(), 1); + input.board_picker_activate_page_with_measurer(&TextMeasurer::default(), 1); assert_eq!(input.board_id(), "whiteboard"); assert_eq!(input.boards.active_board().pages.active_index(), 1); @@ -1640,14 +1629,14 @@ fn board_picker_activate_page_ignores_out_of_range_indices() { .position(|board| board.spec.id == "whiteboard") .expect("whiteboard board"); set_board_page_count(&mut input, whiteboard_index, 1); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected( input .board_picker_row_for_board(whiteboard_index) .expect("whiteboard row"), ); - input.board_picker_activate_page_with_measurer(&crate::draw::TextMeasurer::default(), 5); + input.board_picker_activate_page_with_measurer(&TextMeasurer::default(), 5); assert_eq!(input.board_id(), "transparent"); assert!(input.is_board_picker_open()); @@ -1657,9 +1646,9 @@ fn board_picker_activate_page_ignores_out_of_range_indices() { fn board_picker_create_new_from_quick_mode_promotes_to_full_and_starts_editing() { let mut input = create_test_input_state(); let initial_count = input.boards.board_count(); - input.open_board_picker_quick_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_quick_with_measurer(&TextMeasurer::default()); - input.board_picker_create_new_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_create_new_with_measurer(&TextMeasurer::default()); let active_row = input .board_picker_row_for_board(input.boards.active_index()) @@ -1686,14 +1675,14 @@ fn board_picker_duplicate_page_uses_selected_page_panel_board() { .position(|board| board.spec.id == "blackboard") .expect("blackboard board"); set_board_page_count(&mut input, blackboard_index, 1); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected( input .board_picker_row_for_board(blackboard_index) .expect("blackboard row"), ); - input.board_picker_duplicate_page_with_measurer(&crate::draw::TextMeasurer::default(), 0); + input.board_picker_duplicate_page_with_measurer(&TextMeasurer::default(), 0); assert_eq!( input.boards.board_states()[blackboard_index] @@ -1713,14 +1702,14 @@ fn board_picker_add_page_uses_selected_page_panel_board() { .position(|board| board.spec.id == "blackboard") .expect("blackboard board"); set_board_page_count(&mut input, blackboard_index, 1); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected( input .board_picker_row_for_board(blackboard_index) .expect("blackboard row"), ); - input.board_picker_add_page_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_add_page_with_measurer(&TextMeasurer::default()); assert_eq!( input.boards.board_states()[blackboard_index] @@ -1740,14 +1729,14 @@ fn board_picker_delete_page_requires_confirmation_for_multi_page_boards() { .position(|board| board.spec.id == "blackboard") .expect("blackboard board"); set_board_page_count(&mut input, blackboard_index, 2); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected( input .board_picker_row_for_board(blackboard_index) .expect("blackboard row"), ); - input.board_picker_delete_page_with_measurer(&crate::draw::TextMeasurer::default(), 1); + input.board_picker_delete_page_with_measurer(&TextMeasurer::default(), 1); assert_eq!( input.boards.board_states()[blackboard_index] .pages @@ -1760,7 +1749,7 @@ fn board_picker_delete_page_requires_confirmation_for_multi_page_boards() { .is_some_and(|toast| toast.message.contains("Click delete again to confirm.")) ); - input.board_picker_delete_page_with_measurer(&crate::draw::TextMeasurer::default(), 1); + input.board_picker_delete_page_with_measurer(&TextMeasurer::default(), 1); assert_eq!( input.boards.board_states()[blackboard_index] .pages @@ -1773,10 +1762,10 @@ fn board_picker_delete_page_requires_confirmation_for_multi_page_boards() { fn board_picker_delete_selected_ignores_new_row() { let mut input = create_test_input_state(); let initial_count = input.boards.board_count(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected(initial_count); - input.board_picker_delete_selected_with_measurer(&crate::draw::TextMeasurer::default()); + input.board_picker_delete_selected_with_measurer(&TextMeasurer::default()); assert_eq!(input.boards.board_count(), initial_count); assert_eq!(input.board_picker_selected_index(), Some(initial_count)); @@ -1786,7 +1775,7 @@ fn board_picker_delete_selected_ignores_new_row() { fn board_picker_toggle_pin_selected_ignores_new_row() { let mut input = create_test_input_state(); let board_count = input.boards.board_count(); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); input.board_picker_set_selected(board_count); let pinned_before = input.board_picker_pinned_count(); @@ -1799,11 +1788,11 @@ fn board_picker_toggle_pin_selected_ignores_new_row() { #[test] fn board_pin_runtime_request_is_not_gated_by_config_customization_persistence() { let mut input = create_test_input_state(); - let boards = crate::config::BoardsConfig { + let boards = BoardsConfig { persist_customizations: false, - ..crate::config::BoardsConfig::default() + ..BoardsConfig::default() }; - input.boards = crate::input::boards::BoardManager::from_config(boards); + input.boards = BoardManager::from_config(boards); let whiteboard = input .boards .board_states() @@ -1828,12 +1817,12 @@ fn board_picker_activate_existing_row_switches_board_and_closes_picker() { .iter() .position(|board| board.spec.id == "blackboard") .expect("blackboard board"); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); let row = input .board_picker_row_for_board(blackboard_index) .expect("blackboard row"); - input.board_picker_activate_row_with_measurer(&crate::draw::TextMeasurer::default(), row); + input.board_picker_activate_row_with_measurer(&TextMeasurer::default(), row); assert_eq!(input.board_id(), "blackboard"); assert!(!input.is_board_picker_open()); diff --git a/src/input/state/tests/boards.rs b/src/input/state/tests/boards.rs index e11a7d9d5..e6af07d1e 100644 --- a/src/input/state/tests/boards.rs +++ b/src/input/state/tests/boards.rs @@ -1,3 +1,11 @@ +use std::fs; + +use crate::config; +use crate::config::test_helpers::ConfigFileSnapshot; +use crate::config::{Config, test_helpers}; +use crate::draw::TextMeasurer; +use crate::input::state; + use super::*; use crate::draw::{EmbeddedImage, ShapeId}; use crate::input::state::core::board_picker::BoardPickerState; @@ -79,7 +87,7 @@ fn switch_board_recent_shows_toast_when_no_other_recent_board_exists() { #[test] fn switch_board_updates_open_board_picker_selection_and_clears_hover() { let mut state = create_test_input_state(); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); if let BoardPickerState::Open { hover_index, .. } = &mut state.board_picker.state { *hover_index = Some(0); @@ -143,7 +151,7 @@ fn failed_switch_board_preserves_active_interaction() { #[test] fn switch_board_cancels_text_edit_on_source_board_before_switching() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Text { @@ -170,7 +178,7 @@ fn switch_board_cancels_text_edit_on_source_board_before_switching() { #[test] fn switch_board_cancels_selection_move_on_source_board_before_switching() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Rect { @@ -187,7 +195,7 @@ fn switch_board_cancels_selection_move_on_source_board_before_switching() { let snapshots = state.capture_movable_selection_snapshots(); assert!(state.apply_translation_to_selection_with(&test_text_measurer, 25, 35)); state.state = DrawingState::MovingSelection { - grab: crate::input::state::SelectionGrab::new(shape_id, 25, 35), + grab: state::SelectionGrab::new(shape_id, 25, 35), last_x: 25, last_y: 35, snapshots, @@ -244,7 +252,7 @@ fn assert_board_transition_dropped_page_local_state( assert!(!state.is_context_menu_open()); let shape_count = state.boards.active_frame().shapes.len(); - assert!(!state.delete_selection_with(&crate::draw::TextMeasurer::default())); + assert!(!state.delete_selection_with(&TextMeasurer::default())); assert_eq!(state.boards.active_frame().shapes.len(), shape_count); assert_eq!( @@ -445,7 +453,7 @@ fn duplicate_board_preflight_handles_existing_copy_board_when_over_image_limit() #[test] fn duplicate_board_cancels_text_edit_before_cloning_board() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.switch_board(BOARD_ID_WHITEBOARD); @@ -567,24 +575,22 @@ fn create_board_adds_board_and_emits_toast() { /// keeps its bytes, its metadata, and its neighbours through every one of them. #[test] fn live_board_edits_leave_the_config_file_untouched() { - crate::config::test_helpers::with_temp_config_home(|config_root| { - let config_dir = config_root.join(crate::config::PRIMARY_CONFIG_DIR); - std::fs::create_dir_all(&config_dir).expect("test config directory"); + test_helpers::with_temp_config_home(|config_root| { + let config_dir = config_root.join(config::PRIMARY_CONFIG_DIR); + fs::create_dir_all(&config_dir).expect("test config directory"); let path = config_dir.join("config.toml"); // `persist_customizations` is still parsed, and it no longer buys the // overlay permission to write anything. - std::fs::write(&path, "[boards]\npersist_customizations = true\n") + fs::write(&path, "[boards]\npersist_customizations = true\n") .expect("test config should be written"); - let snapshot = crate::config::test_helpers::ConfigFileSnapshot::capture(&path); + let snapshot = ConfigFileSnapshot::capture(&path); - let configured = crate::config::Config::load() - .expect("test config should load") - .config; + let configured = Config::load().expect("test config should load").config; let mut state = create_test_input_state(); state.boards = BoardManager::from_config(configured.resolved_boards()); let whiteboard = board_index(&state, BOARD_ID_WHITEBOARD); - assert!(state.set_board_name(whiteboard, "Renamed this run".to_string())); + assert!(state.set_board_name(whiteboard, "Renamed this run")); assert!(state.set_board_background_color( whiteboard, Color { @@ -601,9 +607,7 @@ fn live_board_edits_leave_the_config_file_untouched() { // Restart semantics: the next process seeds from the authored template, // not from the boards this run edited. - let restarted = crate::config::Config::load() - .expect("test config should reload") - .config; + let restarted = Config::load().expect("test config should reload").config; assert_eq!( restarted .resolved_boards() diff --git a/src/input/state/tests/delete_restore.rs b/src/input/state/tests/delete_restore.rs index db6a7cba7..8fe751c92 100644 --- a/src/input/state/tests/delete_restore.rs +++ b/src/input/state/tests/delete_restore.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::*; use crate::draw::{Frame, PageDeleteOutcome, ShapeId}; use crate::input::boards::PendingBoardRuntimeUiAction; @@ -81,7 +83,7 @@ fn delete_active_board_requires_confirmation_then_restore_recovers_board() { #[test] fn delete_active_board_restore_preserves_cancelled_text_edit() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.switch_board(BOARD_ID_BLACKBOARD); @@ -222,7 +224,7 @@ fn page_delete_requires_confirmation_and_restore_recovers_deleted_page() { #[test] fn page_delete_restore_preserves_cancelled_text_edit() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let board = board_index(&state, BOARD_ID_BLACKBOARD); @@ -289,7 +291,7 @@ fn page_delete_on_last_page_clears_shapes_without_removing_page() { #[test] fn pending_board_delete_survives_active_drift_and_deletes_original_board() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.switch_board(BOARD_ID_BLACKBOARD); let requested_at = Instant::now(); @@ -305,14 +307,14 @@ fn pending_board_delete_survives_active_drift_and_deletes_original_board() { #[test] fn board_rename_does_not_stale_pending_board_delete() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.switch_board(BOARD_ID_BLACKBOARD); let requested_at = Instant::now(); state.delete_active_board_at_with_measurer(&measurer, requested_at); let index = board_index(&state, BOARD_ID_BLACKBOARD); - assert!(state.set_board_name(index, "Renamed Board".to_string())); + assert!(state.set_board_name(index, "Renamed Board")); state.delete_active_board_at_with_measurer(&measurer, requested_at + Duration::from_millis(1)); assert!(!state.boards.has_board(BOARD_ID_BLACKBOARD)); @@ -324,7 +326,7 @@ fn board_rename_does_not_stale_pending_board_delete() { #[test] fn page_content_edit_does_not_stale_pending_page_delete() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let board = board_index(&state, BOARD_ID_BLACKBOARD); state.switch_board(BOARD_ID_BLACKBOARD); @@ -355,7 +357,7 @@ fn page_content_edit_does_not_stale_pending_page_delete() { #[test] fn pending_page_delete_survives_active_board_drift_and_deletes_original_page() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let blackboard = board_index(&state, BOARD_ID_BLACKBOARD); state.switch_board(BOARD_ID_BLACKBOARD); @@ -407,7 +409,7 @@ fn pending_page_delete_survives_active_board_drift_and_deletes_original_page() { #[test] fn stale_active_page_delete_confirmation_does_not_cancel_active_interaction() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let board = board_index(&state, BOARD_ID_BLACKBOARD); state.switch_board(BOARD_ID_BLACKBOARD); @@ -446,7 +448,7 @@ fn stale_active_page_delete_confirmation_does_not_cancel_active_interaction() { #[test] fn stale_board_panel_page_delete_confirmation_does_not_cancel_active_interaction() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let board = board_index(&state, BOARD_ID_BLACKBOARD); state.switch_board(BOARD_ID_BLACKBOARD); diff --git a/src/input/state/tests/drawing.rs b/src/input/state/tests/drawing.rs index 493ab49b8..afcf93825 100644 --- a/src/input/state/tests/drawing.rs +++ b/src/input/state/tests/drawing.rs @@ -1,3 +1,10 @@ +use std::f64::consts; + +use crate::draw::{PolygonKind, TextMeasurer, shape}; +use crate::input::DragTool; +use crate::ui_text::UiTextEngine; +use crate::util::Rect; + use super::*; use crate::draw::BlurStyle; use crate::input::{DragBinding, DragButtonBindings, DragToolBindings}; @@ -99,7 +106,7 @@ fn live_shape_strokes_preview_and_commit_lines_and_circles() { state.on_mouse_press(MouseButton::Left, 120, 80); for index in 1..=32 { - let angle = std::f64::consts::TAU * f64::from(index) / 32.0; + let angle = consts::TAU * f64::from(index) / 32.0; state.on_mouse_motion( 80 + (40.0 * angle.cos()).round() as i32, 80 + (40.0 * angle.sin()).round() as i32, @@ -229,7 +236,7 @@ fn live_shape_previews_and_commits_hand_drawn_rectangles_and_ovals() { state.style.shape_recognition_sensitivity = 0; state.on_mouse_press(MouseButton::Left, 260, 80); for index in 1..=32 { - let angle = std::f64::consts::TAU * f64::from(index) / 32.0; + let angle = consts::TAU * f64::from(index) / 32.0; state.on_mouse_motion( 200 + (60.0 * angle.cos()).round() as i32, 80 + (35.0 * angle.sin()).round() as i32, @@ -303,7 +310,7 @@ fn live_shape_sensitivity_controls_rough_closed_shapes() { ]; let oval: Vec<_> = (0..=32) .map(|index| { - let angle = std::f64::consts::TAU * f64::from(index) / 32.0; + let angle = consts::TAU * f64::from(index) / 32.0; let wobble = 1.0 + 0.13 * (3.0 * angle).sin(); ( 200 + (50.0 * wobble * angle.cos()).round() as i32, @@ -449,7 +456,7 @@ fn regular_polygon_drag_stores_concrete_points_and_side_metadata() { let shape = &state.boards.active_frame().shapes[0].shape; match shape { Shape::Polygon { - kind: crate::draw::PolygonKind::Regular { sides }, + kind: PolygonKind::Regular { sides }, points, .. } => { @@ -462,7 +469,7 @@ fn regular_polygon_drag_stores_concrete_points_and_side_metadata() { assert!(state.set_polygon_sides(3)); match &state.boards.active_frame().shapes[0].shape { Shape::Polygon { - kind: crate::draw::PolygonKind::Regular { sides }, + kind: PolygonKind::Regular { sides }, points, .. } => { @@ -497,7 +504,7 @@ fn invalid_drag_polygon_tools_do_not_commit_ghost_shapes() { fn alt_click_selects_filled_polygon_interior() { let mut state = create_test_input_state(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Polygon { - kind: crate::draw::PolygonKind::Triangle, + kind: PolygonKind::Triangle, points: vec![(10, 10), (40, 10), (25, 40)], fill: true, fill_color: None, @@ -524,7 +531,7 @@ fn freeform_polygon_double_click_finishes_without_duplicate_vertex() { assert!(matches!(state.state, DrawingState::Idle)); match &state.boards.active_frame().shapes[0].shape { Shape::Polygon { - kind: crate::draw::PolygonKind::Freeform, + kind: PolygonKind::Freeform, points, .. } => assert_eq!(points, &vec![(0, 0), (20, 0), (20, 20)]), @@ -534,7 +541,7 @@ fn freeform_polygon_double_click_finishes_without_duplicate_vertex() { #[test] fn freeform_polygon_backspace_does_not_prime_double_click_commit() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); assert!(state.set_tool_override(Some(Tool::FreeformPolygon))); @@ -556,7 +563,7 @@ fn freeform_polygon_backspace_does_not_prime_double_click_commit() { #[test] fn freeform_polygon_commit_records_first_stroke_onboarding() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); assert!(state.set_tool_override(Some(Tool::FreeformPolygon))); @@ -580,7 +587,7 @@ fn freeform_polygon_preview_dirty_has_antialias_padding() { let DrawingState::BuildingPolygon { thick, .. } = state.state else { panic!("expected polygon building state"); }; - let base = crate::draw::shape::bounding_box_for_points(&[(10, 10), (20, 20)], thick) + let base = shape::bounding_box_for_points(&[(10, 10), (20, 20)], thick) .expect("preview should have bounds"); assert_eq!( state.provisional_bounds(), @@ -603,11 +610,11 @@ fn append_path_motion_dirties_only_new_tail_segment() { state.on_mouse_motion(30, 10); let dirty = state.take_dirty_regions(); let thick = state.thickness_for_tool(Tool::Pen); - let tail_bounds = crate::draw::shape::bounding_box_for_points(&[(20, 10), (30, 10)], thick) + let tail_bounds = shape::bounding_box_for_points(&[(20, 10), (30, 10)], thick) .expect("tail segment should have bounds"); - let full_bounds = crate::draw::shape::bounding_box_for_points(&[(10, 10), (30, 10)], thick) + let full_bounds = shape::bounding_box_for_points(&[(10, 10), (30, 10)], thick) .expect("full provisional stroke should have bounds"); - let head_probe = crate::util::Rect::new(10, 10, 1, 1).unwrap(); + let head_probe = Rect::new(10, 10, 1, 1).unwrap(); assert!( dirty @@ -640,9 +647,8 @@ fn append_path_long_diagonal_motion_splits_tail_damage() { state.on_mouse_motion(900, 900); let dirty = state.take_dirty_regions(); let thick = state.thickness_for_tool(Tool::Pen); - let full_tail_bounds = - crate::draw::shape::bounding_box_for_points(&[(10, 10), (900, 900)], thick) - .expect("long tail segment should have bounds"); + let full_tail_bounds = shape::bounding_box_for_points(&[(10, 10), (900, 900)], thick) + .expect("long tail segment should have bounds"); assert!( dirty.len() > 1, @@ -675,9 +681,8 @@ fn append_path_long_diagonal_release_splits_finished_path_damage() { state.on_mouse_release(MouseButton::Left, 900, 900); let dirty = state.take_dirty_regions(); let thick = state.thickness_for_tool(Tool::Pen); - let full_path_bounds = - crate::draw::shape::bounding_box_for_points(&[(10, 10), (900, 900)], thick) - .expect("long finished path should have bounds"); + let full_path_bounds = shape::bounding_box_for_points(&[(10, 10), (900, 900)], thick) + .expect("long finished path should have bounds"); assert_eq!(state.boards.active_frame().shapes.len(), 1); assert_eq!(state.provisional_bounds(), None); @@ -727,13 +732,13 @@ fn pressure_preview_release_cleans_wide_preview_when_final_freehand_narrows() { let _ = state.take_dirty_regions(); state.on_mouse_press(MouseButton::Left, 10, 100); - state.set_pressure_thickness_for_active_tool_with(&crate::draw::TextMeasurer::default(), 32.0); + state.set_pressure_thickness_for_active_tool_with(&TextMeasurer::default(), 32.0); state.on_mouse_motion(900, 100); let wide_preview_bounds = state .provisional_bounds() .expect("wide pressure preview should track cleanup bounds"); - let wide_only_probe = crate::util::Rect::new(450, wide_preview_bounds.y, 1, 1).unwrap(); - let final_bounds = crate::draw::shape::bounding_box_for_points(&[(10, 100), (900, 100)], 2.0) + let wide_only_probe = Rect::new(450, wide_preview_bounds.y, 1, 1).unwrap(); + let final_bounds = shape::bounding_box_for_points(&[(10, 100), (900, 100)], 2.0) .expect("final freehand should have bounds"); assert!( !test_rects_intersect(final_bounds, wide_only_probe), @@ -741,7 +746,7 @@ fn pressure_preview_release_cleans_wide_preview_when_final_freehand_narrows() { ); let _ = state.take_dirty_regions(); - state.set_pressure_thickness_for_active_tool_with(&crate::draw::TextMeasurer::default(), 2.0); + state.set_pressure_thickness_for_active_tool_with(&TextMeasurer::default(), 2.0); let _ = state.take_dirty_regions(); state.on_mouse_release(MouseButton::Left, 900, 100); let dirty = state.take_dirty_regions(); @@ -807,10 +812,10 @@ fn pressure_sample_shrink_dirties_previous_full_provisional_bounds() { let old_full_bounds = state .provisional_bounds() .expect("initial pressure preview should have bounds"); - let old_only_probe = crate::util::Rect::new(10, old_full_bounds.y, 1, 1).unwrap(); + let old_only_probe = Rect::new(10, old_full_bounds.y, 1, 1).unwrap(); let _ = state.take_dirty_regions(); - state.set_pressure_thickness_for_active_tool_with(&crate::draw::TextMeasurer::default(), 2.0); + state.set_pressure_thickness_for_active_tool_with(&TextMeasurer::default(), 2.0); state.on_mouse_motion(30, 10); let dirty = state.take_dirty_regions(); @@ -836,13 +841,12 @@ fn marker_size_increase_updates_accumulated_cleanup_bounds() { assert!(state.set_thickness(32.0)); let _ = state.take_dirty_regions(); - state.cancel_active_interaction_with(&crate::draw::TextMeasurer::default()); + state.cancel_active_interaction_with(&TextMeasurer::default()); let dirty = state.take_dirty_regions(); let marker_width = (32.0f64 * 1.35).max(32.0 + 1.0); - let expanded_bounds = - crate::draw::shape::bounding_box_for_points(&[(10, 10), (20, 10)], marker_width) - .expect("expanded marker preview should have bounds"); - let expanded_only_probe = crate::util::Rect::new(10, expanded_bounds.y, 1, 1).unwrap(); + let expanded_bounds = shape::bounding_box_for_points(&[(10, 10), (20, 10)], marker_width) + .expect("expanded marker preview should have bounds"); + let expanded_only_probe = Rect::new(10, expanded_bounds.y, 1, 1).unwrap(); assert!( dirty @@ -867,11 +871,11 @@ fn eraser_size_increase_updates_accumulated_cleanup_bounds() { assert!(state.set_eraser_size(32.0)); let _ = state.take_dirty_regions(); - state.cancel_active_interaction_with(&crate::draw::TextMeasurer::default()); + state.cancel_active_interaction_with(&TextMeasurer::default()); let dirty = state.take_dirty_regions(); - let expanded_bounds = crate::draw::shape::bounding_box_for_eraser(&[(10, 10), (20, 10)], 32.0) + let expanded_bounds = shape::bounding_box_for_eraser(&[(10, 10), (20, 10)], 32.0) .expect("expanded eraser preview should have bounds"); - let expanded_only_probe = crate::util::Rect::new(10, expanded_bounds.y, 1, 1).unwrap(); + let expanded_only_probe = Rect::new(10, expanded_bounds.y, 1, 1).unwrap(); assert!( dirty @@ -893,10 +897,10 @@ fn cancel_active_path_dirties_full_accumulated_provisional_bounds() { state.on_mouse_motion(30, 10); let _ = state.take_dirty_regions(); - state.cancel_active_interaction_with(&crate::draw::TextMeasurer::default()); + state.cancel_active_interaction_with(&TextMeasurer::default()); let dirty = state.take_dirty_regions(); let thick = state.thickness_for_tool(Tool::Pen); - let full_bounds = crate::draw::shape::bounding_box_for_points(&[(10, 10), (30, 10)], thick) + let full_bounds = shape::bounding_box_for_points(&[(10, 10), (30, 10)], thick) .expect("full provisional stroke should have bounds"); assert!( @@ -910,12 +914,12 @@ fn cancel_active_path_dirties_full_accumulated_provisional_bounds() { #[test] fn freeform_polygon_freezes_style_on_first_click() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); assert!(state.set_tool_override(Some(Tool::FreeformPolygon))); let original = state.style.current_color; - let changed = crate::draw::Color { + let changed = Color { r: 0.0, g: 0.0, b: 1.0, @@ -1078,7 +1082,7 @@ fn drag_binding_color_overrides_stroke_without_changing_current_color() { a: 1.0, }; let mut bindings = DragToolBindings::default(); - bindings.right.drag = DragBinding::new(crate::input::DragTool::Pen, Some(blue)); + bindings.right.drag = DragBinding::new(DragTool::Pen, Some(blue)); assert!(state.set_drag_tool_bindings(bindings)); state.on_mouse_press(MouseButton::Right, 0, 0); @@ -1094,8 +1098,8 @@ fn drag_binding_color_overrides_stroke_without_changing_current_color() { #[test] fn toggle_click_highlight_action_changes_state() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -1149,8 +1153,8 @@ fn toolbar_select_highlight_sticks_when_highlight_is_active_via_modifier() { #[test] fn highlight_tool_prevents_drawing() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -1244,10 +1248,10 @@ fn provisional_shape_readout_excludes_freehand_and_non_size_badge_tools() { let mut state = create_test_input_state(); // A single Shape Pen point is still ink, so it has no readout either. - for tool in Tool::ALL + let tools_without_readout = Tool::ALL .into_iter() - .filter(|tool| !matches!(tool, Tool::Rect | Tool::Ellipse)) - { + .filter(|tool| !matches!(tool, Tool::Rect | Tool::Ellipse)); + for tool in tools_without_readout { state.state = DrawingState::Drawing { tool, start_x: 10, @@ -1263,7 +1267,7 @@ fn provisional_shape_readout_excludes_freehand_and_non_size_badge_tools() { assert_eq!(state.provisional_shape_readout(40, 70), None); } -fn test_rects_intersect(a: crate::util::Rect, b: crate::util::Rect) -> bool { +fn test_rects_intersect(a: Rect, b: Rect) -> bool { let a_right = a.x.saturating_add(a.width); let a_bottom = a.y.saturating_add(a.height); let b_right = b.x.saturating_add(b.width); diff --git a/src/input/state/tests/erase.rs b/src/input/state/tests/erase.rs index d3b7be26b..69eb9990e 100644 --- a/src/input/state/tests/erase.rs +++ b/src/input/state/tests/erase.rs @@ -1,9 +1,13 @@ +use std::f64::consts; + +use crate::draw::TextMeasurer; + use super::*; use crate::draw::ArrowStyle; #[test] fn erase_stroke_samples_sparse_path() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.style.eraser_size = 4.0; @@ -57,7 +61,7 @@ fn erase_stroke_includes_release_segment() { #[test] fn erase_stroke_skips_locked_shapes() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.style.eraser_size = 4.0; @@ -102,7 +106,7 @@ fn erase_stroke_skips_locked_shapes() { #[test] fn erase_stroke_samples_randomized_crossings() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); fn next_unit(seed: &mut u64) -> f64 { *seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); @@ -131,7 +135,7 @@ fn erase_stroke_samples_randomized_crossings() { }); let unit = next_unit(&mut seed); - let angle = std::f64::consts::PI * (0.35 + unit * 0.3); + let angle = consts::PI * (0.35 + unit * 0.3); let dx = angle.cos(); let dy = angle.sin(); let length = 80.0; @@ -159,7 +163,7 @@ fn erase_stroke_samples_randomized_crossings() { #[test] fn erase_stroke_hits_various_shapes() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let cases = vec![ ( @@ -238,7 +242,7 @@ fn erase_stroke_hits_various_shapes() { /// Uses a low threshold to force the grid path to be exercised. #[test] fn spatial_grid_eraser_hits_after_add_move_delete() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.style.eraser_size = 10.0; @@ -324,7 +328,7 @@ fn spatial_grid_eraser_hits_after_add_move_delete() { /// Tests that tolerance larger than cell size still finds shapes. #[test] fn spatial_grid_large_tolerance_finds_distant_shapes() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.set_hit_test_threshold(2); state.set_hit_test_tolerance(100.0); // Larger than cell size (64) diff --git a/src/input/state/tests/focus_mode.rs b/src/input/state/tests/focus_mode.rs index 8fd8fc27f..738163059 100644 --- a/src/input/state/tests/focus_mode.rs +++ b/src/input/state/tests/focus_mode.rs @@ -1,5 +1,8 @@ //! Focus mode: snapshot-hide of every chrome surface and exact restore. +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::create_test_input_state; use crate::config::{ Action, ColorSpec, StatusBarItem, StatusBarStyle, StatusPosition, ToolPresetConfig, @@ -10,8 +13,8 @@ use crate::input::state::{Toast, ToastPriority}; #[test] fn focus_mode_hides_all_chrome_and_restores_exactly() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -56,8 +59,8 @@ fn focus_mode_hides_all_chrome_and_restores_exactly() { #[test] fn focus_mode_toast_offers_restore_action() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -78,8 +81,8 @@ fn focus_mode_toast_offers_restore_action() { #[test] fn focus_mode_suppresses_fallback_mode_badges_but_keeps_restore_toast() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -110,8 +113,8 @@ fn focus_mode_suppresses_fallback_mode_badges_but_keeps_restore_toast() { #[test] fn manual_chrome_toggle_breaks_focus_mode() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -145,8 +148,8 @@ fn manual_chrome_toggle_breaks_focus_mode() { #[test] fn breaking_focus_mode_retracts_a_restore_toast_queued_behind_a_warning() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -182,8 +185,8 @@ fn breaking_focus_mode_retracts_a_restore_toast_queued_behind_a_warning() { #[test] fn preset_status_bar_update_stays_hidden_until_focus_mode_restores_it() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -230,8 +233,8 @@ fn preset_status_bar_update_stays_hidden_until_focus_mode_restores_it() { #[test] fn focus_mode_rescues_a_fully_hidden_ui() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -276,8 +279,8 @@ fn focus_mode_rescues_a_fully_hidden_ui() { #[test] fn focus_mode_rescues_when_the_enabled_status_bar_has_no_visible_content() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -289,8 +292,8 @@ fn focus_mode_rescues_when_the_enabled_status_bar_has_no_visible_content() { state.ui_visibility.show_zoom_chip = false; for item in StatusBarItem::ALL { state.set_status_bar_item_visible_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), item, false, ); @@ -324,8 +327,8 @@ fn focus_mode_rescues_when_the_enabled_status_bar_has_no_visible_content() { #[test] fn focus_mode_hides_a_floating_badge_when_it_is_the_only_visible_chrome() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -355,8 +358,8 @@ fn focus_mode_hides_a_floating_badge_when_it_is_the_only_visible_chrome() { #[test] fn focus_mode_hides_a_zoom_badge_when_it_is_the_only_visible_chrome() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -384,8 +387,8 @@ fn focus_mode_hides_a_zoom_badge_when_it_is_the_only_visible_chrome() { #[test] fn focus_mode_hides_a_fallback_badge_when_the_enabled_status_bar_is_empty() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -398,8 +401,8 @@ fn focus_mode_hides_a_fallback_badge_when_the_enabled_status_bar_is_empty() { state.set_zoom_status(true, false, 2.0, (0.0, 0.0)); for item in StatusBarItem::ALL { state.set_status_bar_item_visible_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), item, false, ); @@ -424,8 +427,8 @@ fn focus_mode_hides_a_fallback_badge_when_the_enabled_status_bar_is_empty() { #[test] fn focus_mode_never_enqueues_persistence() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -450,8 +453,8 @@ fn focus_mode_never_enqueues_persistence() { /// transient and neither of them queues durable work. #[test] fn visibility_toggles_stay_process_only_across_focus_mode() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -480,8 +483,8 @@ fn visibility_toggles_stay_process_only_across_focus_mode() { #[test] fn presenter_mode_gates_focus_mode() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -499,8 +502,8 @@ fn presenter_mode_gates_focus_mode() { #[test] fn focus_mode_exits_light_mode_before_taking_ownership() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -524,8 +527,8 @@ fn focus_mode_exits_light_mode_before_taking_ownership() { #[test] fn light_mode_exits_focus_mode_before_taking_ownership() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -549,8 +552,8 @@ fn light_mode_exits_focus_mode_before_taking_ownership() { #[test] fn unsupported_light_mode_does_not_break_focus_mode() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -573,8 +576,8 @@ fn unsupported_light_mode_does_not_break_focus_mode() { #[test] fn light_mode_drawing_exits_focus_mode_before_taking_ownership() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, diff --git a/src/input/state/tests/helpers.rs b/src/input/state/tests/helpers.rs index 7425878cd..319df4a95 100644 --- a/src/input/state/tests/helpers.rs +++ b/src/input/state/tests/helpers.rs @@ -1,20 +1,23 @@ +use crate::config::KeybindingsConfig; +use crate::input::state::test_support::TestInputStateBuilder; + use super::*; pub(super) fn create_test_input_state() -> InputState { - create_test_input_state_with_keybindings(crate::config::KeybindingsConfig::default()) + create_test_input_state_with_keybindings(&KeybindingsConfig::default()) } pub(super) fn create_test_input_state_with_click_highlight( click_highlight_settings: ClickHighlightSettings, ) -> InputState { create_test_input_state_with_keybindings_and_click_highlight( - crate::config::KeybindingsConfig::default(), + &KeybindingsConfig::default(), click_highlight_settings, ) } pub(super) fn create_test_input_state_with_keybindings( - keybindings: crate::config::KeybindingsConfig, + keybindings: &KeybindingsConfig, ) -> InputState { create_test_input_state_with_keybindings_and_click_highlight( keybindings, @@ -23,10 +26,10 @@ pub(super) fn create_test_input_state_with_keybindings( } fn create_test_input_state_with_keybindings_and_click_highlight( - keybindings: crate::config::KeybindingsConfig, + keybindings: &KeybindingsConfig, click_highlight_settings: ClickHighlightSettings, ) -> InputState { - crate::input::state::test_support::TestInputStateBuilder::with_keybindings(keybindings) + TestInputStateBuilder::with_keybindings(keybindings) .thickness(3.0) .eraser_size(12.0) .font_descriptor(FontDescriptor { diff --git a/src/input/state/tests/hit_testing.rs b/src/input/state/tests/hit_testing.rs index 7a6823f1e..649cb8736 100644 --- a/src/input/state/tests/hit_testing.rs +++ b/src/input/state/tests/hit_testing.rs @@ -1,6 +1,9 @@ +use crate::draw::{Frame, ShapeId, TextMeasurer}; +use crate::input::{BOARD_ID_BLACKBOARD, BOARD_ID_TRANSPARENT}; + use super::*; -fn add_test_line(state: &mut InputState) -> crate::draw::ShapeId { +fn add_test_line(state: &mut InputState) -> ShapeId { state.boards.active_frame_mut().add_shape(Shape::Line { x1: 0, y1: 0, @@ -42,7 +45,7 @@ fn indexed_state_with_two_overlapping_rects() -> InputState { #[test] fn explicit_hit_testing_rejects_invalid_tolerances() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); add_test_line(&mut state); @@ -71,7 +74,7 @@ fn stored_hit_test_tolerance_is_always_valid() { #[test] fn extreme_persisted_rectangle_is_selectable_through_the_spatial_index() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.set_hit_test_threshold(1); let color = Color { @@ -140,11 +143,11 @@ fn spatial_hit_testing_reuses_resolved_candidate_indices_without_id_rescans() { thick: 1.0, }); - crate::draw::Frame::reset_linear_id_lookup_count(); + Frame::reset_linear_id_lookup_count(); assert_eq!(state.hit_test_at(10, 10), Some(topmost)); assert!(state.has_spatial_index()); assert_eq!( - crate::draw::Frame::linear_id_lookup_count(), + Frame::linear_id_lookup_count(), 0, "spatial candidates already carry resolved frame indices" ); @@ -164,7 +167,7 @@ fn unchanged_spatial_hit_tests_build_shape_indices_once() { #[test] fn unchanged_spatial_multi_point_hit_tests_build_shape_indices_once() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = indexed_state_with_two_overlapping_rects(); InputState::reset_spatial_shape_index_build_count(); @@ -336,13 +339,13 @@ fn spatial_shape_indices_are_scoped_to_the_active_board_and_page() { assert!(state.page_prev()); assert_ne!(state.hit_test_at(10, 0), Some(second_page_top)); - state.switch_board_force(crate::input::BOARD_ID_BLACKBOARD); + state.switch_board_force(BOARD_ID_BLACKBOARD); add_test_line(&mut state); add_test_line(&mut state); let blackboard_top = add_test_line(&mut state); assert_eq!(state.hit_test_at(10, 0), Some(blackboard_top)); - state.switch_board_force(crate::input::BOARD_ID_TRANSPARENT); + state.switch_board_force(BOARD_ID_TRANSPARENT); assert_eq!(state.hit_test_at(10, 0), Some(first_page_top)); assert_eq!(InputState::spatial_shape_index_build_count(), 5); } diff --git a/src/input/state/tests/input_hud.rs b/src/input/state/tests/input_hud.rs index 2388d96a4..7066a1c6c 100644 --- a/src/input/state/tests/input_hud.rs +++ b/src/input/state/tests/input_hud.rs @@ -1,3 +1,6 @@ +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::create_test_input_state; use crate::config::{Action, InputHudConfig}; use crate::input::state::{InputHudActiveSource, InputHudEntryKind, InputHudSettings}; @@ -15,8 +18,8 @@ fn enabled_hud_state() -> InputState { #[test] fn toggle_input_hud_action_changes_state_and_redraws() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -41,8 +44,8 @@ fn toggle_input_hud_action_changes_state_and_redraws() { /// source-independent and toasts immediately. #[test] fn toggle_input_hud_defers_the_source_announcement_to_the_backend() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -76,8 +79,8 @@ fn toggle_input_hud_defers_the_source_announcement_to_the_backend() { /// swallowed exactly like `ToggleClickHighlight` is. #[test] fn presenter_mode_forces_input_hud_and_gates_the_manual_toggle() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -112,8 +115,8 @@ fn presenter_mode_forces_input_hud_and_gates_the_manual_toggle() { /// Presenter mode leaves an already-enabled HUD on after exit. #[test] fn presenter_mode_restores_a_manually_enabled_input_hud() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -176,8 +179,8 @@ fn system_source_suppresses_overlay_notes() { /// an earlier session of the feature. #[test] fn disabling_the_hud_drops_its_chips() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -200,8 +203,8 @@ fn disabling_the_hud_drops_its_chips() { /// presenter gate applies to it too. #[test] fn toolbar_checkbox_toggles_the_input_hud() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, diff --git a/src/input/state/tests/laser.rs b/src/input/state/tests/laser.rs index c1ee41308..04d9f1e0b 100644 --- a/src/input/state/tests/laser.rs +++ b/src/input/state/tests/laser.rs @@ -1,5 +1,10 @@ +use std::path::PathBuf; use std::time::{Duration, Instant}; +use crate::config::{Config, toolbar_item_ids}; +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::*; use crate::config::{LaserConfig, PresenterToolBehavior}; use crate::input::tool::ProvisionalToolStroke; @@ -27,8 +32,8 @@ fn draw(state: &mut InputState, path: &[(i32, i32)]) { } fn run_action(state: &mut InputState, action: Action) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); state.handle_action_with_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -75,7 +80,7 @@ fn a_laser_stroke_never_reaches_the_frame_history_or_session() { fn session_snapshots_and_exports_see_no_laser_ink() { let mut state = laser_state(); draw(&mut state, &STROKE); - let mut options = SessionOptions::new(std::path::PathBuf::from("/tmp"), "laser"); + let mut options = SessionOptions::new(PathBuf::from("/tmp"), "laser"); options.persist_transparent = true; let snapshot = snapshot_from_input(&state, &options).expect("tool state is persisted"); @@ -189,7 +194,7 @@ fn clearing_the_canvas_also_clears_laser_ink() { #[test] fn from_config_applies_the_laser_section() { - let mut config = crate::config::Config::default(); + let mut config = Config::default(); config.laser.width = 12.0; config.laser.color = [0.0, 0.5, 1.0, 1.0]; @@ -204,8 +209,8 @@ fn locked_presenter_mode_still_allows_the_laser() { let mut state = create_test_input_state(); state.presenter_mode_config_mut_for_test().tool_behavior = PresenterToolBehavior::ForceHighlightLocked; - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); state.toggle_presenter_mode_with_resources(crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, @@ -247,7 +252,7 @@ fn regular_and_advanced_strips_show_the_laser_with_the_pens_and_simple_does_not( ); assert_eq!( model::toolbar_item_id_for_tool(Tool::Laser), - crate::config::toolbar_item_ids::TOP_TOOL_LASER + toolbar_item_ids::TOP_TOOL_LASER ); } diff --git a/src/input/state/tests/light_mode.rs b/src/input/state/tests/light_mode.rs index 12d845dd4..7e446abc4 100644 --- a/src/input/state/tests/light_mode.rs +++ b/src/input/state/tests/light_mode.rs @@ -1,8 +1,12 @@ +use crate::draw::TextMeasurer; +use crate::input::InputState; +use crate::ui_text::UiTextEngine; + use super::{create_test_input_state, create_test_input_state_with_click_highlight}; use crate::config::Action; use crate::input::{ClickHighlightSettings, Tool}; -fn create_light_mode_test_state() -> crate::input::InputState { +fn create_light_mode_test_state() -> InputState { let mut state = create_test_input_state(); state.compositor_capabilities.layer_shell = true; state @@ -10,7 +14,7 @@ fn create_light_mode_test_state() -> crate::input::InputState { fn create_light_mode_test_state_with_click_highlight( click_highlight_settings: ClickHighlightSettings, -) -> crate::input::InputState { +) -> InputState { let mut state = create_test_input_state_with_click_highlight(click_highlight_settings); state.compositor_capabilities.layer_shell = true; state @@ -18,8 +22,8 @@ fn create_light_mode_test_state_with_click_highlight( #[test] fn light_mode_enters_passthrough_and_hides_heavy_ui() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -45,8 +49,8 @@ fn light_mode_enters_passthrough_and_hides_heavy_ui() { #[test] fn light_mode_keeps_an_active_laser_instead_of_switching_to_the_pen() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -65,8 +69,8 @@ fn light_mode_keeps_an_active_laser_instead_of_switching_to_the_pen() { #[test] fn light_mode_drawing_toggle_disables_passthrough_without_exiting() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -100,8 +104,8 @@ fn light_draw_off_does_not_enter_light_mode() { #[test] fn light_mode_restores_previous_ui_and_tool_on_exit() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -128,8 +132,8 @@ fn light_mode_restores_previous_ui_and_tool_on_exit() { #[test] fn light_mode_force_enables_click_highlight_by_default_and_restores_on_exit() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -151,8 +155,8 @@ fn light_mode_force_enables_click_highlight_by_default_and_restores_on_exit() { #[test] fn light_mode_can_leave_click_highlight_disabled() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -175,8 +179,8 @@ fn light_mode_can_leave_click_highlight_disabled() { #[test] fn light_mode_and_presenter_mode_are_mutually_exclusive() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -195,8 +199,8 @@ fn light_mode_and_presenter_mode_are_mutually_exclusive() { #[test] fn light_mode_does_not_enter_without_layer_shell() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, diff --git a/src/input/state/tests/menus/clear.rs b/src/input/state/tests/menus/clear.rs index 7e317f4ea..c787ff5e8 100644 --- a/src/input/state/tests/menus/clear.rs +++ b/src/input/state/tests/menus/clear.rs @@ -1,6 +1,8 @@ +use crate::draw::ShapeId; + use super::*; -fn add_rect(state: &mut InputState, x: i32, locked: bool) -> crate::draw::ShapeId { +fn add_rect(state: &mut InputState, x: i32, locked: bool) -> ShapeId { let id = state.boards.active_frame_mut().add_shape(Shape::Rect { x, y: 10, diff --git a/src/input/state/tests/menus/clipboard.rs b/src/input/state/tests/menus/clipboard.rs index 0483a068b..8101b7d37 100644 --- a/src/input/state/tests/menus/clipboard.rs +++ b/src/input/state/tests/menus/clipboard.rs @@ -1,6 +1,8 @@ +use crate::draw::ShapeId; + use super::*; -fn add_rect(state: &mut InputState) -> crate::draw::ShapeId { +fn add_rect(state: &mut InputState) -> ShapeId { state.boards.active_frame_mut().add_shape(Shape::Rect { x: 10, y: 20, diff --git a/src/input/state/tests/menus/context_menu.rs b/src/input/state/tests/menus/context_menu.rs index fc29e8f34..bfc844b64 100644 --- a/src/input/state/tests/menus/context_menu.rs +++ b/src/input/state/tests/menus/context_menu.rs @@ -1,3 +1,9 @@ +use crate::config::{KeybindingsConfig, RadialMenuMouseBinding}; +use crate::draw::TextMeasurer; +use crate::input::boards::PendingBoardRuntimeUiAction; +use crate::input::state; +use crate::ui_text::UiTextEngine; + use super::*; use crate::draw::{BoardPages, Frame}; use crate::input::state::core::board_picker::{BoardPickerFocus, BoardPickerPageNavMode}; @@ -326,9 +332,9 @@ fn context_menu_radial_entry_shows_default_mouse_shortcut() { #[test] fn context_menu_radial_entry_shows_mouse_and_keyboard_shortcut() { - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); keybindings.ui.toggle_radial_menu = vec!["Ctrl+R".to_string()]; - let mut state = create_test_input_state_with_keybindings(keybindings); + let mut state = create_test_input_state_with_keybindings(&keybindings); state.toggle_context_menu_via_keyboard(); let entries = state.context_menu_entries(); @@ -345,7 +351,7 @@ fn context_menu_radial_entry_shows_mouse_and_keyboard_shortcut() { #[test] fn context_menu_radial_entry_shows_right_click_shortcut_when_configured() { let mut state = create_test_input_state(); - state.radial_menu.mouse_binding = crate::config::RadialMenuMouseBinding::Right; + state.radial_menu.mouse_binding = RadialMenuMouseBinding::Right; state.toggle_context_menu_via_keyboard(); let entries = state.context_menu_entries(); @@ -358,10 +364,10 @@ fn context_menu_radial_entry_shows_right_click_shortcut_when_configured() { #[test] fn context_menu_radial_entry_shows_keyboard_shortcut_when_mouse_binding_disabled() { - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); keybindings.ui.toggle_radial_menu = vec!["Ctrl+R".to_string()]; - let mut state = create_test_input_state_with_keybindings(keybindings); - state.radial_menu.mouse_binding = crate::config::RadialMenuMouseBinding::Disabled; + let mut state = create_test_input_state_with_keybindings(&keybindings); + state.radial_menu.mouse_binding = RadialMenuMouseBinding::Disabled; state.toggle_context_menu_via_keyboard(); let entries = state.context_menu_entries(); @@ -443,9 +449,9 @@ fn context_menu_zoom_centres_on_the_right_click_origin() { assert_eq!( state.take_pending_zoom_request(), - Some(crate::input::ZoomRequest { + Some(ZoomRequest { action: ZoomAction::In, - anchor: crate::input::ZoomAnchor::At(12, 34), + anchor: ZoomAnchor::At(12, 34), }) ); } @@ -464,7 +470,7 @@ fn keyboard_opened_canvas_menu_zooms_the_screen_centre() { state .take_pending_zoom_request() .map(|request| request.anchor), - Some(crate::input::ZoomAnchor::ScreenCenter) + Some(ZoomAnchor::ScreenCenter) ); } @@ -660,11 +666,11 @@ fn boards_menu_disables_delete_for_transparent_board_and_shows_overflow_entry() assert!(delete_entry.disabled); } -fn menu_entry(state: &InputState, command: MenuCommand) -> ContextMenuEntry { +fn menu_entry(state: &InputState, command: &MenuCommand) -> ContextMenuEntry { state .context_menu_entries() .into_iter() - .find(|entry| entry.command == Some(command.clone())) + .find(|entry| entry.command.as_ref() == Some(command)) .unwrap_or_else(|| panic!("missing {command:?} entry")) } @@ -674,7 +680,7 @@ fn boards_menu_edits_active_board_paper_and_skips_the_overlay() { state.switch_board(BOARD_ID_WHITEBOARD); state.open_context_menu((0, 0), Vec::new(), ContextMenuKind::Boards, None); - assert!(!menu_entry(&state, MenuCommand::BoardEditPaper).disabled); + assert!(!menu_entry(&state, &MenuCommand::BoardEditPaper).disabled); state.execute_menu_command(MenuCommand::BoardEditPaper); assert!(!state.is_context_menu_open()); @@ -687,7 +693,7 @@ fn boards_menu_edits_active_board_paper_and_skips_the_overlay() { state.close_board_picker(); state.switch_board(BOARD_ID_TRANSPARENT); state.open_context_menu((0, 0), Vec::new(), ContextMenuKind::Boards, None); - assert!(menu_entry(&state, MenuCommand::BoardEditPaper).disabled); + assert!(menu_entry(&state, &MenuCommand::BoardEditPaper).disabled); } #[test] @@ -695,11 +701,11 @@ fn board_row_menu_edits_renames_and_pins_its_own_board() { let mut state = create_test_input_state(); let blackboard = board_index(&state, BOARD_ID_BLACKBOARD); let overlay = board_index(&state, BOARD_ID_TRANSPARENT); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); state.open_board_context_menu((5, 5), overlay); assert_eq!(state.context_menu_entries()[0].label, "Overlay"); - assert!(menu_entry(&state, MenuCommand::BoardEditPaperFromContext).disabled); + assert!(menu_entry(&state, &MenuCommand::BoardEditPaperFromContext).disabled); state.open_board_context_menu((5, 5), blackboard); assert!( @@ -722,7 +728,7 @@ fn board_row_menu_edits_renames_and_pins_its_own_board() { state .board_picker_edit_state() .map(|(mode, index, _)| (mode, index)), - Some((crate::input::state::BoardPickerEditMode::Name, row)) + Some((state::BoardPickerEditMode::Name, row)) ); state.board_picker_cancel_edit(); @@ -731,7 +737,7 @@ fn board_row_menu_edits_renames_and_pins_its_own_board() { state.execute_menu_command(MenuCommand::BoardTogglePinFromContext); assert!(matches!( state.take_pending_board_runtime_ui_actions().as_slice(), - [crate::input::boards::PendingBoardRuntimeUiAction::TogglePin { board_id, .. }] + [PendingBoardRuntimeUiAction::TogglePin { board_id, .. }] if board_id == BOARD_ID_BLACKBOARD )); } @@ -740,7 +746,7 @@ fn board_row_menu_edits_renames_and_pins_its_own_board() { fn right_clicking_a_board_row_selects_it_and_opens_its_menu() { let mut state = create_test_input_state(); let blackboard = board_index(&state, BOARD_ID_BLACKBOARD); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 1280, 720).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); state.update_board_picker_layout(&ctx, 1280, 720); @@ -752,7 +758,7 @@ fn right_clicking_a_board_row_selects_it_and_opens_its_menu() { + layout.header_height + layout.row_height * (row as f64 + 0.5)) as i32; - assert!(state.handle_board_picker_press(crate::input::MouseButton::Right, x, y)); + assert!(state.handle_board_picker_press(MouseButton::Right, x, y)); assert!(state.is_board_picker_open()); assert_eq!(state.board_picker_selected_index(), Some(row)); @@ -834,7 +840,7 @@ fn page_duplicate_from_context_duplicates_target_page_and_closes_menu() { #[test] fn page_delete_from_context_reconciles_board_picker_page_search_cursor() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let blackboard = board_index(&state, BOARD_ID_BLACKBOARD); state.switch_board(BOARD_ID_BLACKBOARD); @@ -844,7 +850,7 @@ fn page_delete_from_context_reconciles_board_picker_page_search_cursor() { &[Some("Match one"), Some("Match two"), Some("Other")], 0, ); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); state.board_picker_set_focus(BoardPickerFocus::PagePanel); state.handle_board_picker_key_with_measurer(&route_measurer, Key::Char('/')); @@ -888,7 +894,7 @@ fn page_delete_from_context_reconciles_board_picker_page_search_cursor() { #[test] fn page_search_active_match_clamps_stale_cursor_after_external_page_delete() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let blackboard = board_index(&state, BOARD_ID_BLACKBOARD); state.switch_board(BOARD_ID_BLACKBOARD); @@ -898,7 +904,7 @@ fn page_search_active_match_clamps_stale_cursor_after_external_page_delete() { &[Some("Match one"), Some("Match two"), Some("Other")], 0, ); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); state.board_picker_set_focus(BoardPickerFocus::PagePanel); state.handle_board_picker_key_with_measurer(&route_measurer, Key::Char('/')); @@ -999,7 +1005,7 @@ fn highlight_row_uses_the_action_short_label() { let mut state = create_test_input_state(); state.open_context_menu((40, 40), Vec::new(), ContextMenuKind::Canvas, None); - let highlight = menu_entry(&state, MenuCommand::ToggleHighlightTool); + let highlight = menu_entry(&state, &MenuCommand::ToggleHighlightTool); assert_eq!(highlight.label, "Highlight"); assert!(!highlight.label.contains("(tool + click)")); @@ -1026,16 +1032,16 @@ fn undo_and_redo_rows_follow_history_and_run_it() { 16, ); state.open_context_menu((40, 40), Vec::new(), ContextMenuKind::Canvas, None); - assert!(!menu_entry(&state, MenuCommand::Undo).disabled); - assert!(menu_entry(&state, MenuCommand::Redo).disabled); + assert!(!menu_entry(&state, &MenuCommand::Undo).disabled); + assert!(menu_entry(&state, &MenuCommand::Redo).disabled); state.execute_menu_command(MenuCommand::Undo); assert!(!state.is_context_menu_open()); assert!(state.boards.active_frame().shapes.is_empty()); state.open_context_menu((40, 40), Vec::new(), ContextMenuKind::Canvas, None); - assert!(menu_entry(&state, MenuCommand::Undo).disabled); - assert!(!menu_entry(&state, MenuCommand::Redo).disabled); + assert!(menu_entry(&state, &MenuCommand::Undo).disabled); + assert!(!menu_entry(&state, &MenuCommand::Redo).disabled); } #[test] @@ -1051,7 +1057,7 @@ fn exit_row_exits_or_hides_a_daemon_overlay() { let mut state = create_test_input_state(); state.set_context_menu_exit_hides_overlay(true); state.open_context_menu((40, 40), Vec::new(), ContextMenuKind::Canvas, None); - assert_eq!(menu_entry(&state, MenuCommand::Exit).label, "Hide Overlay"); + assert_eq!(menu_entry(&state, &MenuCommand::Exit).label, "Hide Overlay"); } #[test] @@ -1064,7 +1070,7 @@ fn capture_region_row_hands_the_capture_to_the_backend() { assert!(!state.is_context_menu_open()); assert!(matches!( state.take_pending_backend_action(), - Some(crate::input::state::PendingBackendAction::Screenshot( + Some(state::PendingBackendAction::Screenshot( Action::CaptureRegionInteractive )) )); @@ -1103,7 +1109,7 @@ fn shape_menu_also_ends_with_exit() { #[test] fn the_footer_lives_inside_the_menu_and_leaves_row_hits_alone() { - let engine = crate::ui_text::UiTextEngine::default(); + let engine = UiTextEngine::default(); let mut state = create_test_input_state(); state.open_context_menu((40, 40), Vec::new(), ContextMenuKind::Canvas, None); state.update_context_menu_layout_with_engine(&engine, 1280, 900); diff --git a/src/input/state/tests/menus/history.rs b/src/input/state/tests/menus/history.rs index 1620b0840..78841740c 100644 --- a/src/input/state/tests/menus/history.rs +++ b/src/input/state/tests/menus/history.rs @@ -1,3 +1,7 @@ +use std::time::{Duration, Instant}; + +use crate::draw::TextMeasurer; + use super::*; fn push_rect_create(state: &mut InputState, x: i32) { @@ -27,7 +31,7 @@ fn push_rect_create(state: &mut InputState, x: i32) { #[test] fn undo_all_and_redo_all_process_entire_stack() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let frame = state.boards.active_frame_mut(); @@ -104,14 +108,14 @@ fn undo_all_with_delay_respects_history() { ); state.start_undo_all_delayed(0); - state.tick_delayed_history(std::time::Instant::now()); + state.tick_delayed_history(Instant::now()); assert_eq!(state.boards.active_frame().shapes.len(), 0); assert_eq!(state.boards.active_frame().redo_stack_len(), 1); } #[test] fn redo_all_with_delay_replays_history() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let frame = state.boards.active_frame_mut(); @@ -136,7 +140,7 @@ fn redo_all_with_delay_replays_history() { assert_eq!(state.boards.active_frame().redo_stack_len(), 1); state.start_redo_all_delayed(0); - state.tick_delayed_history(std::time::Instant::now()); + state.tick_delayed_history(Instant::now()); assert_eq!(state.boards.active_frame().shapes.len(), 1); assert_eq!(state.boards.active_frame().undo_stack_len(), 1); } @@ -149,24 +153,24 @@ fn custom_undo_uses_step_budget_and_minimum_delay_between_steps() { assert_eq!(state.boards.active_frame().shapes.len(), 2); state.start_custom_undo(0, 5); - let now = std::time::Instant::now(); + let now = Instant::now(); assert!(state.has_pending_history()); assert!(state.tick_delayed_history(now)); assert_eq!(state.boards.active_frame().shapes.len(), 1); assert!(state.has_pending_history()); - assert!(!state.tick_delayed_history(now + std::time::Duration::from_millis(49))); + assert!(!state.tick_delayed_history(now + Duration::from_millis(49))); assert_eq!(state.boards.active_frame().shapes.len(), 1); - assert!(state.tick_delayed_history(now + std::time::Duration::from_millis(50))); + assert!(state.tick_delayed_history(now + Duration::from_millis(50))); assert_eq!(state.boards.active_frame().shapes.len(), 0); assert!(!state.has_pending_history()); } #[test] fn custom_redo_respects_step_budget() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); push_rect_create(&mut state, 0); push_rect_create(&mut state, 20); @@ -174,7 +178,7 @@ fn custom_redo_respects_step_budget() { assert_eq!(state.boards.active_frame().redo_stack_len(), 2); state.start_custom_redo(0, 1); - assert!(state.tick_delayed_history(std::time::Instant::now())); + assert!(state.tick_delayed_history(Instant::now())); assert_eq!(state.boards.active_frame().shapes.len(), 1); assert_eq!(state.boards.active_frame().undo_stack_len(), 1); diff --git a/src/input/state/tests/menus/submenu.rs b/src/input/state/tests/menus/submenu.rs index 63e97ec4a..559218cb8 100644 --- a/src/input/state/tests/menus/submenu.rs +++ b/src/input/state/tests/menus/submenu.rs @@ -1,5 +1,8 @@ use std::time::{Duration, Instant}; +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::*; use crate::input::BOARD_ID_WHITEBOARD; use crate::input::events::Key; @@ -12,11 +15,11 @@ fn open_canvas_menu(state: &mut InputState, anchor: (i32, i32)) { state.update_context_menu_layout(SCREEN.0, SCREEN.1); } -fn row_of(state: &InputState, command: MenuCommand) -> usize { +fn row_of(state: &InputState, command: &MenuCommand) -> usize { state .context_menu_entries() .iter() - .position(|entry| entry.command.as_ref() == Some(&command)) + .position(|entry| entry.command.as_ref() == Some(command)) .expect("menu row") } @@ -76,11 +79,8 @@ fn settle(state: &mut InputState) { state.update_context_menu_layout(SCREEN.0, SCREEN.1); } -fn text_resources() -> (crate::draw::TextMeasurer, crate::ui_text::UiTextEngine) { - ( - crate::draw::TextMeasurer::default(), - crate::ui_text::UiTextEngine::default(), - ) +fn text_resources() -> (TextMeasurer, UiTextEngine) { + (TextMeasurer::default(), UiTextEngine::default()) } #[test] @@ -144,7 +144,7 @@ fn heading_into_a_submenu_keeps_it_while_other_rows_switch_or_close_it() { open_canvas_menu(&mut state, (100, 100)); let boards = parent_row_of(&state, ContextMenuKind::Boards); let pages = parent_row_of(&state, ContextMenuKind::Pages); - let help = row_of(&state, MenuCommand::ToggleHelp); + let help = row_of(&state, &MenuCommand::ToggleHelp); assert_eq!(pages, boards + 1, "Pages sits right below Boards"); let (x, boards_y) = menu_row(&state, boards); let (_, pages_y) = menu_row(&state, pages); @@ -205,7 +205,7 @@ fn sweeping_across_a_parent_row_does_not_open_its_submenu() { let mut state = create_test_input_state(); open_canvas_menu(&mut state, (100, 100)); let boards = parent_row_of(&state, ContextMenuKind::Boards); - let help = row_of(&state, MenuCommand::ToggleHelp); + let help = row_of(&state, &MenuCommand::ToggleHelp); let (x, boards_y) = menu_row(&state, boards); let (_, help_y) = menu_row(&state, help); @@ -274,7 +274,7 @@ fn the_menu_swallows_arrow_keys_that_have_nothing_to_do() { }; let mut state = create_test_input_state(); state.toggle_context_menu_via_keyboard(); - let help = row_of(&state, MenuCommand::ToggleHelp); + let help = row_of(&state, &MenuCommand::ToggleHelp); state.set_context_menu_focus(Some(help)); assert!(state.handle_context_menu_key_with_resources(resources, Key::Right)); @@ -433,7 +433,7 @@ fn leaving_the_menu_lifts_a_collapsed_parent_rows_suppression() { fn page_overflow_from_the_picker_keeps_the_picker_open() { let mut state = create_test_input_state(); state.switch_board(BOARD_ID_WHITEBOARD); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); state.execute_menu_command(MenuCommand::OpenPagesMenu); assert!(state.is_board_picker_open() && state.is_context_menu_open()); @@ -450,7 +450,7 @@ fn page_overflow_from_the_picker_keeps_the_picker_open() { fn a_menu_command_with_no_parent_row_opens_that_menu_on_its_own() { let mut state = create_test_input_state(); state.switch_board(BOARD_ID_WHITEBOARD); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); state.update_pointer_position_synthetic(300, 200); // The board picker's page overflow link runs this with no menu open. diff --git a/src/input/state/tests/menus/zoomed.rs b/src/input/state/tests/menus/zoomed.rs index 644e1ffd7..46d1fd341 100644 --- a/src/input/state/tests/menus/zoomed.rs +++ b/src/input/state/tests/menus/zoomed.rs @@ -1,6 +1,9 @@ //! The context menu over a zoomed view: right-click and the keyboard //! shortcut open it, and the canvas menu leads with the zoom controls. +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::*; use crate::input::ZoomAction; @@ -22,8 +25,8 @@ fn labels(state: &InputState) -> Vec { /// fails here. #[test] fn the_keyboard_shortcut_opens_the_menu_while_zoomed() { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); let mut state = zoomed_state(false); state.handle_action_with_resources( diff --git a/src/input/state/tests/mod.rs b/src/input/state/tests/mod.rs index 13c753912..8f6ef345f 100644 --- a/src/input/state/tests/mod.rs +++ b/src/input/state/tests/mod.rs @@ -1,7 +1,8 @@ use super::core::{ContextMenuKind, ContextMenuState, MenuCommand}; use super::*; use crate::config::{Action, ColorSpec, ToolPresetConfig}; -use crate::draw::{Color, EraserKind, FontDescriptor, Shape, frame::UndoAction}; +use crate::draw::frame::UndoAction; +use crate::draw::{Color, EraserKind, FontDescriptor, Shape}; use crate::input::{ClickHighlightSettings, EraserMode, Key, MouseButton, Tool}; mod helpers; diff --git a/src/input/state/tests/modal.rs b/src/input/state/tests/modal.rs index 01757eae8..a4ec02812 100644 --- a/src/input/state/tests/modal.rs +++ b/src/input/state/tests/modal.rs @@ -5,6 +5,10 @@ //! of another and starve it in the key-routing chain. These tests pin the //! rule (open closes everything else) and its one deliberate exception. +use crate::draw::TextMeasurer; +use crate::input::InputState; +use crate::input::state::{DrawingState, EyedropperCaptureSource}; + use super::helpers::create_test_input_state; use crate::input::state::core::modal::ModalSurface; @@ -26,7 +30,7 @@ fn opening_help_closes_the_color_picker_popup() { #[test] fn opening_help_closes_the_board_picker() { let mut state = create_test_input_state(); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); assert!(state.is_board_picker_open()); state.toggle_help_overlay(); @@ -40,7 +44,7 @@ fn opening_help_closes_the_board_picker() { #[test] fn a_context_menu_keeps_the_board_picker_open() { let mut state = create_test_input_state(); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + state.open_board_picker_with_measurer(&TextMeasurer::default()); assert!(state.is_board_picker_open()); state.open_page_context_menu((10, 10), 0, 0); @@ -57,11 +61,7 @@ fn a_context_menu_keeps_the_board_picker_open() { /// fixture can open. #[test] fn openers_leave_no_excluded_surface_behind() { - type Opener = ( - &'static str, - ModalSurface, - fn(&mut crate::input::InputState), - ); + type Opener = (&'static str, ModalSurface, fn(&mut InputState)); let openers: &[Opener] = &[ ("palette", ModalSurface::CommandPalette, |state| { @@ -80,7 +80,7 @@ fn openers_leave_no_excluded_surface_behind() { state.open_page_context_menu((10, 10), 0, 0) }), ("board picker", ModalSurface::BoardPicker, |state| { - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()) + state.open_board_picker_with_measurer(&TextMeasurer::default()) }), ]; @@ -130,10 +130,10 @@ fn popups_text_editor_and_pending_screen_modals_own_pointer_shortcuts() { assert!(state.modal_owns_pointer_shortcuts()); state.close_color_picker_popup(true); - state.state = crate::input::state::DrawingState::text_input(0, 0, String::new()); + state.state = DrawingState::text_input(0, 0, String::new()); assert!(state.modal_owns_pointer_shortcuts()); - state.state = crate::input::state::DrawingState::Idle; + state.state = DrawingState::Idle; - state.set_eyedropper_pending_capture(crate::input::state::EyedropperCaptureSource::Frozen); + state.set_eyedropper_pending_capture(EyedropperCaptureSource::Frozen); assert!(state.modal_owns_pointer_shortcuts()); } diff --git a/src/input/state/tests/pages.rs b/src/input/state/tests/pages.rs index a24664750..a384552b4 100644 --- a/src/input/state/tests/pages.rs +++ b/src/input/state/tests/pages.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::*; use crate::draw::{BoardPages, EmbeddedImage, Frame, ShapeId}; use crate::input::{ @@ -62,7 +64,7 @@ fn set_board_name_trims_the_live_board_name() { let mut state = create_test_input_state(); let index = board_index(&state, BOARD_ID_BLACKBOARD); - assert!(state.set_board_name(index, " Focus Board ".to_string())); + assert!(state.set_board_name(index, " Focus Board ")); assert_eq!(state.boards.board_states()[index].spec.name, "Focus Board"); } @@ -73,7 +75,7 @@ fn set_board_name_rejects_empty_input_with_warning_toast() { let index = board_index(&state, BOARD_ID_BLACKBOARD); let original = state.boards.board_states()[index].spec.name.clone(); - assert!(!state.set_board_name(index, " \t ".to_string())); + assert!(!state.set_board_name(index, " \t ")); assert_eq!(state.boards.board_states()[index].spec.name, original); assert_eq!( @@ -116,7 +118,7 @@ fn set_board_background_color_updates_active_auto_adjust_pen_color() { #[test] fn reorder_page_in_board_moves_named_pages_and_active_index() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let index = board_index(&state, BOARD_ID_BLACKBOARD); @@ -133,7 +135,7 @@ fn reorder_page_in_board_moves_named_pages_and_active_index() { #[test] fn move_page_between_boards_copy_preserves_source_and_adds_page_to_target() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let source = board_index(&state, BOARD_ID_WHITEBOARD); @@ -181,7 +183,7 @@ fn reset_active_canvas_position_clears_view_offset_on_solid_board() { #[test] fn move_page_between_boards_move_removes_source_page_and_activates_target_copy() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let source = board_index(&state, BOARD_ID_WHITEBOARD); @@ -220,7 +222,7 @@ fn move_page_between_boards_move_removes_source_page_and_activates_target_copy() #[test] fn switch_to_page_cancels_text_edit_before_leaving_source_page() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let board = board_index(&state, BOARD_ID_BLACKBOARD); @@ -239,7 +241,7 @@ fn switch_to_page_cancels_text_edit_before_leaving_source_page() { #[test] fn page_duplicate_cancels_text_edit_before_cloning_source_page() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let board = board_index(&state, BOARD_ID_BLACKBOARD); @@ -423,7 +425,7 @@ fn page_duplicate_blocks_uncompressed_text_when_real_save_exceeds_limit() { #[test] fn cross_board_page_copy_blocks_when_clone_would_exceed_persisted_session_limit() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let source = board_index(&state, BOARD_ID_WHITEBOARD); @@ -457,7 +459,7 @@ fn cross_board_page_copy_blocks_when_clone_would_exceed_persisted_session_limit( #[test] fn cross_board_page_copy_preflights_when_source_was_not_previously_persisted() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let source = board_index(&state, BOARD_ID_TRANSPARENT); @@ -497,7 +499,7 @@ fn cross_board_page_copy_preflights_when_source_was_not_previously_persisted() { #[test] fn cross_board_page_copy_cancels_active_source_text_edit_before_cloning() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let source = board_index(&state, BOARD_ID_WHITEBOARD); diff --git a/src/input/state/tests/presenter_mode.rs b/src/input/state/tests/presenter_mode.rs index 043ffef7c..f505f04bb 100644 --- a/src/input/state/tests/presenter_mode.rs +++ b/src/input/state/tests/presenter_mode.rs @@ -1,3 +1,6 @@ +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::create_test_input_state; use crate::config::{ Action, ColorSpec, PresenterToolBehavior, PresenterToolbarMode, ToolPresetConfig, @@ -8,8 +11,8 @@ use crate::ui::toolbar::ToolbarEvent; #[test] fn presenter_mode_forces_click_highlight() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -30,8 +33,8 @@ fn presenter_mode_forces_click_highlight() { #[test] fn presenter_mode_exits_focus_mode_before_taking_chrome_ownership() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -66,8 +69,8 @@ fn presenter_mode_exits_focus_mode_before_taking_chrome_ownership() { #[test] fn presenter_mode_blocks_preset_status_bar_toggle() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -106,8 +109,8 @@ fn presenter_mode_blocks_preset_status_bar_toggle() { #[test] fn presenter_mode_blocks_tool_preview_toggle() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -124,8 +127,8 @@ fn presenter_mode_blocks_tool_preview_toggle() { #[test] fn presenter_mode_closes_help_overlay_and_switches_to_highlight_tool() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -143,8 +146,8 @@ fn presenter_mode_closes_help_overlay_and_switches_to_highlight_tool() { #[test] fn presenter_locked_mode_blocks_non_left_drag_bindings() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -167,8 +170,8 @@ fn presenter_locked_mode_blocks_non_left_drag_bindings() { #[test] fn presenter_mode_restores_status_bar_toolbars_and_tool_override_on_exit() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -193,8 +196,8 @@ fn presenter_mode_restores_status_bar_toolbars_and_tool_override_on_exit() { #[test] fn presenter_micro_mapping_shows_the_chip_and_restores_on_exit() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -229,8 +232,8 @@ fn presenter_micro_mapping_shows_the_chip_and_restores_on_exit() { #[test] fn presenter_hidden_mapping_keeps_todays_behavior() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -248,8 +251,8 @@ fn presenter_hidden_mapping_keeps_todays_behavior() { #[test] fn presenter_mode_emits_entry_and_exit_toasts() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -264,7 +267,7 @@ fn presenter_mode_emits_entry_and_exit_toasts() { .action .as_ref() .and_then(|action| action.dispatch_action()), - Some(crate::config::Action::TogglePresenterMode) + Some(Action::TogglePresenterMode) ); state.toggle_presenter_mode_with_resources(route_resources); diff --git a/src/input/state/tests/pressure_modes.rs b/src/input/state/tests/pressure_modes.rs index c8dee9fcb..f1ba924b9 100644 --- a/src/input/state/tests/pressure_modes.rs +++ b/src/input/state/tests/pressure_modes.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::*; use crate::draw::ShapeId; @@ -32,7 +34,7 @@ fn add_text_shape(state: &mut InputState) -> ShapeId { } fn open_panel(state: &mut InputState) { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); assert!(state.show_properties_panel_with(&route_measurer)); } @@ -120,7 +122,7 @@ fn pressure_entry_mode_pressure_only_requires_all_pressure() { #[test] fn pressure_entry_enabled_when_edit_mode_add_and_unlocked() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.style.pressure_thickness_entry_mode = PressureThicknessEntryMode::PressureOnly; state.style.pressure_thickness_edit_mode = PressureThicknessEditMode::Add; @@ -146,7 +148,7 @@ fn pressure_entry_enabled_when_edit_mode_add_and_unlocked() { #[test] fn pressure_entry_add_mode_decrements_thickness() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.style.pressure_thickness_entry_mode = PressureThicknessEntryMode::PressureOnly; state.style.pressure_thickness_edit_mode = PressureThicknessEditMode::Add; @@ -172,7 +174,7 @@ fn pressure_entry_add_mode_decrements_thickness() { #[test] fn pressure_entry_disabled_when_edit_mode_disabled() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.style.pressure_thickness_entry_mode = PressureThicknessEntryMode::PressureOnly; state.style.pressure_thickness_edit_mode = PressureThicknessEditMode::Disabled; @@ -258,7 +260,7 @@ fn pressure_entry_mode_any_pressure_mixed_lock_states_is_editable() { #[test] fn pressure_entry_mode_any_pressure_add_updates_pressure_only() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.style.pressure_thickness_entry_mode = PressureThicknessEntryMode::AnyPressure; state.style.pressure_thickness_edit_mode = PressureThicknessEditMode::Add; @@ -288,7 +290,7 @@ fn pressure_entry_mode_any_pressure_add_updates_pressure_only() { #[test] fn pressure_entry_mode_any_pressure_scale_updates_pressure_only() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.style.pressure_thickness_entry_mode = PressureThicknessEntryMode::AnyPressure; state.style.pressure_thickness_edit_mode = PressureThicknessEditMode::Scale; @@ -318,7 +320,7 @@ fn pressure_entry_mode_any_pressure_scale_updates_pressure_only() { #[test] fn pressure_entry_scale_mode_applies_step() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.style.pressure_thickness_entry_mode = PressureThicknessEntryMode::PressureOnly; state.style.pressure_thickness_edit_mode = PressureThicknessEditMode::Scale; @@ -345,7 +347,7 @@ fn pressure_entry_scale_mode_applies_step() { #[test] fn pressure_entry_scale_mode_decrements_thickness() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.style.pressure_thickness_entry_mode = PressureThicknessEntryMode::PressureOnly; state.style.pressure_thickness_edit_mode = PressureThicknessEditMode::Scale; diff --git a/src/input/state/tests/properties_panel.rs b/src/input/state/tests/properties_panel.rs index f2b08db63..010ea22b2 100644 --- a/src/input/state/tests/properties_panel.rs +++ b/src/input/state/tests/properties_panel.rs @@ -1,9 +1,14 @@ +use crate::draw; +use crate::draw::{ShapeId, TextMeasurer}; +use crate::input::state; +use crate::ui_text::UiTextEngine; + use super::*; use crate::draw::ArrowStyle; use crate::input::BOARD_ID_WHITEBOARD; use crate::util::Rect; -fn add_rect(state: &mut InputState, x: i32, y: i32, w: i32, h: i32) -> crate::draw::ShapeId { +fn add_rect(state: &mut InputState, x: i32, y: i32, w: i32, h: i32) -> ShapeId { state.boards.active_frame_mut().add_shape(Shape::Rect { x, y, @@ -28,7 +33,7 @@ fn entry_index(state: &InputState, label: &str) -> usize { #[test] fn show_properties_panel_for_single_shape_reports_type_layer_and_lock_state() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = add_rect(&mut state, 10, 20, 30, 40); state.set_selection(vec![shape_id]); @@ -46,15 +51,12 @@ fn show_properties_panel_for_single_shape_reports_type_layer_and_lock_state() { .as_deref() .is_some_and(|details| details.starts_with(&format!("Shape ID {shape_id}"))) ); - assert_eq!( - panel.lock, - crate::input::state::PropertiesPanelLock::Unlocked - ); + assert_eq!(panel.lock, state::PropertiesPanelLock::Unlocked); } #[test] fn show_properties_panel_for_multi_selection_includes_locked_count_and_summary() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let first = add_rect(&mut state, 10, 10, 20, 20); let second = add_rect(&mut state, 50, 15, 10, 15); @@ -73,10 +75,7 @@ fn show_properties_panel_for_multi_selection_includes_locked_count_and_summary() assert!(panel.multiple_selection); let subtitle = panel.subtitle.as_deref().expect("subtitle"); assert!(subtitle.ends_with(" px · 1 of 2 locked"), "{subtitle}"); - assert_eq!( - panel.lock, - crate::input::state::PropertiesPanelLock::Partial - ); + assert_eq!(panel.lock, state::PropertiesPanelLock::Partial); } #[test] @@ -155,8 +154,8 @@ fn style_pill_selection_docking_routes_through_the_properties_apply_machinery() #[test] fn spotlight_magnification_property_steps_the_selected_shape_and_is_undoable() { - let route_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &test_ui_engine, @@ -204,7 +203,7 @@ fn spotlight_magnification_property_steps_the_selected_shape_and_is_undoable() { #[test] fn close_properties_panel_clears_panel_and_requests_redraw() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = add_rect(&mut state, 5, 5, 10, 10); state.set_selection(vec![shape_id]); @@ -220,7 +219,7 @@ fn close_properties_panel_clears_panel_and_requests_redraw() { #[test] fn show_properties_panel_anchors_to_screen_space_on_panned_boards() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.switch_board(BOARD_ID_WHITEBOARD); assert!(state.boards.active_frame_mut().set_view_offset(100, 50)); @@ -236,7 +235,7 @@ fn show_properties_panel_anchors_to_screen_space_on_panned_boards() { #[test] fn activate_fill_entry_toggles_rectangle_fill_and_refreshes_panel_value() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = add_rect(&mut state, 5, 5, 20, 20); state.set_selection(vec![shape_id]); @@ -265,7 +264,7 @@ fn activate_fill_entry_toggles_rectangle_fill_and_refreshes_panel_value() { #[test] fn adjust_font_size_entry_increases_text_size_and_refreshes_panel_value() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Text { x: 10, @@ -302,7 +301,7 @@ fn adjust_font_size_entry_increases_text_size_and_refreshes_panel_value() { #[test] fn activate_text_background_entry_on_mixed_selection_turns_all_backgrounds_on() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let first = state.boards.active_frame_mut().add_shape(Shape::Text { x: 10, @@ -353,7 +352,7 @@ fn activate_text_background_entry_on_mixed_selection_turns_all_backgrounds_on() #[test] fn adjust_arrow_length_entry_clamps_to_max_and_refreshes_panel_value() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Arrow { x1: 0, @@ -393,7 +392,7 @@ fn adjust_arrow_length_entry_clamps_to_max_and_refreshes_panel_value() { ); } -fn add_spotlight(state: &mut InputState, magnification: f64) -> crate::draw::ShapeId { +fn add_spotlight(state: &mut InputState, magnification: f64) -> ShapeId { state.boards.active_frame_mut().add_shape(Shape::Spotlight { cx: 100, cy: 80, @@ -403,7 +402,7 @@ fn add_spotlight(state: &mut InputState, magnification: f64) -> crate::draw::Sha }) } -fn spotlight_magnification(state: &InputState, id: crate::draw::ShapeId) -> f64 { +fn spotlight_magnification(state: &InputState, id: ShapeId) -> f64 { match &state .boards .active_frame() @@ -416,7 +415,7 @@ fn spotlight_magnification(state: &InputState, id: crate::draw::ShapeId) -> f64 } } -fn magnification_entry(state: &InputState) -> Option { +fn magnification_entry(state: &InputState) -> Option { state .selection_pill_entries() .into_iter() @@ -425,8 +424,8 @@ fn magnification_entry(state: &InputState) -> Option crate::config::QuickColorPalette { - let entry = |label: &str, color: Color| crate::config::QuickColorPaletteEntry { +fn opacity_variant_palette() -> QuickColorPalette { + let entry = |label: &str, color: Color| QuickColorPaletteEntry { label: label.to_string(), color, }; - crate::config::QuickColorPalette::from_entries(vec![ + QuickColorPalette::from_entries(vec![ entry("Red", PALETTE_RED), entry( "Faint red", @@ -1006,7 +1005,7 @@ fn the_opacity_row_steps_by_five_percent_and_a_marker_stays_translucent() { #[test] fn dragging_the_thickness_slider_follows_the_pointer_and_undoes_in_one_step() { let measurer = TextMeasurer::default(); - let engine = crate::ui_text::UiTextEngine::default(); + let engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &engine, @@ -1162,9 +1161,9 @@ fn a_selection_saves_its_style_into_a_preset_slot_and_another_takes_it_on() { assert_eq!(saved.size, 8.0); assert_eq!(saved.fill_enabled, Some(true)); assert!(matches!( - state.drain_input_effects(crate::input::state::InputEffectDrain::Runtime) - .into_iter().filter(|effect| matches!(effect, crate::input::state::InputEffect::Preset(_))).collect::>().as_slice(), - [crate::input::state::InputEffect::Preset(crate::input::state::PresetAction::Save { slot, .. })] if *slot == empty_slot + state.drain_input_effects(InputEffectDrain::Runtime) + .into_iter().filter(|effect| matches!(effect, InputEffect::Preset(_))).collect::>().as_slice(), + [InputEffect::Preset(PresetAction::Save { slot, .. })] if *slot == empty_slot )); let tool_before = state.active_tool(); @@ -1264,7 +1263,7 @@ fn a_shape_no_tool_draws_cannot_be_saved_as_a_preset() { #[test] fn a_key_pressed_mid_drag_lands_after_the_drag_in_history() { let measurer = TextMeasurer::default(); - let engine = crate::ui_text::UiTextEngine::default(); + let engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &engine, @@ -1311,7 +1310,7 @@ fn a_key_pressed_mid_drag_lands_after_the_drag_in_history() { #[test] fn undo_mid_drag_first_lands_the_drag() { let measurer = TextMeasurer::default(); - let engine = crate::ui_text::UiTextEngine::default(); + let engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &engine, @@ -1373,7 +1372,7 @@ fn a_preset_saved_from_a_separately_filled_shape_brings_its_fill_along() { ); click(&mut state, slot); - let target = add_styled_rect(&mut state, crate::domain::color::PALETTE_BLUE, 5.0, true); + let target = add_styled_rect(&mut state, color::PALETTE_BLUE, 5.0, true); open(&mut state, vec![target]); click(&mut state, slot); diff --git a/src/input/state/tests/radial_menu.rs b/src/input/state/tests/radial_menu.rs index bed58b748..5f6ab8294 100644 --- a/src/input/state/tests/radial_menu.rs +++ b/src/input/state/tests/radial_menu.rs @@ -1,3 +1,8 @@ +use crate::config::{KeybindingsConfig, RadialMenuMouseBinding}; +use crate::draw::{TextMeasurer, color}; +use crate::ui::toolbar::model; +use crate::ui_text::UiTextEngine; + use super::helpers::create_test_input_state_with_keybindings; use super::*; use crate::config::{QuickColorPalette, QuickColorPaletteEntry}; @@ -244,7 +249,7 @@ fn shapes_sub_ring_matches_shape_tools_catalog() { // Membership and order both come from the toolbar's shape_tools() // catalog, the shapes source of truth. - let catalog = crate::ui::toolbar::model::shape_tools(); + let catalog = model::shape_tools(); let mut previous_position = None; for action in children { let tool = Tool::from_select_action(*action).expect("shape child must be a tool action"); @@ -405,7 +410,7 @@ fn opening_radial_menu_closes_help_overlay() { #[test] fn right_click_toggles_radial_when_configured() { let mut state = create_test_input_state(); - state.radial_menu.mouse_binding = crate::config::RadialMenuMouseBinding::Right; + state.radial_menu.mouse_binding = RadialMenuMouseBinding::Right; state.on_mouse_press(MouseButton::Right, 200, 150); assert!(state.is_radial_menu_open()); @@ -418,8 +423,8 @@ fn right_click_toggles_radial_when_configured() { #[test] fn toggle_radial_menu_action_opens_and_closes_menu() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -445,9 +450,9 @@ fn toggle_radial_menu_action_opens_and_closes_menu() { #[test] fn toggle_radial_menu_with_modifier_keybinding_closes_when_open() { - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); keybindings.ui.toggle_radial_menu = vec!["Ctrl+R".to_string()]; - let mut state = create_test_input_state_with_keybindings(keybindings); + let mut state = create_test_input_state_with_keybindings(&keybindings); state.update_pointer_position(320, 240); state.on_key_press(Key::Ctrl); @@ -542,7 +547,7 @@ fn color_ring_selection_uses_configured_quick_palette() { state.set_quick_colors(QuickColorPalette::from_entries(vec![ QuickColorPaletteEntry { label: "First".to_string(), - color: crate::draw::color::RED, + color: color::RED, }, QuickColorPaletteEntry { label: "Configured".to_string(), @@ -597,7 +602,7 @@ fn hover_of(state: &InputState) -> Option { } fn open_via_right_press(state: &mut InputState) -> RadialMenuLayout { - state.radial_menu.mouse_binding = crate::config::RadialMenuMouseBinding::Right; + state.radial_menu.mouse_binding = RadialMenuMouseBinding::Right; state.on_mouse_press(MouseButton::Right, 400, 300); assert!(state.is_radial_menu_open()); state.update_radial_menu_layout(800, 600); @@ -668,13 +673,13 @@ fn tick_radial_menu_paint_requests_redraw_only_at_deadline() { #[test] fn blind_flick_release_commits_wedge_by_direction() { let mut state = create_test_input_state(); - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, }; - state.radial_menu.mouse_binding = crate::config::RadialMenuMouseBinding::Right; + state.radial_menu.mouse_binding = RadialMenuMouseBinding::Right; state.on_mouse_press_with_canvas_and_resources( resources, MouseButton::Right, @@ -822,7 +827,7 @@ fn toggle_release_without_leaving_deadzone_keeps_menu_open() { /// Open the menu with a right press near the left screen edge, where the /// layout clamps the rendered center far away from the press point. fn open_via_right_press_at_edge(state: &mut InputState, x: i32, y: i32) -> RadialMenuLayout { - state.radial_menu.mouse_binding = crate::config::RadialMenuMouseBinding::Right; + state.radial_menu.mouse_binding = RadialMenuMouseBinding::Right; state.on_mouse_press(MouseButton::Right, x, y); assert!(state.is_radial_menu_open()); state.update_radial_menu_layout(800, 600); @@ -965,7 +970,7 @@ fn size_ring_hit_test_covers_arc_but_not_gap() { #[test] fn size_ring_drag_adjusts_thickness_live_and_keeps_menu_open() { let mut state = create_test_input_state(); - state.radial_menu.mouse_binding = crate::config::RadialMenuMouseBinding::Right; + state.radial_menu.mouse_binding = RadialMenuMouseBinding::Right; state.on_mouse_press(MouseButton::Right, 400, 300); state.update_radial_menu_layout(800, 600); let layout = state.radial_menu.layout.expect("layout"); @@ -1014,10 +1019,7 @@ fn recent_colors_are_deduped_most_recent_first_and_capped() { }; // Steps of 0.125 are exact binary fractions, so equality is exact. for i in 0..8 { - state.apply_color_from_ui_with_measurer( - &crate::draw::TextMeasurer::default(), - color(i as f64 * 0.125), - ); + state.apply_color_from_ui_with_measurer(&TextMeasurer::default(), color(i as f64 * 0.125)); } assert_eq!(state.style.recent_colors.len(), 6, "recents are capped"); assert_eq!( @@ -1027,7 +1029,7 @@ fn recent_colors_are_deduped_most_recent_first_and_capped() { ); // Re-applying an existing color moves it to the front without growing. - state.apply_color_from_ui_with_measurer(&crate::draw::TextMeasurer::default(), color(0.5)); + state.apply_color_from_ui_with_measurer(&TextMeasurer::default(), color(0.5)); assert_eq!(state.style.recent_colors.len(), 6); assert_eq!(state.style.recent_colors[0], color(0.5)); assert_eq!( @@ -1051,14 +1053,14 @@ fn radial_ring_appends_recents_after_quick_palette_without_duplicates() { b: 0.33, a: 1.0, }; - state.apply_color_from_ui_with_measurer(&crate::draw::TextMeasurer::default(), unique); + state.apply_color_from_ui_with_measurer(&TextMeasurer::default(), unique); // A recent identical to a quick swatch is filtered from the ring. let quick0 = state .style .quick_colors .radial_color_for_index(0) .expect("quick color 0"); - state.apply_color_from_ui_with_measurer(&crate::draw::TextMeasurer::default(), quick0); + state.apply_color_from_ui_with_measurer(&TextMeasurer::default(), quick0); let swatches = state.radial_ring_swatches(); assert_eq!(swatches.len(), quick_len + 1); @@ -1076,14 +1078,14 @@ fn recent_color_segment_applies_through_the_color_path() { b: 0.33, a: 1.0, }; - state.apply_color_from_ui_with_measurer(&crate::draw::TextMeasurer::default(), unique); + state.apply_color_from_ui_with_measurer(&TextMeasurer::default(), unique); // Move the current color away so applying the recent is observable. let quick0 = state .style .quick_colors .radial_color_for_index(0) .expect("quick color 0"); - state.apply_color_from_ui_with_measurer(&crate::draw::TextMeasurer::default(), quick0); + state.apply_color_from_ui_with_measurer(&TextMeasurer::default(), quick0); let quick_len = state.style.quick_colors.radial_rendered_len(); let layout = open_with_layout(&mut state); @@ -1129,9 +1131,7 @@ fn escape_exit_resumes_after_dismissal_guard_and_ctrl_q_is_immediate() { state.modifiers.ctrl = true; state.on_key_press(Key::Char('q')); } else { - state.note_escape_dismissal( - std::time::Instant::now() - std::time::Duration::from_millis(401), - ); + state.note_escape_dismissal(Instant::now() - Duration::from_millis(401)); state.on_key_press(Key::Escape); } diff --git a/src/input/state/tests/selection/actions.rs b/src/input/state/tests/selection/actions.rs index 7e91193c0..e24e78fb0 100644 --- a/src/input/state/tests/selection/actions.rs +++ b/src/input/state/tests/selection/actions.rs @@ -1,9 +1,12 @@ +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::*; #[test] fn select_all_action_selects_shapes() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, diff --git a/src/input/state/tests/selection/damage.rs b/src/input/state/tests/selection/damage.rs index 1a186c7ec..8db8a4ecc 100644 --- a/src/input/state/tests/selection/damage.rs +++ b/src/input/state/tests/selection/damage.rs @@ -1,3 +1,6 @@ +use crate::draw::ShapeId; +use crate::ui_text::UiTextEngine; + use super::*; use crate::draw::TextMeasurer; use crate::util::Rect; @@ -13,7 +16,7 @@ fn damage_state() -> InputState { state } -fn add_filled_rect(state: &mut InputState, x: i32, y: i32) -> crate::draw::ShapeId { +fn add_filled_rect(state: &mut InputState, x: i32, y: i32) -> ShapeId { state.boards.active_frame_mut().add_shape(Shape::Rect { x, y, @@ -26,7 +29,7 @@ fn add_filled_rect(state: &mut InputState, x: i32, y: i32) -> crate::draw::Shape }) } -fn chrome_of(state: &InputState, id: crate::draw::ShapeId) -> Rect { +fn chrome_of(state: &InputState, id: ShapeId) -> Rect { let measurer = TextMeasurer::default(); state .boards @@ -139,7 +142,7 @@ fn reselecting_the_same_shapes_adds_no_damage() { #[test] fn undo_repaints_the_chrome_where_it_was_drawn_before_the_step() { let test_text_measurer = TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_ui_engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, diff --git a/src/input/state/tests/selection/deletion.rs b/src/input/state/tests/selection/deletion.rs index f9c0d736b..d1e95d817 100644 --- a/src/input/state/tests/selection/deletion.rs +++ b/src/input/state/tests/selection/deletion.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::*; #[test] @@ -12,14 +14,14 @@ fn delete_shapes_by_ids_ignores_missing_ids() { thick: state.style.current_thickness, }); - let removed = state.delete_shapes_by_ids_with(&crate::draw::TextMeasurer::default(), &[9999]); + let removed = state.delete_shapes_by_ids_with(&TextMeasurer::default(), &[9999]); assert!(!removed); assert_eq!(state.boards.active_frame().shapes.len(), 1); } #[test] fn locked_shape_blocks_edit_and_delete() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Text { x: 40, diff --git a/src/input/state/tests/selection/double_click.rs b/src/input/state/tests/selection/double_click.rs index f8cf612b0..eed822b0b 100644 --- a/src/input/state/tests/selection/double_click.rs +++ b/src/input/state/tests/selection/double_click.rs @@ -1,3 +1,7 @@ +use crate::draw::ShapeId; +use crate::input::tool::ToolPressBehavior; +use crate::util::Rect; + use super::*; fn select_state() -> InputState { @@ -7,7 +11,7 @@ fn select_state() -> InputState { state } -fn add_filled_rect(state: &mut InputState, x: i32, y: i32) -> crate::draw::ShapeId { +fn add_filled_rect(state: &mut InputState, x: i32, y: i32) -> ShapeId { state.boards.active_frame_mut().add_shape(Shape::Rect { x, y, @@ -31,7 +35,7 @@ fn drag(state: &mut InputState, from: (i32, i32), to: (i32, i32)) { state.on_mouse_release(MouseButton::Left, to.0, to.1); } -fn bounds_of(state: &InputState, id: crate::draw::ShapeId) -> crate::util::Rect { +fn bounds_of(state: &InputState, id: ShapeId) -> Rect { state .boards .active_frame() @@ -120,7 +124,7 @@ fn alt_double_click_opens_properties_without_drawing() { let rect = add_filled_rect(&mut state, 100, 100); assert!(!matches!( state.active_tool().press_behavior(), - crate::input::tool::ToolPressBehavior::Selection + ToolPressBehavior::Selection )); state.modifiers.alt = true; diff --git a/src/input/state/tests/selection/duplicate.rs b/src/input/state/tests/selection/duplicate.rs index eab564663..ddcd9f10a 100644 --- a/src/input/state/tests/selection/duplicate.rs +++ b/src/input/state/tests/selection/duplicate.rs @@ -1,3 +1,6 @@ +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::*; fn shape_bounds_center(shape: &Shape) -> (i32, i32) { @@ -7,8 +10,8 @@ fn shape_bounds_center(shape: &Shape) -> (i32, i32) { #[test] fn duplicate_selection_via_action_creates_offset_shape() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -52,8 +55,8 @@ fn duplicate_selection_via_action_creates_offset_shape() { #[test] fn copy_paste_selection_centers_shape_at_pointer() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -112,8 +115,8 @@ fn copy_paste_selection_centers_shape_at_pointer() { #[test] fn immediate_paste_after_copy_uses_pending_local_publish_shapes() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -156,8 +159,8 @@ fn immediate_paste_after_copy_uses_pending_local_publish_shapes() { #[test] fn stale_publish_completion_is_ignored_for_newer_copy() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -211,8 +214,8 @@ fn stale_publish_completion_is_ignored_for_newer_copy() { #[test] fn published_selection_allows_local_fallback_until_superseded() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -260,8 +263,8 @@ fn published_selection_allows_local_fallback_until_superseded() { #[test] fn fallback_generation_rejects_newer_local_copy() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -304,8 +307,8 @@ fn fallback_generation_rejects_newer_local_copy() { #[test] fn private_payload_for_request_rejects_newer_same_instance_generation() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -360,8 +363,8 @@ fn private_payload_for_request_rejects_newer_same_instance_generation() { #[test] fn private_payload_for_request_uses_payload_when_current_generation_changed() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -418,8 +421,8 @@ fn private_payload_for_request_uses_payload_when_current_generation_changed() { #[test] fn same_instance_private_payload_with_no_fallback_generation_uses_payload() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -465,8 +468,8 @@ fn same_instance_private_payload_with_no_fallback_generation_uses_payload() { #[test] fn request_generation_supersede_ignores_newer_local_copy() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -522,8 +525,8 @@ fn request_generation_supersede_ignores_newer_local_copy() { #[test] fn duplicate_selection_skips_locked_shapes() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -596,7 +599,7 @@ fn copy_selection_of_only_locked_shapes_leaves_clipboard_empty() { #[test] fn repeated_paste_selection_uses_current_pointer_anchor() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let original_id = state.boards.active_frame_mut().add_shape(Shape::Rect { x: 10, @@ -628,7 +631,7 @@ fn repeated_paste_selection_uses_current_pointer_anchor() { #[test] fn paste_selection_warns_when_shape_limit_prevents_any_paste() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let original_id = state.boards.active_frame_mut().add_shape(Shape::Rect { x: 10, @@ -653,7 +656,7 @@ fn paste_selection_warns_when_shape_limit_prevents_any_paste() { #[test] fn paste_selection_warns_when_shape_limit_allows_only_partial_paste() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let first = state.boards.active_frame_mut().add_shape(Shape::Rect { x: 0, @@ -688,10 +691,7 @@ fn paste_selection_warns_when_shape_limit_allows_only_partial_paste() { ); } -fn paste_local_clipboard( - state: &mut crate::input::InputState, - measurer: &crate::draw::TextMeasurer, -) -> usize { +fn paste_local_clipboard(state: &mut InputState, measurer: &TextMeasurer) -> usize { let request = state.request_clipboard_paste(); let shapes = state .selection_clipboard_snapshot() diff --git a/src/input/state/tests/selection/mid_drag_actions.rs b/src/input/state/tests/selection/mid_drag_actions.rs index 0be2421a1..238073eaf 100644 --- a/src/input/state/tests/selection/mid_drag_actions.rs +++ b/src/input/state/tests/selection/mid_drag_actions.rs @@ -1,3 +1,6 @@ +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::*; use crate::draw::ShapeId; use crate::input::state::SelectionHandle; @@ -31,8 +34,8 @@ fn rect_geometry(state: &InputState, id: ShapeId) -> (i32, i32, i32, i32) { } fn undo(state: &mut InputState) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); state.handle_action_with_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -80,7 +83,7 @@ fn a_nudge_key_mid_move_lands_the_move_before_recording_the_nudge() { #[test] fn a_nudge_key_mid_resize_lands_the_resize_before_recording_the_nudge() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = select_state(); let rect = add_filled_rect(&mut state, 100, 100); let original = rect_geometry(&state, rect); diff --git a/src/input/state/tests/selection/reorder.rs b/src/input/state/tests/selection/reorder.rs index 2a718cb5d..61e498556 100644 --- a/src/input/state/tests/selection/reorder.rs +++ b/src/input/state/tests/selection/reorder.rs @@ -1,3 +1,5 @@ +use crate::ui_text::UiTextEngine; + use super::*; use crate::draw::{ShapeId, TextMeasurer}; @@ -91,7 +93,7 @@ fn a_selected_block_steps_as_one_and_keeps_its_own_order() { #[test] fn a_step_is_one_undo_entry_that_undoes_and_redoes() { let measurer = TextMeasurer::default(); - let resources_engine = crate::ui_text::UiTextEngine::default(); + let resources_engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &resources_engine, diff --git a/src/input/state/tests/shape_pen.rs b/src/input/state/tests/shape_pen.rs index dcc72536b..10ef783c2 100644 --- a/src/input/state/tests/shape_pen.rs +++ b/src/input/state/tests/shape_pen.rs @@ -1,3 +1,9 @@ +use std::f64::consts; + +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; +use crate::util::Rect; + use super::*; use crate::config::ToolbarLayoutMode; use crate::input::tool::ProvisionalToolStroke; @@ -43,13 +49,13 @@ fn release_at_end(state: &mut InputState, path: &[(i32, i32)]) { state.on_mouse_release(MouseButton::Left, last.0, last.1); } -fn overlaps(a: crate::util::Rect, b: crate::util::Rect) -> bool { +fn overlaps(a: Rect, b: Rect) -> bool { a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height } fn run_action(state: &mut InputState, action: Action) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); state.handle_action_with_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -95,8 +101,8 @@ fn overlapping_ovals_keep_their_preview_and_commit_as_ellipses() { let end = samples * 3 / 2; let path: Vec<_> = (0..=end) .map(|step| { - let angle = start - + direction * std::f64::consts::TAU * step as f64 / samples as f64; + let angle = + start + direction * consts::TAU * step as f64 / samples as f64; let sway = wobble * (7.0 * angle).sin(); ( (200.0 + (rx + sway) * angle.cos()).round() as i32, @@ -322,7 +328,7 @@ fn recognized_preview_keeps_a_faint_copy_of_the_ink_underneath() { // The preview paints the ink as well as the shape, so the damage it // reports must cover every ink point. let bounds = preview - .bounds_with(&crate::draw::TextMeasurer::default()) + .bounds_with(&TextMeasurer::default()) .expect("preview bounds"); for &(x, y) in &RECTANGLE { assert!(bounds.contains(x, y), "({x}, {y}) outside {bounds:?}"); @@ -373,9 +379,8 @@ fn preview_damage_and_readout_share_one_recognition_per_point() { #[test] fn ink_previews_damage_only_their_new_tail() { - let near_start = crate::util::Rect::new(0, 0, 12, 12).expect("probe"); - let touches_start = - |dirty: &[crate::util::Rect]| dirty.iter().any(|rect| overlaps(*rect, near_start)); + let near_start = Rect::new(0, 0, 12, 12).expect("probe"); + let touches_start = |dirty: &[Rect]| dirty.iter().any(|rect| overlaps(*rect, near_start)); // A sawtooth is neither a line nor a closed shape, so it stays ink. let mut state = shape_pen_state(); @@ -391,7 +396,7 @@ fn ink_previews_damage_only_their_new_tail() { // A recognized rectangle can move anywhere, so its whole preview repaints, // including the corner farthest from the new segment. let mut state = shape_pen_state(); - let far_corner = crate::util::Rect::new(106, 86, 8, 8).expect("probe"); + let far_corner = Rect::new(106, 86, 8, 8).expect("probe"); draw_path(&mut state, &RECTANGLE[..RECTANGLE.len() - 1]); let _ = state.take_dirty_regions(); state.on_mouse_motion(10, 10); @@ -433,7 +438,7 @@ fn releasing_as_ink_repaints_where_the_shape_preview_was() { ); // The preview's left edge sat on x = 40, outside the ink. - let left_edge = crate::util::Rect::new(37, 75, 3, 10).expect("probe"); + let left_edge = Rect::new(37, 75, 3, 10).expect("probe"); let dirty = state.take_dirty_regions(); assert!( dirty.iter().any(|rect| overlaps(*rect, left_edge)), diff --git a/src/input/state/tests/shape_pen_feedback.rs b/src/input/state/tests/shape_pen_feedback.rs index 07bd0018b..623b5cc86 100644 --- a/src/input/state/tests/shape_pen_feedback.rs +++ b/src/input/state/tests/shape_pen_feedback.rs @@ -1,3 +1,9 @@ +use crate::config::KeybindingsConfig; +use crate::draw::TextMeasurer; +use crate::input::BOARD_ID_WHITEBOARD; +use crate::ui::anim; +use crate::ui_text::UiTextEngine; + use super::*; use std::time::{Duration, Instant}; @@ -56,8 +62,8 @@ fn chip_label(state: &InputState) -> Option { } fn run_action(state: &mut InputState, action: Action) { - let measurer = crate::draw::TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let ui_engine = UiTextEngine::default(); state.handle_action_with_resources( crate::input::state::InputTextResources { measurer: &measurer, @@ -69,7 +75,7 @@ fn run_action(state: &mut InputState, action: Action) { #[test] fn a_recognized_stroke_names_the_shape_and_the_undo_that_keeps_its_ink() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = shape_pen_state(create_test_input_state()); state.needs_redraw = false; @@ -156,9 +162,9 @@ fn any_edit_that_changes_the_next_undo_retires_the_chip() { #[test] fn the_chip_names_the_configured_undo_shortcut() { - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); keybindings.core.undo = vec!["Ctrl+Alt+U".to_string()]; - let mut state = shape_pen_state(create_test_input_state_with_keybindings(keybindings)); + let mut state = shape_pen_state(create_test_input_state_with_keybindings(&keybindings)); draw(&mut state, &RECTANGLE); @@ -170,9 +176,9 @@ fn the_chip_names_the_configured_undo_shortcut() { #[test] fn an_unbound_undo_is_named_in_words() { - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); keybindings.core.undo = Vec::new(); - let mut state = shape_pen_state(create_test_input_state_with_keybindings(keybindings)); + let mut state = shape_pen_state(create_test_input_state_with_keybindings(&keybindings)); draw(&mut state, &RECTANGLE); @@ -198,7 +204,7 @@ fn turning_recognition_feedback_off_shows_no_chip() { #[test] fn the_chip_fades_and_then_expires() { - let _motion = crate::ui::anim::override_motion_for_test(true); + let _motion = anim::override_motion_for_test(true); let mut state = shape_pen_state(create_test_input_state()); draw(&mut state, &RECTANGLE); let shown = Instant::now(); @@ -215,7 +221,7 @@ fn the_chip_fades_and_then_expires() { #[test] fn reduced_motion_keeps_the_chip_opaque_for_its_lifetime() { - let _motion = crate::ui::anim::override_motion_for_test(false); + let _motion = anim::override_motion_for_test(false); let mut state = shape_pen_state(create_test_input_state()); draw(&mut state, &RECTANGLE); let shown = Instant::now(); @@ -231,7 +237,7 @@ fn switching_boards_takes_the_chip_away() { draw(&mut state, &RECTANGLE); assert!(state.recognition_chip().is_some()); - state.switch_board(crate::input::BOARD_ID_WHITEBOARD); + state.switch_board(BOARD_ID_WHITEBOARD); assert_eq!(chip_label(&state), None); } diff --git a/src/input/state/tests/spotlight.rs b/src/input/state/tests/spotlight.rs index 437a7a891..f6092e93b 100644 --- a/src/input/state/tests/spotlight.rs +++ b/src/input/state/tests/spotlight.rs @@ -1,5 +1,12 @@ //! The spotlight tool: drag geometry, region collection, and damage behavior. +use crate::draw; +use crate::draw::{ShapeId, TextMeasurer}; +use crate::input::BOARD_ID_WHITEBOARD; +use crate::input::state::spotlight::FrameIdentity; +use crate::ui::toolbar::ToolbarEvent; +use crate::ui_text::UiTextEngine; + use super::*; use crate::input::state::{SpotlightWheelClaim, SpotlightWheelOutcome}; @@ -205,7 +212,7 @@ fn spotlight_exposes_a_readable_kind_name() { cy: 0, rx: 10, ry: 10, - magnification: crate::draw::DEFAULT_SPOTLIGHT_MAGNIFICATION, + magnification: draw::DEFAULT_SPOTLIGHT_MAGNIFICATION, }; assert_eq!(shape.kind_name(), "Spotlight"); } @@ -217,7 +224,7 @@ fn spotlight_bounds_cover_its_opening() { cy: 100, rx: 40, ry: 20, - magnification: crate::draw::DEFAULT_SPOTLIGHT_MAGNIFICATION, + magnification: draw::DEFAULT_SPOTLIGHT_MAGNIFICATION, }; let bounds = shape.bounding_box().expect("spotlight has area"); assert!(bounds.x <= 60 && bounds.y <= 80); @@ -225,7 +232,7 @@ fn spotlight_bounds_cover_its_opening() { assert!(bounds.y + bounds.height >= 120); } -fn spotlight_state_with_one_loupe(magnification: f64) -> (InputState, crate::draw::ShapeId) { +fn spotlight_state_with_one_loupe(magnification: f64) -> (InputState, ShapeId) { let mut state = create_test_input_state(); // A real viewport, so the on-canvas control is placed under the same // clamping rules it gets on screen. @@ -240,7 +247,7 @@ fn spotlight_state_with_one_loupe(magnification: f64) -> (InputState, crate::dra (state, id) } -fn magnification_of(state: &InputState, id: crate::draw::ShapeId) -> f64 { +fn magnification_of(state: &InputState, id: ShapeId) -> f64 { match state.boards.active_frame().shape(id).expect("loupe").shape { Shape::Spotlight { magnification, .. } => magnification, ref other => panic!("expected a spotlight, got {other:?}"), @@ -250,8 +257,7 @@ fn magnification_of(state: &InputState, id: crate::draw::ShapeId) -> f64 { /// Every control that edits this property must leave the shape on the 0.25 /// grid, or the toolbar would show a value the shape does not hold. fn assert_on_the_step_grid(value: f64) { - let steps = (value - crate::draw::MIN_SPOTLIGHT_MAGNIFICATION) - / crate::draw::SPOTLIGHT_MAGNIFICATION_STEP; + let steps = (value - draw::MIN_SPOTLIGHT_MAGNIFICATION) / draw::SPOTLIGHT_MAGNIFICATION_STEP; assert!( (steps - steps.round()).abs() < 1e-9, "{value} is not a whole number of 0.25 steps above 1x" @@ -260,7 +266,7 @@ fn assert_on_the_step_grid(value: f64) { #[test] fn every_pixel_of_the_track_snaps_to_the_same_grid_the_toolbar_uses() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let (mut state, id) = spotlight_state_with_one_loupe(1.0); state.set_selection(vec![id]); let track = state @@ -283,7 +289,7 @@ fn every_pixel_of_the_track_snaps_to_the_same_grid_the_toolbar_uses() { #[test] fn a_wheel_tick_pulls_an_off_grid_loupe_back_onto_the_grid() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); // A factor from an older session or a hand-edited file. One tick should // land on a real step rather than carrying the offset forever. @@ -300,7 +306,7 @@ fn a_wheel_tick_pulls_an_off_grid_loupe_back_onto_the_grid() { #[test] fn the_wheel_over_a_loupe_steps_its_own_magnification() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let (mut state, id) = spotlight_state_with_one_loupe(2.0); @@ -321,8 +327,8 @@ fn the_wheel_over_a_loupe_steps_its_own_magnification() { #[test] fn a_wheel_burst_over_one_loupe_undoes_as_a_single_step() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -346,14 +352,14 @@ fn a_wheel_burst_over_one_loupe_undoes_as_a_single_step() { #[test] fn the_wheel_stops_at_the_end_of_the_range_without_opening_a_gesture() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, }; - let (mut state, id) = spotlight_state_with_one_loupe(crate::draw::MAX_SPOTLIGHT_MAGNIFICATION); + let (mut state, id) = spotlight_state_with_one_loupe(draw::MAX_SPOTLIGHT_MAGNIFICATION); // Still the loupe's event, so the wheel must not fall through and resize // a brush behind the user's back. @@ -364,19 +370,19 @@ fn the_wheel_stops_at_the_end_of_the_range_without_opening_a_gesture() { ); assert_eq!( magnification_of(&state, id), - crate::draw::MAX_SPOTLIGHT_MAGNIFICATION + draw::MAX_SPOTLIGHT_MAGNIFICATION ); state.handle_action_with_resources(test_text_resources, Action::Undo); assert_eq!( magnification_of(&state, id), - crate::draw::MAX_SPOTLIGHT_MAGNIFICATION, + draw::MAX_SPOTLIGHT_MAGNIFICATION, "a refused step must not have left an undo entry behind" ); } #[test] fn a_locked_loupe_claims_the_wheel_without_changing() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let (mut state, id) = spotlight_state_with_one_loupe(2.0); let index = state @@ -395,7 +401,7 @@ fn a_locked_loupe_claims_the_wheel_without_changing() { #[test] fn a_locked_topmost_loupe_hides_an_unlocked_loupe_from_the_wheel() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let (mut state, lower_id) = spotlight_state_with_one_loupe(2.0); let upper_id = state.boards.active_frame_mut().add_shape(Shape::Spotlight { @@ -422,7 +428,7 @@ fn a_locked_topmost_loupe_hides_an_unlocked_loupe_from_the_wheel() { #[test] fn the_on_canvas_knob_only_appears_for_one_unlocked_selected_loupe() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let (mut state, id) = spotlight_state_with_one_loupe(2.0); assert!( state @@ -456,7 +462,7 @@ fn the_on_canvas_knob_only_appears_for_one_unlocked_selected_loupe() { #[test] fn the_control_only_shows_on_a_page_that_already_forces_full_damage() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); // The control is drawn well outside the loupe's bounds, above it. It is // never clipped away because a page holding any Spotlight repaints in full // (`render_force_full_damage_reason`), and the control cannot appear @@ -485,8 +491,8 @@ fn the_control_only_shows_on_a_page_that_already_forces_full_damage() { #[test] fn a_wheel_gesture_never_commits_against_a_different_page() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -535,11 +541,11 @@ fn a_changed_page_set_never_looks_like_the_frame_a_gesture_started_on() { // are what separate them; the index alone would alias, and the old // snapshot would attach to whatever now holds that id. let (mut state, _id) = spotlight_state_with_one_loupe(2.0); - let started_on = crate::input::state::spotlight::FrameIdentity::of(&state.boards); + let started_on = FrameIdentity::of(&state.boards); state.boards.new_page(); assert_ne!( - crate::input::state::spotlight::FrameIdentity::of(&state.boards), + FrameIdentity::of(&state.boards), started_on, "a different page must not compare equal to the gesture's own" ); @@ -551,7 +557,7 @@ fn a_changed_page_set_never_looks_like_the_frame_a_gesture_started_on() { state.boards.prev_page(); assert_eq!(state.boards.active_page_index(), 0); assert_ne!( - crate::input::state::spotlight::FrameIdentity::of(&state.boards), + FrameIdentity::of(&state.boards), started_on, "a changed page set invalidates the gesture rather than risking an alias" ); @@ -559,8 +565,8 @@ fn a_changed_page_set_never_looks_like_the_frame_a_gesture_started_on() { #[test] fn moving_off_a_loupe_ends_its_wheel_gesture() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -611,8 +617,8 @@ fn moving_off_a_loupe_discards_its_partial_wheel_step() { #[test] fn a_toolbar_page_switch_closes_the_wheel_gesture_too() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -627,10 +633,10 @@ fn a_toolbar_page_switch_closes_the_wheel_gesture_too() { SpotlightWheelOutcome::Adjusted ); - state.apply_toolbar_event(crate::ui::toolbar::ToolbarEvent::PageNew); + state.apply_toolbar_event(ToolbarEvent::PageNew); assert_ne!(state.boards.active_page_index(), 0); - state.apply_toolbar_event(crate::ui::toolbar::ToolbarEvent::PagePrev); + state.apply_toolbar_event(ToolbarEvent::PagePrev); assert_eq!(magnification_of(&state, id), 2.25); state.handle_action_with_resources(test_text_resources, Action::Undo); assert_eq!( @@ -642,7 +648,7 @@ fn a_toolbar_page_switch_closes_the_wheel_gesture_too() { #[test] fn a_panned_board_still_places_the_control_where_the_user_can_reach_it() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); // The clamp is in canvas coordinates, so it has to follow the pan. A loupe // at the top of the *visible* area needs the control flipped below it even // though its canvas y is far from zero. @@ -678,9 +684,9 @@ fn a_panned_board_still_places_the_control_where_the_user_can_reach_it() { #[test] fn maximum_persisted_view_offsets_do_not_overflow_the_selected_control_path() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); - state.switch_board(crate::input::BOARD_ID_WHITEBOARD); + state.switch_board(BOARD_ID_WHITEBOARD); state.update_screen_dimensions(1920, 1080); let id = state.boards.active_frame_mut().add_shape(Shape::Spotlight { cx: 200, @@ -705,7 +711,7 @@ fn maximum_persisted_view_offsets_do_not_overflow_the_selected_control_path() { #[test] fn an_edge_loupe_keeps_its_control_on_screen() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let (mut state, _) = spotlight_state_with_one_loupe(2.0); state.boards.active_frame_mut().shapes.clear(); @@ -758,7 +764,7 @@ fn an_edge_loupe_keeps_its_control_on_screen() { #[test] fn extreme_loupe_coordinates_do_not_overflow_the_hit_test_or_the_track() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let id = state.boards.active_frame_mut().add_shape(Shape::Spotlight { @@ -779,8 +785,8 @@ fn extreme_loupe_coordinates_do_not_overflow_the_hit_test_or_the_track() { #[test] fn dragging_the_knob_magnifies_live_and_commits_one_undo_entry() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -811,7 +817,7 @@ fn dragging_the_knob_magnifies_live_and_commits_one_undo_entry() { state.on_mouse_motion(track.x + track.width, track.y + 6); assert_eq!( magnification_of(&state, id), - crate::draw::MAX_SPOTLIGHT_MAGNIFICATION + draw::MAX_SPOTLIGHT_MAGNIFICATION ); state.on_mouse_release(MouseButton::Left, track.x + track.width, track.y + 6); @@ -827,7 +833,7 @@ fn dragging_the_knob_magnifies_live_and_commits_one_undo_entry() { #[test] fn cancelling_a_knob_drag_restores_the_factor_it_started_from() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let (mut state, id) = spotlight_state_with_one_loupe(2.0); state.set_selection(vec![id]); let track = state @@ -839,10 +845,10 @@ fn cancelling_a_knob_drag_restores_the_factor_it_started_from() { state.on_mouse_press(MouseButton::Left, track.x + track.width, track.y + 6); assert_eq!( magnification_of(&state, id), - crate::draw::MAX_SPOTLIGHT_MAGNIFICATION + draw::MAX_SPOTLIGHT_MAGNIFICATION ); - state.cancel_active_interaction_with(&crate::draw::TextMeasurer::default()); + state.cancel_active_interaction_with(&TextMeasurer::default()); assert!(matches!(state.state, DrawingState::Idle)); assert_eq!(magnification_of(&state, id), 2.0); } diff --git a/src/input/state/tests/status_hud.rs b/src/input/state/tests/status_hud.rs index 837a2a350..8024c1212 100644 --- a/src/input/state/tests/status_hud.rs +++ b/src/input/state/tests/status_hud.rs @@ -4,6 +4,11 @@ //! contract: the press side reports a hit without side effects, activation //! happens on release-inside, and releases outside are ignored. +use crate::draw::TextMeasurer; +use crate::ui::ZoomChipButtonKind; +use crate::ui::toolbar::ToolbarEvent; +use crate::ui_text::UiTextEngine; + use super::*; use crate::config::{StatusBarItem, StatusBarStyle, StatusPosition}; use crate::input::state::core::board_picker::BoardPickerFocus; @@ -55,7 +60,7 @@ fn status_hud_hover_tracks_segments_and_requests_redraw() { assert!(input.needs_redraw, "hover transition requests a redraw"); input.needs_redraw = false; - input.zoom_chip.hover = Some(crate::ui::ZoomChipButtonKind::In); + input.zoom_chip.hover = Some(ZoomChipButtonKind::In); input.clear_chrome_hover(); assert_eq!(input.status_hud.hover, None); assert_eq!(input.zoom_chip.hover, None); @@ -118,8 +123,8 @@ fn status_hud_layout_rebuild_preserves_cleared_hover_after_pointer_leave() { input.set_toolbar_visible(false); input.update_status_hud_layout_for_pointer_with_resources( crate::input::state::InputTextResources { - measurer: &crate::draw::TextMeasurer::default(), - ui_engine: &crate::ui_text::UiTextEngine::default(), + measurer: &TextMeasurer::default(), + ui_engine: &UiTextEngine::default(), }, StatusPosition::BottomLeft, &StatusBarStyle::default(), @@ -151,7 +156,7 @@ fn status_hud_press_reports_hit_without_side_effect() { #[test] fn status_hud_click_board_segment_toggles_board_picker() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); update_hud_layout(&mut input, 1280, 720); @@ -168,7 +173,7 @@ fn status_hud_click_board_segment_toggles_board_picker() { #[test] fn status_hud_click_page_segment_opens_board_picker_page_panel() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); update_hud_layout(&mut input, 1280, 720); @@ -185,7 +190,7 @@ fn status_hud_click_page_segment_opens_board_picker_page_panel() { #[test] fn status_hud_click_color_dot_opens_color_picker_popup() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); update_hud_layout(&mut input, 1280, 720); @@ -199,7 +204,7 @@ fn status_hud_click_color_dot_opens_color_picker_popup() { #[test] fn status_hud_click_tool_segment_opens_radial_menu() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); update_hud_layout(&mut input, 1280, 720); @@ -213,7 +218,7 @@ fn status_hud_click_tool_segment_opens_radial_menu() { #[test] fn status_hud_click_size_segment_opens_radial_menu() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); update_hud_layout(&mut input, 1280, 720); @@ -227,7 +232,7 @@ fn status_hud_click_size_segment_opens_radial_menu() { #[test] fn status_hud_click_help_segment_returns_toggle_help_action() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); update_hud_layout(&mut input, 1280, 720); @@ -247,7 +252,7 @@ fn status_hud_click_help_segment_returns_toggle_help_action() { /// dispatch the action rather than open a surface in place. #[test] fn status_hud_click_version_chip_returns_open_about_action() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); // The version chip is opt-in. @@ -287,7 +292,7 @@ fn a_narrow_status_hud_sheds_the_version_chip_first() { #[test] fn status_hud_click_toolbar_hint_returns_toggle_toolbar_action() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); @@ -323,8 +328,8 @@ fn status_hud_click_toolbar_hint_returns_toggle_toolbar_action() { /// no-op. #[test] fn status_hud_toolbar_hint_recovers_cycle_hidden_top_strip() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -365,7 +370,7 @@ fn status_hud_toolbar_hint_recovers_cycle_hidden_top_strip() { #[test] fn status_hud_click_between_segments_consumes_without_action() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); update_hud_layout(&mut input, 1280, 720); @@ -393,7 +398,7 @@ fn status_hud_click_between_segments_consumes_without_action() { #[test] fn status_hud_click_outside_is_ignored() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); update_hud_layout(&mut input, 1280, 720); @@ -408,7 +413,7 @@ fn status_hud_click_outside_is_ignored() { #[test] fn status_hud_ignores_clicks_when_not_interactive() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); input.ui_visibility.status_bar_interactive = false; @@ -431,8 +436,8 @@ fn disabling_every_content_item_removes_the_hud_and_restores_badge_fallback() { input.boards.new_page(); for item in StatusBarItem::ALL { input.set_status_bar_item_visible_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), item, false, ); @@ -464,8 +469,8 @@ fn changing_status_hud_content_leaves_damage_to_the_effect_pass() { let _ = input.take_dirty_region_report(); assert!(input.set_status_bar_item_visible_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), StatusBarItem::Help, false )); @@ -484,17 +489,13 @@ fn changing_status_hud_master_or_interactivity_leaves_damage_to_the_effect_pass( update_hud_layout(&mut input, 1280, 720); let _ = input.take_dirty_region_report(); - assert!( - input.apply_toolbar_event(crate::ui::toolbar::ToolbarEvent::SetStatusBarInteractive( - false, - )) - ); + assert!(input.apply_toolbar_event(ToolbarEvent::SetStatusBarInteractive(false,))); assert!( input.take_dirty_region_report().regions.is_empty(), "interactivity only changes pixels inside the HUD footprint" ); - assert!(input.apply_toolbar_event(crate::ui::toolbar::ToolbarEvent::ToggleStatusBar(false))); + assert!(input.apply_toolbar_event(ToolbarEvent::ToggleStatusBar(false))); assert!( input.take_dirty_region_report().regions.is_empty(), "the effect pass owns HUD and fallback transitions" @@ -520,7 +521,7 @@ fn status_hud_press_routing_consumes_left_press_over_interactive_hud() { #[test] fn status_hud_ignored_while_radial_menu_overlays_it() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); update_hud_layout(&mut input, 1280, 720); @@ -552,7 +553,7 @@ fn status_hud_ignored_while_radial_menu_overlays_it() { #[test] fn status_hud_ignored_while_other_eclipsing_overlays_are_open() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut input = create_test_input_state(); update_hud_layout(&mut input, 1280, 720); @@ -561,7 +562,7 @@ fn status_hud_ignored_while_other_eclipsing_overlays_are_open() { // Board picker (also covers a picker opened between press and release: // check_status_hud_click shares the same guard). - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); assert!(!input.status_hud_contains(x, y)); assert!( !input diff --git a/src/input/state/tests/status_hud/engine_mutations.rs b/src/input/state/tests/status_hud/engine_mutations.rs index b3645af60..8899aca7b 100644 --- a/src/input/state/tests/status_hud/engine_mutations.rs +++ b/src/input/state/tests/status_hud/engine_mutations.rs @@ -1,3 +1,6 @@ +use crate::config::{PresenterToolbarMode, TopDisplayMode}; +use crate::domain; + use super::*; use crate::ui_text::UiTextEngine; @@ -5,11 +8,10 @@ fn seeded(engine: &UiTextEngine) -> InputState { let mut input = create_test_input_state(); input.compositor_capabilities.layer_shell = true; input.presenter_mode_config_mut_for_test().hide_toolbars = true; - input.presenter_mode_config_mut_for_test().toolbar_mode = - crate::config::PresenterToolbarMode::Micro; + input.presenter_mode_config_mut_for_test().toolbar_mode = PresenterToolbarMode::Micro; input.update_status_hud_layout_for_pointer_with_resources( crate::input::state::InputTextResources { - measurer: &crate::draw::TextMeasurer::default(), + measurer: &TextMeasurer::default(), ui_engine: engine, }, StatusPosition::BottomLeft, @@ -32,7 +34,7 @@ fn explicit_mutations_rebuild_immediately_and_preserve_no_op_results() { input.needs_redraw = false; assert!(input.set_status_bar_item_visible_with_resources( &engine, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), StatusBarItem::Help, false )); @@ -50,17 +52,13 @@ fn explicit_mutations_rebuild_immediately_and_preserve_no_op_results() { input.needs_redraw = false; assert!(!input.set_status_bar_item_visible_with_resources( &engine, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), StatusBarItem::Help, false )); assert!(!input.needs_redraw); - assert!(input.set_toolbar_visible_with_resources( - &engine, - &crate::draw::TextMeasurer::default(), - false - )); + assert!(input.set_toolbar_visible_with_resources(&engine, &TextMeasurer::default(), false)); assert!( input .status_hud_layout() @@ -70,17 +68,13 @@ fn explicit_mutations_rebuild_immediately_and_preserve_no_op_results() { .any(|s| s.kind == StatusHudSegmentKind::Toolbar) ); input.needs_redraw = false; - assert!(!input.set_toolbar_visible_with_resources( - &engine, - &crate::draw::TextMeasurer::default(), - false - )); + assert!(!input.set_toolbar_visible_with_resources(&engine, &TextMeasurer::default(), false)); assert!(!input.needs_redraw); let mut unpainted = create_test_input_state(); assert!(unpainted.set_status_bar_item_visible_with_resources( &engine, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), StatusBarItem::Help, false )); @@ -117,7 +111,7 @@ fn assert_same_chrome(actual: &InputState, expected: &InputState) { #[test] fn explicit_mode_cycles_match_legacy_without_an_intervening_frame() { let engine = UiTextEngine::default(); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &engine, @@ -166,17 +160,17 @@ fn explicit_mode_cycles_match_legacy_without_an_intervening_frame() { #[test] fn explicit_focus_rescue_and_display_cycle_refresh_saved_geometry() { let engine = UiTextEngine::default(); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &engine, }; let mut input = seeded(&engine); - input.set_toolbar_visible_with_resources(&engine, &crate::draw::TextMeasurer::default(), false); + input.set_toolbar_visible_with_resources(&engine, &TextMeasurer::default(), false); input.ui_visibility.show_status_bar = false; input.ui_visibility.show_floating_badge = false; input.ui_visibility.show_zoom_chip = false; - input.refresh_status_hud_layout_with_resources(&engine, &crate::draw::TextMeasurer::default()); + input.refresh_status_hud_layout_with_resources(&engine, &TextMeasurer::default()); assert!(!input.focus_mode_active()); input.toggle_focus_mode_with_resources(resources); assert!(input.toolbar_visible(), "Focus rescues fully hidden chrome"); @@ -186,35 +180,29 @@ fn explicit_focus_rescue_and_display_cycle_refresh_saved_geometry() { assert!(input.status_hud_layout().is_none()); input.set_top_display_mode_with_resources( &engine, - &crate::draw::TextMeasurer::default(), - crate::config::TopDisplayMode::Full, + &TextMeasurer::default(), + TopDisplayMode::Full, ); assert!(input.status_hud_layout().is_some()); assert_eq!( - input.cycle_top_toolbar_display_with_resources( - &engine, - &crate::draw::TextMeasurer::default() - ), - crate::config::TopDisplayMode::Micro - ); - input.derive_toolbar_visibility_from_pins_with_resources( - &engine, - &crate::draw::TextMeasurer::default(), + input.cycle_top_toolbar_display_with_resources(&engine, &TextMeasurer::default()), + TopDisplayMode::Micro ); + input.derive_toolbar_visibility_from_pins_with_resources(&engine, &TextMeasurer::default()); assert!(input.status_hud_layout().is_some()); } #[test] fn explicit_action_and_toolbar_routes_refresh_status_geometry_before_another_frame() { let engine = UiTextEngine::default(); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &engine, }; let mut input = seeded(&engine); - input.set_toolbar_visible_with_resources(&engine, &crate::draw::TextMeasurer::default(), true); - input.handle_action_with_resources(resources, crate::domain::Action::ToggleToolbar); + input.set_toolbar_visible_with_resources(&engine, &TextMeasurer::default(), true); + input.handle_action_with_resources(resources, domain::Action::ToggleToolbar); assert!(!input.toolbar_visible()); assert!( input @@ -230,7 +218,7 @@ fn explicit_action_and_toolbar_routes_refresh_status_geometry_before_another_fra assert_eq!(input.status_hud.hover, Some(StatusHudSegmentKind::Help)); assert!(input.apply_toolbar_event_with_resources( resources, - crate::ui::toolbar::ToolbarEvent::SetStatusBarItemVisible(StatusBarItem::Help, false) + ToolbarEvent::SetStatusBarItemVisible(StatusBarItem::Help, false) )); assert_eq!(input.status_hud.hover, None); assert!( diff --git a/src/input/state/tests/text_edit/commit_cancel.rs b/src/input/state/tests/text_edit/commit_cancel.rs index 93b753881..8a4e5966b 100644 --- a/src/input/state/tests/text_edit/commit_cancel.rs +++ b/src/input/state/tests/text_edit/commit_cancel.rs @@ -1,8 +1,10 @@ +use crate::draw::TextMeasurer; + use super::*; #[test] fn edit_selected_text_commit_updates_and_undo() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Text { x: 100, @@ -50,7 +52,7 @@ fn edit_selected_text_commit_updates_and_undo() { #[test] fn edit_selected_text_cancel_restores_original() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Text { x: 40, @@ -87,7 +89,7 @@ fn edit_selected_text_cancel_restores_original() { #[test] fn edit_selected_sticky_note_commit_updates_and_undo() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let background = Color { r: 0.9, @@ -157,7 +159,7 @@ fn edit_selected_sticky_note_commit_updates_and_undo() { #[test] fn edit_selected_sticky_note_cancel_restores_original() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let background = Color { r: 0.3, diff --git a/src/input/state/tests/text_edit/resize.rs b/src/input/state/tests/text_edit/resize.rs index f31a95529..c848af88a 100644 --- a/src/input/state/tests/text_edit/resize.rs +++ b/src/input/state/tests/text_edit/resize.rs @@ -1,8 +1,10 @@ +use crate::draw::TextMeasurer; + use super::*; #[test] fn dragging_text_resize_handle_updates_wrap_width_within_screen() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.update_screen_dimensions(300, 200); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Text { diff --git a/src/input/state/tests/text_input/actions.rs b/src/input/state/tests/text_input/actions.rs index 9d3fa3ec8..dff17ce5f 100644 --- a/src/input/state/tests/text_input/actions.rs +++ b/src/input/state/tests/text_input/actions.rs @@ -1,9 +1,12 @@ +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::super::*; #[test] fn test_redo_restores_shape_after_undo() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -43,8 +46,8 @@ fn test_redo_restores_shape_after_undo() { #[test] fn capture_action_sets_pending_and_clears_modifiers() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -74,8 +77,8 @@ fn capture_action_sets_pending_and_clears_modifiers() { #[test] fn region_capture_action_preserves_live_shift_until_the_picker_arms() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -99,8 +102,8 @@ fn region_capture_action_preserves_live_shift_until_the_picker_arms() { #[test] fn canvas_export_action_sets_pending_backend_action() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -120,8 +123,8 @@ fn canvas_export_action_sets_pending_backend_action() { #[test] fn board_pdf_export_action_sets_pending_backend_action() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -141,8 +144,8 @@ fn board_pdf_export_action_sets_pending_backend_action() { #[test] fn all_boards_pdf_export_action_sets_pending_backend_action() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, diff --git a/src/input/state/tests/text_input/editing.rs b/src/input/state/tests/text_input/editing.rs index ffeb06112..5eb390e34 100644 --- a/src/input/state/tests/text_input/editing.rs +++ b/src/input/state/tests/text_input/editing.rs @@ -4,6 +4,9 @@ //! unit-tested in `actions::key_press::caret_edit`; these assert the wiring: //! key rerouting, selection, and clipboard intent capture. +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::super::*; fn text_state(buffer: &str) -> InputState { @@ -152,7 +155,7 @@ fn collapsing_a_selection_at_its_caret_boundary_marks_an_external_change() { #[test] fn keyboard_pointer_and_paste_changes_mark_an_external_ime_update() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); use crate::input::MouseButton; let mut state = text_state("abc"); @@ -272,7 +275,7 @@ fn ctrl_x_without_a_selection_falls_through_without_editing() { #[test] fn ctrl_x_deletes_the_selection_only_after_clipboard_publication() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = text_state("hello"); state.modifiers.shift = true; state.on_key_press(Key::Home); @@ -317,7 +320,7 @@ fn repeated_ctrl_x_requests_are_retained_before_backend_draining() { #[test] fn stale_cut_completion_never_deletes_later_edits() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = text_state("hello"); state.modifiers.shift = true; state.on_key_press(Key::Home); @@ -337,7 +340,7 @@ fn stale_cut_completion_never_deletes_later_edits() { #[test] fn cut_completion_is_invalid_after_intervening_edits_restore_the_same_selection() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = text_state("hello"); state.modifiers.ctrl = true; state.on_key_press(Key::Char('a')); @@ -373,7 +376,7 @@ fn cut_completion_is_invalid_after_intervening_edits_restore_the_same_selection( #[test] fn ctrl_v_requests_a_paste_that_inserts_at_the_caret() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = text_state("ac"); state.modifiers.ctrl = true; state.on_key_press(Key::Char('v')); @@ -410,7 +413,7 @@ fn repeated_ctrl_v_requests_are_retained_before_backend_draining() { #[test] fn delayed_paste_replaces_the_selection_captured_at_invocation() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = text_state("hello"); state.modifiers.ctrl = true; state.on_key_press(Key::Char('a')); @@ -433,8 +436,8 @@ fn delayed_paste_replaces_the_selection_captured_at_invocation() { #[test] fn paste_generation_does_not_match_a_later_text_edit() { - let measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &test_ui_engine, @@ -459,8 +462,8 @@ fn paste_generation_does_not_match_a_later_text_edit() { #[test] fn first_click_places_a_new_empty_text_block() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -641,7 +644,7 @@ fn click_after_visible_preedit_maps_back_to_the_committed_buffer() { .style .font_descriptor .to_pango_string(state.style.current_font_size); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let geometry = measurer .caret_geometry_text(preview, &font, None, 11) .expect("preview caret geometry is measurable"); @@ -657,7 +660,7 @@ fn click_after_visible_preedit_maps_back_to_the_committed_buffer() { #[test] fn edit_ghost_is_hidden_in_place_and_shown_after_moving() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); use crate::draw::Shape; diff --git a/src/input/state/tests/text_input/idle.rs b/src/input/state/tests/text_input/idle.rs index 02ee8e351..9dbb7f11a 100644 --- a/src/input/state/tests/text_input/idle.rs +++ b/src/input/state/tests/text_input/idle.rs @@ -1,3 +1,5 @@ +use crate::draw::color; + use super::super::*; use crate::config::{QuickColorPalette, QuickColorPaletteEntry}; @@ -33,7 +35,7 @@ fn idle_mode_plain_letters_use_configured_quick_palette() { state.set_quick_colors(QuickColorPalette::from_entries(vec![ QuickColorPaletteEntry { label: "First".to_string(), - color: crate::draw::color::RED, + color: color::RED, }, QuickColorPaletteEntry { label: "Configured".to_string(), diff --git a/src/input/state/tests/text_input/ime.rs b/src/input/state/tests/text_input/ime.rs index 69ac68e35..0def3ce45 100644 --- a/src/input/state/tests/text_input/ime.rs +++ b/src/input/state/tests/text_input/ime.rs @@ -3,6 +3,8 @@ //! `ime_apply_done` coordinator, and assert the caret-aware editor buffer and //! transient preedit follow the protocol's batch-then-done model. +use crate::draw::TextMeasurer; + use super::super::*; fn enter_text_mode(state: &mut InputState) { @@ -378,7 +380,7 @@ fn preedit_replacing_a_forward_selection_has_one_effective_preview_and_cursor() #[test] fn preedit_start_removes_selection_and_invalidates_pending_clipboard_edit() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = create_test_input_state(); state.state = DrawingState::text_input(0, 0, "hello world".to_string()); if let DrawingState::TextInput { diff --git a/src/input/state/tests/text_input/placement.rs b/src/input/state/tests/text_input/placement.rs index 9e464dace..206d20e93 100644 --- a/src/input/state/tests/text_input/placement.rs +++ b/src/input/state/tests/text_input/placement.rs @@ -1,6 +1,9 @@ +use crate::ui_text::UiTextEngine; + use super::super::*; use crate::draw::TextMeasurer; use crate::draw::shape::{bounding_box_for_sticky_note_preview_with, bounding_box_for_text_with}; +use crate::input::BOARD_ID_WHITEBOARD; use crate::util::Rect; const OUTPUT: (u32, u32) = (800, 600); @@ -13,7 +16,7 @@ fn output_state() -> InputState { fn enter_mode(state: &mut InputState, action: Action) { let measurer = TextMeasurer::default(); - let ui_engine = crate::ui_text::UiTextEngine::default(); + let ui_engine = UiTextEngine::default(); let resources = crate::input::state::InputTextResources { measurer: &measurer, ui_engine: &ui_engine, @@ -189,7 +192,7 @@ type MovedView = fn(&mut InputState); fn keyboard_text_entry_starts_at_the_centre_of_a_panned_or_zoomed_view() { let views: [(&str, MovedView); 2] = [ ("panned", |state| { - state.switch_board(crate::input::BOARD_ID_WHITEBOARD); + state.switch_board(BOARD_ID_WHITEBOARD); assert!(state.boards.active_frame_mut().set_view_offset(1500, 900)); }), ("zoomed", |state| { diff --git a/src/input/state/tests/text_input/text_mode.rs b/src/input/state/tests/text_input/text_mode.rs index b210ec00c..85a45bf98 100644 --- a/src/input/state/tests/text_input/text_mode.rs +++ b/src/input/state/tests/text_input/text_mode.rs @@ -1,3 +1,6 @@ +use crate::config::{KeyBinding, KeybindingsConfig, Shortcut, ShortcutTrigger}; +use crate::input::state::actions::key_press::bindings; + use super::super::*; #[test] @@ -161,20 +164,17 @@ const FUNCTION_KEYS: [Key; 12] = [ ]; fn function_key_label(key: Key) -> String { - crate::input::state::actions::key_press::bindings::key_to_action_label(key) - .expect("function keys have binding labels") + bindings::key_to_action_label(key).expect("function keys have binding labels") } /// Every default binding on a function key, with the key that triggers it. -fn default_function_key_bindings() -> Vec<(crate::config::KeyBinding, Key, Action)> { - let map = crate::config::KeybindingsConfig::default() +fn default_function_key_bindings() -> Vec<(KeyBinding, Key, Action)> { + let map = KeybindingsConfig::default() .build_action_map() .expect("default keybindings build"); let mut bindings = Vec::new(); for (shortcut, action) in map { - let crate::config::Shortcut::Single(crate::config::ShortcutTrigger::Keyboard(binding)) = - shortcut - else { + let Shortcut::Single(ShortcutTrigger::Keyboard(binding)) = shortcut else { continue; }; if let Some(key) = FUNCTION_KEYS @@ -239,7 +239,7 @@ fn every_default_function_key_binding_works_while_typing() { fn user_bound_function_keys_work_while_typing() { for key in FUNCTION_KEYS { let label = function_key_label(key); - let mut keybindings = crate::config::KeybindingsConfig::default(); + let mut keybindings = KeybindingsConfig::default(); for bound in [ &mut keybindings.ui.toggle_help, &mut keybindings.ui.toggle_toolbar, @@ -250,7 +250,7 @@ fn user_bound_function_keys_work_while_typing() { bound.retain(|existing| *existing != label); } keybindings.ui.toggle_status_bar = vec![label.clone()]; - let mut state = create_test_input_state_with_keybindings(keybindings); + let mut state = create_test_input_state_with_keybindings(&keybindings); state.state = DrawingState::text_input(100, 100, String::from("draft")); let shown = state.ui_visibility.show_status_bar; diff --git a/src/input/state/tests/tool_controls.rs b/src/input/state/tests/tool_controls.rs index a271f83bc..b0840f245 100644 --- a/src/input/state/tests/tool_controls.rs +++ b/src/input/state/tests/tool_controls.rs @@ -1,3 +1,14 @@ +use std::fs; + +use crate::config; +use crate::config::test_helpers::ConfigFileSnapshot; +use crate::config::{ + ChordModifiers, Config, QuickColorPalette, QuickColorPaletteEntry, test_helpers, +}; +use crate::draw::{TextMeasurer, color}; +use crate::input::state; +use crate::ui_text::UiTextEngine; + use super::*; use crate::config::{PresenterToolBehavior, PresetToolStatesConfig, ToolPresetConfig}; use crate::draw::{ArrowStyle, BlurStyle}; @@ -6,12 +17,12 @@ use crate::ui::toolbar::model::{StylePillControl, StylePillSpec, TopStripPlan}; use crate::ui::toolbar::{ToolContext, ToolOptionsKind, ToolbarEvent, ToolbarSnapshot}; /// Pointer releases publish config edits through the live runtime inventory. -fn runtime_quick_color_edits(state: &mut InputState) -> Vec { +fn runtime_quick_color_edits(state: &mut InputState) -> Vec { state - .drain_input_effects(crate::input::state::InputEffectDrain::Runtime) + .drain_input_effects(state::InputEffectDrain::Runtime) .into_iter() .filter_map(|effect| match effect { - crate::input::state::InputEffect::QuickColor(edit) => Some(edit), + state::InputEffect::QuickColor(edit) => Some(edit), _ => None, }) .collect() @@ -158,8 +169,8 @@ fn cycling_from_black_out_to_sampling_blur_requests_frozen_capture() { #[test] fn pick_screen_color_requests_backend_eyedropper_activation() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -561,8 +572,8 @@ fn tool_color_and_thickness_are_independent_between_pen_and_marker() { #[test] fn increase_thickness_action_changes_marker_width_not_marker_opacity() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -574,10 +585,7 @@ fn increase_thickness_action_changes_marker_width_not_marker_opacity() { let original_opacity = state.style.marker_opacity; let pen_thickness = state.thickness_for_tool(Tool::Pen); - state.handle_action_with_resources( - test_text_resources, - crate::config::Action::IncreaseThickness, - ); + state.handle_action_with_resources(test_text_resources, Action::IncreaseThickness); assert_eq!(state.thickness_for_tool(Tool::Marker), 25.0); assert_eq!(state.thickness_for_tool(Tool::Pen), pen_thickness); @@ -685,7 +693,7 @@ fn sync_modifiers_resyncs_current_settings_to_compositor_tool() { assert_eq!(state.style.current_color, line_color); assert_eq!(state.style.current_thickness, 14.0); - state.sync_modifiers(false, false, false, false); + state.sync_modifiers(ChordModifiers::default()); assert_eq!(state.active_tool(), Tool::Pen); assert_eq!(state.style.current_color, pen_color); assert_eq!(state.style.current_thickness, pen_thickness); @@ -796,7 +804,7 @@ fn accepting_a_recolor_keeps_the_swatch_and_queues_the_durable_write() { assert_eq!(state.style.quick_colors.color_for_index(2), Some(picked)); assert_eq!( runtime_quick_color_edits(&mut state), - vec![crate::input::state::QuickColorEdit { + vec![state::QuickColorEdit { index: 2, color: picked }] @@ -815,9 +823,7 @@ fn accepting_a_recolor_keeps_the_swatch_and_queues_the_durable_write() { fn recoloring_the_swatch_in_use_moves_the_tool_color_with_it() { let mut state = create_test_input_state(); let slot_color = state.style.quick_colors.color_for_index(3).expect("slot 3"); - assert!( - state.apply_color_from_ui_with_measurer(&crate::draw::TextMeasurer::default(), slot_color) - ); + assert!(state.apply_color_from_ui_with_measurer(&TextMeasurer::default(), slot_color)); state.preset_slots.restore_active(Some(2)); state.clear_session_dirty(); @@ -901,7 +907,7 @@ fn recolor_hex_entry_lands_on_the_swatch() { } assert!(state.color_picker_popup_commit_hex()); - let expected = crate::input::state::parse_hex_color("#1A2B3C").expect("hex"); + let expected = state::parse_hex_color("#1A2B3C").expect("hex"); assert_eq!(state.style.quick_colors.color_for_index(4), Some(expected)); assert_eq!(state.color_for_tool(Tool::Pen), tool_color); } @@ -936,11 +942,11 @@ fn default_button_appears_only_for_slots_the_shipped_palette_defines() { state.close_color_picker_popup(true); // A slot the user added past the built-in palette has no default to offer. - state.set_quick_colors(crate::config::QuickColorPalette::from_entries( + state.set_quick_colors(QuickColorPalette::from_entries( (0..13) - .map(|index| crate::config::QuickColorPaletteEntry { + .map(|index| QuickColorPaletteEntry { label: format!("Custom {index}"), - color: crate::draw::color::RED, + color: color::RED, }) .collect(), )); @@ -960,8 +966,8 @@ fn default_button_appears_only_for_slots_the_shipped_palette_defines() { #[test] fn default_button_stages_the_shipped_color_for_ok_to_accept() { let mut state = create_test_input_state(); - let shipped = crate::config::default_quick_color_for_index(1).expect("built-in slot 1"); - let customized = crate::draw::Color { + let shipped = config::default_quick_color_for_index(1).expect("built-in slot 1"); + let customized = Color { r: 0.1, g: 0.2, b: 0.9, @@ -990,7 +996,7 @@ fn default_button_stages_the_shipped_color_for_ok_to_accept() { assert_eq!(state.style.quick_colors.color_for_index(1), Some(shipped)); assert_eq!( runtime_quick_color_edits(&mut state), - vec![crate::input::state::QuickColorEdit { + vec![state::QuickColorEdit { index: 1, color: shipped }] @@ -1000,7 +1006,7 @@ fn default_button_stages_the_shipped_color_for_ok_to_accept() { #[test] fn canceling_after_default_keeps_the_customized_color() { let mut state = create_test_input_state(); - let customized = crate::draw::Color { + let customized = Color { r: 0.4, g: 0.1, b: 0.3, @@ -1047,7 +1053,7 @@ fn accepting_a_recolor_from_the_popup_release_queues_the_durable_write() { assert_eq!(state.style.quick_colors.color_for_index(0), Some(picked)); assert_eq!( runtime_quick_color_edits(&mut state), - vec![crate::input::state::QuickColorEdit { + vec![state::QuickColorEdit { index: 0, color: picked }] @@ -1095,10 +1101,10 @@ fn popup_title_names_the_slot_being_recolored() { #[test] fn popup_title_flattens_and_bounds_an_authored_label() { let mut state = create_test_input_state(); - state.set_quick_colors(crate::config::QuickColorPalette::from_entries(vec![ - crate::config::QuickColorPaletteEntry { + state.set_quick_colors(QuickColorPalette::from_entries(vec![ + QuickColorPaletteEntry { label: " Ünreasonable\n\tlabel with breaks ".to_string(), - color: crate::draw::color::RED, + color: color::RED, }, ])); @@ -1114,10 +1120,10 @@ fn popup_title_flattens_and_bounds_an_authored_label() { // A pathological label is bounded so the renderer's width measurement never // walks a huge string. Fitting it to the panel stays the renderer's job. state.close_color_picker_popup(true); - state.set_quick_colors(crate::config::QuickColorPalette::from_entries(vec![ - crate::config::QuickColorPaletteEntry { + state.set_quick_colors(QuickColorPalette::from_entries(vec![ + QuickColorPaletteEntry { label: "W".repeat(5_000), - color: crate::draw::color::RED, + color: color::RED, }, ])); assert!(state.open_color_picker_popup_for_quick_color(0)); @@ -1244,7 +1250,7 @@ fn restoring_recent_colors_reapplies_the_cap_and_dedupe() { }); } - state.restore_recent_colors(oversized); + state.restore_recent_colors(&oversized); let recents = state.recent_colors(); assert_eq!(recents.len(), 6, "cap survives a hand-edited session"); @@ -1265,9 +1271,9 @@ fn recent_swatches_are_hit_testable_in_the_popup() { b: 0.875, a: 1.0, }; - state.apply_color_from_ui_with_measurer(&crate::draw::TextMeasurer::default(), stashed); + state.apply_color_from_ui_with_measurer(&TextMeasurer::default(), stashed); state.apply_color_from_ui_with_measurer( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), Color { r: 1.0, g: 1.0, @@ -1311,11 +1317,11 @@ fn empty_recents_have_no_clickable_swatches() { // click that does nothing. assert_eq!( layout.cursor_hint_at(sx + 2.0, sy + 2.0, 0), - crate::input::state::ColorPickerCursorHint::Default + state::ColorPickerCursorHint::Default ); assert_eq!( layout.cursor_hint_at(sx + 2.0, sy + 2.0, 1), - crate::input::state::ColorPickerCursorHint::Pointer, + state::ColorPickerCursorHint::Pointer, "a rendered swatch still gets the pointer" ); } @@ -1402,7 +1408,7 @@ fn a_picker_drag_released_over_a_recent_swatch_does_not_adopt_it() { b: 0.875, a: 1.0, }; - state.apply_color_from_ui_with_measurer(&crate::draw::TextMeasurer::default(), stashed); + state.apply_color_from_ui_with_measurer(&TextMeasurer::default(), stashed); state.open_color_picker_popup(); state.color_picker_popup_set_hue(0.1); @@ -1715,7 +1721,7 @@ fn color_picker_set_color_updates_live_preview_and_buffer() { assert_eq!(state.color_picker_popup_current_color(), Some(pasted)); assert_eq!( state.color_picker_popup_hex_buffer(), - Some(crate::input::state::color_to_hex(pasted).as_str()) + Some(state::color_to_hex(pasted).as_str()) ); assert!(!state.color_picker_popup_is_hex_editing()); // Previews live on the editing tool, like a gradient drag. @@ -1741,7 +1747,7 @@ fn typing_a_hex_key_starts_manual_entry_without_a_click() { assert_eq!(state.color_picker_popup_hex_buffer(), Some("A1B2C3")); assert_eq!( state.color_picker_popup_current_color(), - crate::input::state::parse_hex_color("A1B2C3") + state::parse_hex_color("A1B2C3") ); } @@ -1761,7 +1767,7 @@ fn hex_typing_defers_live_preview_until_six_digits() { for ch in ['2', 'c', '3'] { state.on_key_press(Key::Char(ch)); } - let expected = crate::input::state::parse_hex_color("A1B2C3").unwrap(); + let expected = state::parse_hex_color("A1B2C3").unwrap(); assert_eq!(state.color_picker_popup_current_color(), Some(expected)); } @@ -1776,7 +1782,7 @@ fn three_digit_hex_still_applies_when_committed() { state.on_key_press(Key::Return); - let expected = crate::input::state::parse_hex_color("A1B").unwrap(); + let expected = state::parse_hex_color("A1B").unwrap(); assert_eq!(state.color_picker_popup_current_color(), Some(expected)); assert_eq!(state.color_picker_popup_hex_buffer(), Some("#AA11BB")); assert!(!state.color_picker_popup_is_hex_editing()); @@ -1802,7 +1808,7 @@ fn color_picker_ok_button_commits_valid_three_digit_hex_before_apply() { assert!(state.handle_color_picker_press(MouseButton::Left, x, y)); assert!(state.handle_color_picker_popup_release_at(x, y)); - let expected = crate::input::state::parse_hex_color("A1B").unwrap(); + let expected = state::parse_hex_color("A1B").unwrap(); assert!(!state.is_color_picker_popup_open()); assert_eq!(state.color_for_tool(Tool::Pen), expected); assert_eq!(state.style.current_color, expected); @@ -2114,7 +2120,7 @@ fn toolbar_preset_preview_uses_nested_profile_for_active_preset_tool() { drag_tools: None, }); - let snapshot = crate::ui::toolbar::ToolbarSnapshot::from_input(&state); + let snapshot = ToolbarSnapshot::from_input(&state); let preset = snapshot.presets[0].as_ref().expect("preset preview"); let eraser_preset = snapshot.presets[1].as_ref().expect("eraser preset preview"); @@ -2336,26 +2342,24 @@ fn set_marker_opacity_clamps_and_reports_noop_after_reaching_target() { /// beside `handle_quick_color_edit`. #[test] fn a_quick_color_recolor_queues_the_write_without_touching_the_file_itself() { - crate::config::test_helpers::with_temp_config_home(|config_root| { - let config_dir = config_root.join(crate::config::PRIMARY_CONFIG_DIR); - std::fs::create_dir_all(&config_dir).expect("test config directory"); + test_helpers::with_temp_config_home(|config_root| { + let config_dir = config_root.join(config::PRIMARY_CONFIG_DIR); + fs::create_dir_all(&config_dir).expect("test config directory"); let path = config_dir.join("config.toml"); - std::fs::write( + fs::write( &path, "[[drawing.quick_colors]]\nlabel = 'Authored'\ncolor = '#112233'\n", ) .expect("test config should be written"); - let snapshot = crate::config::test_helpers::ConfigFileSnapshot::capture(&path); + let snapshot = ConfigFileSnapshot::capture(&path); - let configured = crate::config::Config::load() - .expect("test config should load") - .config; + let configured = Config::load().expect("test config should load").config; let authored = configured.drawing.quick_colors.effective_entries()[0] .color .clone(); let mut state = create_test_input_state(); - state.set_quick_colors(crate::config::QuickColorPalette::from_config( + state.set_quick_colors(QuickColorPalette::from_config( &configured.drawing.quick_colors, )); assert!(state.open_color_picker_popup_for_quick_color(0)); @@ -2368,16 +2372,14 @@ fn a_quick_color_recolor_queues_the_write_without_touching_the_file_itself() { assert_eq!(state.style.quick_colors.color_for_index(0), Some(picked)); assert_eq!( runtime_quick_color_edits(&mut state), - vec![crate::input::state::QuickColorEdit { + vec![state::QuickColorEdit { index: 0, color: picked }] ); snapshot.assert_unchanged("accepting a quick-color recolor in InputState"); - let restarted = crate::config::Config::load() - .expect("test config should reload") - .config; + let restarted = Config::load().expect("test config should reload").config; assert_eq!( restarted.drawing.quick_colors.effective_entries()[0].color, authored, @@ -2388,8 +2390,8 @@ fn a_quick_color_recolor_queues_the_write_without_touching_the_file_itself() { #[test] fn cycling_arrow_style_with_nothing_selected_only_moves_the_next_arrow() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -2432,8 +2434,8 @@ fn cycling_arrow_style_with_nothing_selected_only_moves_the_next_arrow() { #[test] fn cycling_arrow_style_with_arrows_selected_restyles_them_instead() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -2477,8 +2479,8 @@ fn cycling_arrow_style_with_arrows_selected_restyles_them_instead() { #[test] fn cycling_arrow_style_with_a_non_arrow_selected_falls_back_to_the_default() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -2512,9 +2514,9 @@ fn bold_reaches_selected_text_and_otherwise_sets_what_the_next_label_uses() { x: 10, y: 10, text: "hello".to_string(), - color: crate::draw::Color::new(1.0, 1.0, 1.0, 1.0), + color: Color::new(1.0, 1.0, 1.0, 1.0), size: 24.0, - font_descriptor: crate::draw::FontDescriptor::new( + font_descriptor: FontDescriptor::new( "Sans".to_string(), "normal".to_string(), "normal".to_string(), @@ -2556,7 +2558,7 @@ fn rendered_bold_control_reads_and_mutates_the_selected_text_target() { // toggle whose click bolds the selection, not a checked toggle whose click // sends the no-op "turn normal" event. let mut state = create_test_input_state(); - state.set_font_descriptor(crate::draw::FontDescriptor::new( + state.set_font_descriptor(FontDescriptor::new( "Sans".to_string(), "bold".to_string(), "normal".to_string(), @@ -2565,9 +2567,9 @@ fn rendered_bold_control_reads_and_mutates_the_selected_text_target() { x: 10, y: 10, text: "selected".to_string(), - color: crate::draw::Color::new(1.0, 1.0, 1.0, 1.0), + color: Color::new(1.0, 1.0, 1.0, 1.0), size: 24.0, - font_descriptor: crate::draw::FontDescriptor::new( + font_descriptor: FontDescriptor::new( "Serif".to_string(), "normal".to_string(), "normal".to_string(), @@ -2612,9 +2614,9 @@ fn rendered_bold_control_skips_locked_text_and_disables_without_an_editable_targ x: 10, y: 10, text: "locked bold".to_string(), - color: crate::draw::Color::new(1.0, 1.0, 1.0, 1.0), + color: Color::new(1.0, 1.0, 1.0, 1.0), size: 24.0, - font_descriptor: crate::draw::FontDescriptor::new( + font_descriptor: FontDescriptor::new( "Sans".to_string(), "bold".to_string(), "normal".to_string(), @@ -2632,9 +2634,9 @@ fn rendered_bold_control_skips_locked_text_and_disables_without_an_editable_targ x: 20, y: 20, text: "editable normal".to_string(), - color: crate::draw::Color::new(1.0, 1.0, 1.0, 1.0), + color: Color::new(1.0, 1.0, 1.0, 1.0), size: 24.0, - font_descriptor: crate::draw::FontDescriptor::new( + font_descriptor: FontDescriptor::new( "Sans".to_string(), "normal".to_string(), "normal".to_string(), @@ -2683,7 +2685,7 @@ fn rendered_bold_control_skips_locked_text_and_disables_without_an_editable_targ #[test] fn turning_bold_off_leaves_the_family_and_style_alone() { let mut state = create_test_input_state(); - state.set_font_descriptor(crate::draw::FontDescriptor::new( + state.set_font_descriptor(FontDescriptor::new( "Serif".to_string(), "bold".to_string(), "italic".to_string(), diff --git a/src/input/state/tests/toolbar_display.rs b/src/input/state/tests/toolbar_display.rs index c65c9c273..4478f81ed 100644 --- a/src/input/state/tests/toolbar_display.rs +++ b/src/input/state/tests/toolbar_display.rs @@ -1,3 +1,9 @@ +use crate::config::PresenterToolBehavior; +use crate::draw::TextMeasurer; +use crate::input::Tool; +use crate::ui::toolbar::ToolbarEvent; +use crate::ui_text::UiTextEngine; + use super::create_test_input_state; use crate::config::{ Action, RadialMenuMouseBinding, StatusBarItem, StatusBarStyle, StatusPosition, TopDisplayMode, @@ -16,8 +22,8 @@ fn unbind_chrome_visibility_actions(state: &mut InputState) { } fn hide_all_chrome(state: &mut InputState) { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -40,8 +46,8 @@ fn refresh_status_hud_layout(state: &mut InputState) { #[test] fn cycle_action_walks_full_micro_hidden_full_with_toasts() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -90,8 +96,8 @@ fn cycle_action_walks_full_micro_hidden_full_with_toasts() { #[test] fn entering_micro_unminimizes_and_closes_top_menus() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -121,8 +127,8 @@ fn entering_micro_unminimizes_and_closes_top_menus() { #[test] fn toggle_toolbar_show_restores_a_cycle_hidden_top_strip() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -148,8 +154,8 @@ fn toggle_toolbar_show_restores_a_cycle_hidden_top_strip() { #[test] fn toggle_toolbar_drives_the_top_pin_and_queues_its_persistence() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -188,8 +194,8 @@ fn toggle_toolbar_drives_the_top_pin_and_queues_its_persistence() { /// the visible state the user was looking at. #[test] fn toggle_toolbar_resolves_pins_to_what_is_on_screen() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -223,8 +229,8 @@ fn toggle_toolbar_resolves_pins_to_what_is_on_screen() { /// runtime-only, exactly like F2's hidden rung. #[test] fn cycle_hidden_show_with_unchanged_pins_queues_no_persistence() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -254,8 +260,8 @@ fn cycle_hidden_show_with_unchanged_pins_queues_no_persistence() { /// additional persistence and queues nothing. #[test] fn hide_with_an_already_unpinned_strip_queues_no_persistence() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -277,8 +283,8 @@ fn hide_with_an_already_unpinned_strip_queues_no_persistence() { #[test] fn presenter_swallowed_toggle_leaves_pins_and_persistence_untouched() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -302,8 +308,8 @@ fn presenter_swallowed_toggle_leaves_pins_and_persistence_untouched() { /// flags or queue the visibility persistence the explicit toggle uses. #[test] fn focus_and_presenter_transitions_never_queue_pin_persistence() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -341,8 +347,8 @@ fn focus_and_presenter_transitions_never_queue_pin_persistence() { /// (correctly) drops. #[test] fn a_toggle_and_a_cycle_in_one_batch_both_keep_their_persistence() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -375,8 +381,8 @@ fn a_toggle_and_a_cycle_in_one_batch_both_keep_their_persistence() { /// persistence queue, and neither may cost the other its delivery. #[test] fn visibility_persistence_survives_a_capture_request() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -422,8 +428,8 @@ fn visibility_persistence_survives_a_capture_request() { /// write would be byte-identical to its own rollback. #[test] fn a_toggle_burst_coalesces_to_the_original_rollback_baseline() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -451,8 +457,8 @@ fn a_toggle_burst_coalesces_to_the_original_rollback_baseline() { /// side may drop a queued entry when the same batch also requests an exit. #[test] fn an_exit_request_does_not_clear_queued_toolbar_persistence() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -475,8 +481,8 @@ fn an_exit_request_does_not_clear_queued_toolbar_persistence() { #[test] fn micro_form_survives_a_visibility_toggle() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -495,8 +501,8 @@ fn micro_form_survives_a_visibility_toggle() { #[test] fn hidden_cycle_toast_offers_a_show_action() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -516,8 +522,8 @@ fn hidden_cycle_toast_offers_a_show_action() { #[test] fn hiding_the_last_chrome_surface_warns_with_recovery_bindings() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -552,8 +558,8 @@ fn hiding_the_last_chrome_surface_warns_with_recovery_bindings() { #[test] fn enabled_but_empty_status_bar_does_not_suppress_chrome_recovery_warning() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -564,8 +570,8 @@ fn enabled_but_empty_status_bar_does_not_suppress_chrome_recovery_warning() { assert!(state.status_hud_layout().is_some()); for item in StatusBarItem::ALL { state.set_status_bar_item_visible_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), item, false, ); @@ -581,8 +587,8 @@ fn enabled_but_empty_status_bar_does_not_suppress_chrome_recovery_warning() { assert!(!state.status_hud_effectively_visible()); assert!(state.set_status_bar_item_visible_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), StatusBarItem::About, true )); @@ -595,8 +601,8 @@ fn enabled_but_empty_status_bar_does_not_suppress_chrome_recovery_warning() { "policy sees the synchronously refreshed measured cache" ); assert!(state.set_status_bar_item_visible_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), StatusBarItem::About, false )); @@ -624,8 +630,8 @@ fn width_shed_content_never_reports_an_effectively_visible_hud() { let mut state = create_test_input_state(); for item in StatusBarItem::ALL { state.set_status_bar_item_visible_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), item, false, ); @@ -639,8 +645,8 @@ fn width_shed_content_never_reports_an_effectively_visible_hud() { assert!(state.status_hud_layout().is_none()); assert!(state.set_status_bar_item_visible_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), StatusBarItem::About, true )); @@ -656,8 +662,8 @@ fn width_shed_content_never_reports_an_effectively_visible_hud() { #[test] fn toolbar_hint_prevents_a_false_all_chrome_warning_when_it_becomes_visible() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -666,8 +672,8 @@ fn toolbar_hint_prevents_a_false_all_chrome_warning_when_it_becomes_visible() { let mut state = create_test_input_state(); for item in StatusBarItem::ALL { state.set_status_bar_item_visible_with_resources( - &crate::ui_text::UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &UiTextEngine::default(), + &TextMeasurer::default(), item, item == StatusBarItem::ToolbarHint, ); @@ -693,8 +699,8 @@ fn toolbar_hint_prevents_a_false_all_chrome_warning_when_it_becomes_visible() { #[test] fn all_chrome_warning_fires_from_the_cycle_path_and_supersedes_its_toast() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -768,8 +774,8 @@ fn unbound_chrome_warning_advertises_right_click_only_when_it_can_open_the_menu( #[test] fn all_chrome_warning_suppressed_while_presenting() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -793,8 +799,8 @@ fn all_chrome_warning_suppressed_while_presenting() { #[test] fn all_chrome_warning_fires_when_presenter_mode_did_not_hide_any_chrome() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -822,8 +828,8 @@ fn all_chrome_warning_fires_when_presenter_mode_did_not_hide_any_chrome() { #[test] fn presenter_owned_hidden_toolbar_falls_back_to_status_bar_recovery() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -847,8 +853,8 @@ fn presenter_owned_hidden_toolbar_falls_back_to_status_bar_recovery() { #[test] fn context_menu_offers_recovery_entries_only_while_chrome_hidden() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -908,8 +914,8 @@ fn context_menu_offers_recovery_entries_only_while_chrome_hidden() { #[test] fn presenter_mode_gates_the_cycle_like_toggle_toolbar() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -929,8 +935,8 @@ fn presenter_mode_gates_the_cycle_like_toggle_toolbar() { #[test] fn presenter_mode_gates_the_micro_chip_event_like_the_cycle_action() { - let route_measurer = crate::draw::TextMeasurer::default(); - let route_ui_engine = crate::ui_text::UiTextEngine::default(); + let route_measurer = TextMeasurer::default(); + let route_ui_engine = UiTextEngine::default(); let route_resources = crate::input::state::InputTextResources { measurer: &route_measurer, ui_engine: &route_ui_engine, @@ -974,8 +980,8 @@ fn presenter_mode_gates_the_micro_chip_event_like_the_cycle_action() { /// autosave clobber it. #[test] fn session_independent_chrome_actions_never_mark_the_session_dirty() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -986,8 +992,7 @@ fn session_independent_chrome_actions_never_mark_the_session_dirty() { // would also take the tool override, which *is* session content. state.presenter_mode_config_mut_for_test().hide_status_bar = true; state.presenter_mode_config_mut_for_test().hide_toolbars = true; - state.presenter_mode_config_mut_for_test().tool_behavior = - crate::config::PresenterToolBehavior::Keep; + state.presenter_mode_config_mut_for_test().tool_behavior = PresenterToolBehavior::Keep; state .presenter_mode_config_mut_for_test() .enable_click_highlight = false; @@ -1027,7 +1032,7 @@ fn session_independent_chrome_actions_never_mark_the_session_dirty() { // Control: a change the snapshot does carry still marks the session dirty, // so the assertions above are about the toggles and not about a dirty flag // that stopped working. - assert!(state.set_tool_override(Some(crate::input::Tool::Line))); + assert!(state.set_tool_override(Some(Tool::Line))); assert!(state.is_session_dirty()); } @@ -1172,8 +1177,8 @@ fn run_only_toolbar_preference_events_never_mark_the_session_dirty() { #[test] fn micro_chip_event_restores_the_full_strip() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -1181,16 +1186,8 @@ fn micro_chip_event_restores_the_full_strip() { let mut state = create_test_input_state(); state.handle_action_with_resources(test_text_resources, Action::CycleToolbarDisplay); // micro - assert!( - state.apply_toolbar_event(crate::ui::toolbar::ToolbarEvent::SetTopDisplayMode( - TopDisplayMode::Full - )) - ); + assert!(state.apply_toolbar_event(ToolbarEvent::SetTopDisplayMode(TopDisplayMode::Full))); assert_eq!(state.top_display_state(), TopDisplayMode::Full); // Idempotent: applying the current state reports no change. - assert!( - !state.apply_toolbar_event(crate::ui::toolbar::ToolbarEvent::SetTopDisplayMode( - TopDisplayMode::Full - )) - ); + assert!(!state.apply_toolbar_event(ToolbarEvent::SetTopDisplayMode(TopDisplayMode::Full))); } diff --git a/src/input/state/tests/transform.rs b/src/input/state/tests/transform.rs index 2b08296f5..0e7295648 100644 --- a/src/input/state/tests/transform.rs +++ b/src/input/state/tests/transform.rs @@ -1,8 +1,11 @@ +use crate::draw::{ArrowStyle, TextMeasurer}; +use crate::ui_text::UiTextEngine; + use super::*; #[test] fn translate_selection_with_undo_moves_shape() { - let route_measurer = crate::draw::TextMeasurer::default(); + let route_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Line { x1: 0, @@ -46,7 +49,7 @@ fn translate_selection_with_undo_moves_shape() { #[test] fn resizing_selection_marks_previous_live_bounds_dirty() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Rect { @@ -106,7 +109,7 @@ fn resizing_selection_marks_previous_live_bounds_dirty() { #[test] fn resizing_selection_back_to_start_restores_original_geometry() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Rect { @@ -171,8 +174,8 @@ fn resizing_selection_back_to_start_restores_original_geometry() { #[test] fn move_selection_to_horizontal_edges_uses_screen_bounds() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -213,8 +216,8 @@ fn move_selection_to_horizontal_edges_uses_screen_bounds() { #[test] fn move_selection_to_horizontal_edges_ignores_last_axis() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -256,8 +259,8 @@ fn move_selection_to_horizontal_edges_ignores_last_axis() { #[test] fn move_selection_to_vertical_edges_explicit_actions() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -298,8 +301,8 @@ fn move_selection_to_vertical_edges_explicit_actions() { #[test] fn nudge_selection_large_uses_large_step() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -330,8 +333,8 @@ fn nudge_selection_large_uses_large_step() { #[test] fn nudge_selection_clamps_left_and_top_edges() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -362,8 +365,8 @@ fn nudge_selection_clamps_left_and_top_edges() { #[test] fn nudge_selection_clamps_right_and_bottom_edges() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -397,7 +400,7 @@ fn nudge_selection_clamps_right_and_bottom_edges() { #[test] fn restore_selection_snapshots_reverts_translation() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = create_test_input_state(); let shape_id = state.boards.active_frame_mut().add_shape(Shape::Text { @@ -416,7 +419,7 @@ fn restore_selection_snapshots_reverts_translation() { assert_eq!(snapshots.len(), 1); assert!(state.apply_translation_to_selection_with(&test_text_measurer, 20, 30)); - state.restore_selection_from_snapshots_with(&crate::draw::TextMeasurer::default(), snapshots); + state.restore_selection_from_snapshots_with(&TextMeasurer::default(), snapshots); let frame = state.boards.active_frame(); let shape = frame.shape(shape_id).unwrap(); @@ -430,7 +433,7 @@ fn restore_selection_snapshots_reverts_translation() { #[test] fn resizing_a_curved_arrow_keeps_its_style_and_curvature() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); // `Shape::scaled` rebuilds `Shape::Arrow` field by field, so a field left out // there silently resets on every resize. `style` has to survive untouched; @@ -447,7 +450,7 @@ fn resizing_a_curved_arrow_keeps_its_style_and_curvature() { arrow_length: 20.0, arrow_angle: 30.0, head_at_end: true, - style: crate::draw::ArrowStyle::Curved, + style: ArrowStyle::Curved, bend: 0.4, label: None, }); @@ -476,11 +479,7 @@ fn resizing_a_curved_arrow_keeps_its_style_and_curvature() { x2, .. } => { - assert_eq!( - *style, - crate::draw::ArrowStyle::Curved, - "resize reset the style" - ); + assert_eq!(*style, ArrowStyle::Curved, "resize reset the style"); assert!(*bend > 0.0, "resize reset the bend"); assert!(x2 - x1 > 100, "test setup should have widened the arrow"); } @@ -490,7 +489,7 @@ fn resizing_a_curved_arrow_keeps_its_style_and_curvature() { #[test] fn stretching_a_flat_curved_arrow_downward_grows_its_arc() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); // A horizontal curved arrow's height is almost entirely its arc. Dragging // the bottom handle does not lengthen the chord, so a bend copied through @@ -508,7 +507,7 @@ fn stretching_a_flat_curved_arrow_downward_grows_its_arc() { arrow_length: 20.0, arrow_angle: 30.0, head_at_end: true, - style: crate::draw::ArrowStyle::Curved, + style: ArrowStyle::Curved, bend: 0.3, label: None, }); diff --git a/src/input/state/tests/zoom_chip.rs b/src/input/state/tests/zoom_chip.rs index a1c667ff8..f402728c1 100644 --- a/src/input/state/tests/zoom_chip.rs +++ b/src/input/state/tests/zoom_chip.rs @@ -5,6 +5,10 @@ //! ignored, and visibility/interactivity are gated on the effective //! `zoom_chip_enabled()` state. +use crate::config::ZoomChipDisplay; +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::*; use crate::config::StatusBarStyle; use crate::ui::{ZoomChipButtonKind, ZoomChipPress}; @@ -61,8 +65,8 @@ fn zoom_chip_layout_cleared_when_actions_hidden() { #[test] fn toggle_zoom_chip_action_hides_layout_and_hit_testing() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -75,7 +79,7 @@ fn toggle_zoom_chip_action_hides_layout_and_hit_testing() { // The palette/keybinding toggle hides the chip for this run without // touching the `show_zoom_actions` toolbar preference or queueing any // durable work. - input.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleZoomChip); + input.handle_action_with_resources(test_text_resources, Action::ToggleZoomChip); assert!(!input.zoom_chip_enabled()); assert!( input.ui_visibility.show_zoom_actions, @@ -87,7 +91,7 @@ fn toggle_zoom_chip_action_hides_layout_and_hit_testing() { assert!(!input.zoom_chip_contains(1270, 710)); // Toggling again restores it. - input.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleZoomChip); + input.handle_action_with_resources(test_text_resources, Action::ToggleZoomChip); assert!(input.zoom_chip_enabled()); update_chip_layout(&mut input, 1280, 720); assert!(input.zoom_chip_layout().is_some()); @@ -95,8 +99,8 @@ fn toggle_zoom_chip_action_hides_layout_and_hit_testing() { #[test] fn zoom_chip_hover_tracks_buttons_and_clears_when_hidden() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -112,7 +116,7 @@ fn zoom_chip_hover_tracks_buttons_and_clears_when_hidden() { assert!(input.needs_redraw, "hover transition requests a redraw"); // Runtime-hiding the chip clears hover on the next layout pass. - input.handle_action_with_resources(test_text_resources, crate::config::Action::ToggleZoomChip); + input.handle_action_with_resources(test_text_resources, Action::ToggleZoomChip); update_chip_layout(&mut input, 1280, 720); assert_eq!(input.zoom_chip.hover, None); } @@ -178,7 +182,7 @@ fn zoom_chip_layout_rebuild_preserves_cleared_hover_after_pointer_leave() { // must not reapply that stale hit while focus is elsewhere. input.clear_chrome_hover(); input.update_zoom_chip_layout_for_pointer_with_engine( - &crate::ui_text::UiTextEngine::default(), + &UiTextEngine::default(), &StatusBarStyle::default(), 1280, 720, @@ -194,7 +198,7 @@ fn zoom_chip_layout_rebuild_preserves_cleared_hover_after_pointer_leave() { #[test] fn while_zoomed_display_mode_shows_the_chip_only_during_zoom() { let mut input = create_test_input_state(); - input.zoom_chip.display = crate::config::ZoomChipDisplay::WhileZoomed; + input.zoom_chip.display = ZoomChipDisplay::WhileZoomed; // At 100% the corner stays clean: no layout, no hit. update_chip_layout(&mut input, 1280, 720); @@ -278,8 +282,8 @@ fn zoom_chip_click_lock_returns_toggle_lock_when_zoomed() { #[test] fn zoom_chip_activation_records_usage_and_coach_slow_path() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -444,9 +448,9 @@ fn tablet_path_press_release_dispatches_zoom_action() { assert_eq!(input.zoom_chip.press_pending, ZoomChipPress::None); assert_eq!( input.take_pending_zoom_request(), - Some(crate::input::ZoomRequest { + Some(ZoomRequest { action: ZoomAction::In, - anchor: crate::input::ZoomAnchor::ScreenCenter, + anchor: ZoomAnchor::ScreenCenter, }), "the chip sits in a corner, so its zoom centres the screen" ); @@ -481,7 +485,7 @@ fn zoom_chip_ignored_while_eclipsing_overlay_open() { // An overlay rendering above the chip suppresses its presses (also covers // an overlay opened between press and release: check_zoom_chip_click // shares the same guard). - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + input.open_board_picker_with_measurer(&TextMeasurer::default()); assert!(!input.zoom_chip_contains(x, y)); assert!(!input.check_zoom_chip_click(ZoomChipButtonKind::In, x, y).0); input.close_board_picker(); diff --git a/src/input/state/text_resources.rs b/src/input/state/text_resources.rs index 0bd9cc6e7..4ba162448 100644 --- a/src/input/state/text_resources.rs +++ b/src/input/state/text_resources.rs @@ -7,9 +7,9 @@ use crate::ui_text::UiTextEngine; /// This borrow is operation-local and must not be retained in input values, /// editor state, shapes, or snapshots. #[derive(Clone, Copy)] -pub(crate) struct InputTextResources<'a> { - pub(crate) measurer: &'a TextMeasurer, - pub(crate) ui_engine: &'a UiTextEngine, +pub(crate) struct InputTextResources<'text> { + pub(crate) measurer: &'text TextMeasurer, + pub(crate) ui_engine: &'text UiTextEngine, } #[cfg(test)] diff --git a/src/input/state/text_resources/tests.rs b/src/input/state/text_resources/tests.rs index 7b66e9d9c..8a3cf867b 100644 --- a/src/input/state/text_resources/tests.rs +++ b/src/input/state/text_resources/tests.rs @@ -1,16 +1,21 @@ +use crate::draw; +use crate::input::state::test_support; +use crate::ui::toolbar::ToolbarEvent; +use crate::ui::toolbar::model::ToolbarSliderSpec; + use super::*; use crate::draw::{FontDescriptor, Shape, ShapeId}; use crate::input::{DrawingState, InputState, Tool}; fn editing_state(measurer: &TextMeasurer) -> (InputState, ShapeId, Shape) { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.view.set_screen_dimensions(1000, 800); let original = Shape::Text { x: 100, y: 200, text: "Wrapped original שלום".into(), size: 24.0, - color: crate::draw::RED, + color: draw::RED, font_descriptor: FontDescriptor::default(), background_enabled: true, wrap_width: Some(140), @@ -46,12 +51,7 @@ fn explicit_page_transition_restores_text_before_changing_the_active_frame() { measurer: &measurer, ui_engine: &ui_engine, }; - assert!( - state.apply_toolbar_event_with_resources( - resources, - crate::ui::toolbar::ToolbarEvent::PageNew - ) - ); + assert!(state.apply_toolbar_event_with_resources(resources, ToolbarEvent::PageNew)); assert!(matches!(state.state, DrawingState::Idle)); assert_eq!(state.boards.active_page_index(), 1); assert!(state.boards.active_frame().shapes.is_empty()); @@ -187,7 +187,7 @@ fn explicit_precision_enter_uses_toolbar_clamping_and_escape_leaves_value_alone( measurer: &measurer, ui_engine: &ui_engine, }; - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); assert!(state.apply_toolbar_event_with_resources( resources, ToolbarEvent::OpenPrecisionEntry(PrecisionEntryTarget::Thickness) @@ -199,7 +199,7 @@ fn explicit_precision_enter_uses_toolbar_clamping_and_escape_leaves_value_alone( assert!(!state.is_precision_entry_open()); assert_eq!( state.thickness_for_active_tool(), - crate::ui::toolbar::model::ToolbarSliderSpec::THICKNESS.max + ToolbarSliderSpec::THICKNESS.max ); assert!(state.is_session_dirty()); diff --git a/src/input/tablet/mod.rs b/src/input/tablet/mod.rs index 383e96afd..a253d59d0 100644 --- a/src/input/tablet/mod.rs +++ b/src/input/tablet/mod.rs @@ -3,7 +3,9 @@ //! These helpers map tablet tool events (position, pressure) into the existing //! `InputState` without changing the drawing logic. -use crate::input::{InputState, state::MAX_STROKE_THICKNESS}; +use crate::draw::TextMeasurer; +use crate::input::InputState; +use crate::input::state::MAX_STROKE_THICKNESS; /// User-configurable settings for tablet input. #[derive(Debug, Clone, Copy)] @@ -31,12 +33,12 @@ impl Default for TabletSettings { /// Apply a normalized pressure value [0.0, 1.0] to a pressure-sensitive tool. pub fn apply_pressure_to_state(pressure01: f64, state: &mut InputState, settings: TabletSettings) { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let _ = try_apply_pressure_to_state_with(&measurer, pressure01, state, settings); } pub(crate) fn try_apply_pressure_to_state_with( - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, pressure01: f64, state: &mut InputState, settings: TabletSettings, @@ -68,12 +70,14 @@ pub(crate) fn try_apply_pressure_to_state_with( #[cfg(test)] mod tests { + use crate::input::state::test_support; + use super::*; use crate::draw::Shape; use crate::input::{DrawingState, MouseButton, Tool}; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } #[test] @@ -83,7 +87,7 @@ mod tests { state.needs_redraw = false; assert!(!try_apply_pressure_to_state_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), 0.8, &mut state, TabletSettings::default() @@ -105,7 +109,7 @@ mod tests { }; assert!(!try_apply_pressure_to_state_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), 0.8, &mut state, settings @@ -127,7 +131,7 @@ mod tests { }; assert!(try_apply_pressure_to_state_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), -1.0, &mut state, settings @@ -138,7 +142,7 @@ mod tests { state.needs_redraw = false; assert!(try_apply_pressure_to_state_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), 2.0, &mut state, settings @@ -159,7 +163,7 @@ mod tests { }; assert!(try_apply_pressure_to_state_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), 1.0, &mut state, settings @@ -183,14 +187,14 @@ mod tests { state.set_tool_override(Some(Tool::Pen)); state.on_mouse_press(MouseButton::Left, 0, 0); assert!(try_apply_pressure_to_state_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), 0.0, &mut state, settings )); state.on_mouse_motion(10, 0); assert!(try_apply_pressure_to_state_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), 1.0, &mut state, settings @@ -220,7 +224,7 @@ mod tests { assert!(state.set_thickness(31.0)); assert!(!try_apply_pressure_to_state_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), 0.05, &mut state, settings @@ -251,7 +255,7 @@ mod tests { assert!(state.set_thickness(30.0)); assert!(!try_apply_pressure_to_state_with( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), 0.05, &mut state, settings @@ -269,7 +273,7 @@ mod tests { #[test] fn first_pressure_sample_replaces_unpressured_stroke_samples() { - let test_text_measurer = crate::draw::TextMeasurer::default(); + let test_text_measurer = TextMeasurer::default(); let mut state = make_state(); diff --git a/src/input/tool/drawing.rs b/src/input/tool/drawing.rs index 6e551110e..7e8b4df3f 100644 --- a/src/input/tool/drawing.rs +++ b/src/input/tool/drawing.rs @@ -1,9 +1,11 @@ +use crate::domain::BoardGrid; +use crate::draw; use crate::draw::shape::{ bounding_box_for_blur, bounding_box_for_eraser, bounding_box_for_points, smooth_committed_path, }; use crate::draw::{ ArrowLabel, ArrowStyle, BlurRectParams, BlurStyle, Color, EraserBrush, EraserKind, LaserStyle, - Shape, TextMeasurer, + Shape, StepMarkerLabel, TextMeasurer, shape, }; use crate::input::tool::{ EraserMode, Tool, ToolDrawingBehavior, ToolPathKind, ToolPressureBehavior, @@ -43,7 +45,7 @@ pub(crate) struct ToolStrokeSnapshot { pub(crate) point_thicknesses: Vec, pub(crate) color: Color, pub(crate) size: f64, - pub(crate) grid: crate::domain::BoardGrid, + pub(crate) grid: BoardGrid, pub(crate) shape_recognition_sensitivity: u8, pub(crate) marker_opacity: f64, pub(crate) fill_enabled: bool, @@ -54,7 +56,7 @@ pub(crate) struct ToolStrokeSnapshot { pub(crate) arrow_head_at_end: bool, pub(crate) arrow_style: ArrowStyle, pub(crate) arrow_label: Option, - pub(crate) step_marker_label: crate::draw::StepMarkerLabel, + pub(crate) step_marker_label: StepMarkerLabel, pub(crate) eraser_mode: EraserMode, pub(crate) eraser_size: f64, pub(crate) eraser_kind: EraserKind, @@ -103,15 +105,15 @@ pub(crate) struct ToolUsage { } /// Borrowed inputs needed to classify and render the current live preview. -pub(crate) struct ProvisionalToolSnapshot<'a> { +pub(crate) struct ProvisionalToolSnapshot<'stroke> { pub(crate) tool: Tool, pub(crate) start: (i32, i32), pub(crate) current: (i32, i32), - pub(crate) points: &'a [(f64, f64)], - pub(crate) point_thicknesses: &'a [f32], + pub(crate) points: &'stroke [(f64, f64)], + pub(crate) point_thicknesses: &'stroke [f32], pub(crate) color: Color, pub(crate) size: f64, - pub(crate) grid: crate::domain::BoardGrid, + pub(crate) grid: BoardGrid, pub(crate) shape_recognition_sensitivity: u8, pub(crate) eraser_size: f64, pub(crate) marker_opacity: f64, @@ -123,9 +125,9 @@ pub(crate) struct ProvisionalToolSnapshot<'a> { pub(crate) arrow_head_at_end: bool, pub(crate) arrow_style: ArrowStyle, pub(crate) arrow_label: Option, - pub(crate) step_marker_label: Option, + pub(crate) step_marker_label: Option, /// Shape Pen's recognition of this stroke so far. - pub(crate) live_shape_memo: &'a super::LiveShapeMemo, + pub(crate) live_shape_memo: &'stroke super::LiveShapeMemo, /// Color and width of laser ink, which no tool slot stores. pub(crate) laser_style: LaserStyle, } @@ -141,24 +143,24 @@ pub(crate) struct PolygonProvisionalSnapshot { pub(crate) regular_sides: u8, } -pub(crate) enum ProvisionalToolStroke<'a> { +pub(crate) enum ProvisionalToolStroke<'stroke> { BorrowedFreehand { - points: &'a [(f64, f64)], + points: &'stroke [(f64, f64)], color: Color, size: f64, }, BorrowedPressureFreehand { - points: &'a [(f64, f64)], - point_thicknesses: &'a [f32], + points: &'stroke [(f64, f64)], + point_thicknesses: &'stroke [f32], color: Color, }, BorrowedMarker { - points: &'a [(f64, f64)], + points: &'stroke [(f64, f64)], color: Color, size: f64, }, EraserPreview { - points: &'a [(f64, f64)], + points: &'stroke [(f64, f64)], size: f64, }, Shape(Shape), @@ -166,14 +168,14 @@ pub(crate) enum ProvisionalToolStroke<'a> { /// it came from, so the swap from ink to shape is never silent. Recognized { shape: Shape, - ink: &'a [(f64, f64)], + ink: &'stroke [(f64, f64)], ink_color: Color, ink_size: f64, }, BlurReplayPreview(BlurRectParams), /// The laser stroke under the pointer, drawn exactly as its finished ink. Laser { - points: &'a [(f64, f64)], + points: &'stroke [(f64, f64)], style: LaserStyle, }, None, @@ -181,7 +183,10 @@ pub(crate) enum ProvisionalToolStroke<'a> { impl Tool { pub(crate) fn finish_stroke(self, snapshot: ToolStrokeSnapshot) -> FinishedToolStroke { - debug_assert_eq!(self, snapshot.tool); + debug_assert_eq!( + self, snapshot.tool, + "finish a stroke with the tool that captured its snapshot" + ); let usage = ToolUsage::default(); match self.drawing_behavior() { ToolDrawingBehavior::None => FinishedToolStroke::Noop, @@ -189,7 +194,7 @@ impl Tool { finish_path_stroke(snapshot, kind, pressure, usage) } ToolDrawingBehavior::LiveShape => { - let rounded = crate::draw::shape::quantize_path(&snapshot.points); + let rounded = shape::quantize_path(&snapshot.points); let recognized = if rounded.last().copied() == Some(snapshot.end) { super::live_shape::recognize( &rounded, @@ -316,7 +321,7 @@ impl Tool { cy, rx, ry, - magnification: crate::draw::normalize_spotlight_magnification( + magnification: draw::normalize_spotlight_magnification( snapshot.spotlight_magnification, ), } @@ -333,9 +338,9 @@ impl Tool { label: snapshot.step_marker_label, }) } - ToolDrawingBehavior::Eraser => finish_eraser(snapshot), + ToolDrawingBehavior::Eraser => finish_eraser(&snapshot), ToolDrawingBehavior::Laser => { - let mut points = crate::draw::shape::quantize_path(&snapshot.points); + let mut points = shape::quantize_path(&snapshot.points); if points.last().copied() != Some(snapshot.end) { points.push(snapshot.end); } @@ -348,7 +353,10 @@ impl Tool { self, snapshot: ProvisionalToolSnapshot<'a>, ) -> ProvisionalToolStroke<'a> { - debug_assert_eq!(self, snapshot.tool); + debug_assert_eq!( + self, snapshot.tool, + "preview a stroke with the tool that captured its snapshot" + ); match self.drawing_behavior() { ToolDrawingBehavior::None => ProvisionalToolStroke::None, ToolDrawingBehavior::LiveShape => { @@ -508,7 +516,7 @@ impl Tool { cy, rx, ry, - magnification: crate::draw::normalize_spotlight_magnification( + magnification: draw::normalize_spotlight_magnification( snapshot.spotlight_magnification, ), }) @@ -534,7 +542,7 @@ impl Tool { } } -impl<'a> ProvisionalToolStroke<'a> { +impl<'stroke> ProvisionalToolStroke<'stroke> { pub(crate) fn bounds_with(&self, measurer: &TextMeasurer) -> Option { match self { Self::BorrowedFreehand { points, size, .. } => bounding_box_for_points(points, *size), @@ -637,9 +645,9 @@ fn finish_path_stroke( } } -fn finish_eraser(snapshot: ToolStrokeSnapshot) -> FinishedToolStroke { +fn finish_eraser(snapshot: &ToolStrokeSnapshot) -> FinishedToolStroke { if snapshot.eraser_mode == EraserMode::Stroke { - let mut path = crate::draw::shape::quantize_path(&snapshot.points); + let mut path = shape::quantize_path(&snapshot.points); if path.last().copied() != Some(snapshot.end) { path.push(snapshot.end); } @@ -648,7 +656,7 @@ fn finish_eraser(snapshot: ToolStrokeSnapshot) -> FinishedToolStroke { FinishedToolStroke::Shape { shape: Shape::EraserStroke { - points: crate::draw::shape::quantize_path(&snapshot.points), + points: shape::quantize_path(&snapshot.points), brush: EraserBrush { size: snapshot.eraser_size, kind: snapshot.eraser_kind, diff --git a/src/input/tool/drawing/polygon.rs b/src/input/tool/drawing/polygon.rs index 615f0a499..dd9cf47f4 100644 --- a/src/input/tool/drawing/polygon.rs +++ b/src/input/tool/drawing/polygon.rs @@ -17,9 +17,12 @@ impl Tool { pub(crate) fn finish_polygon_stroke( self, - snapshot: PolygonStrokeSnapshot, + snapshot: &PolygonStrokeSnapshot, ) -> FinishedToolStroke { - debug_assert_eq!(self, snapshot.tool); + debug_assert_eq!( + self, snapshot.tool, + "finish a polygon stroke with the tool that captured its snapshot" + ); let Some(template) = self.polygon_template() else { debug_assert!(false, "non-polygon tool cannot finish a polygon stroke"); return FinishedToolStroke::Noop; @@ -29,9 +32,12 @@ impl Tool { pub(crate) fn provisional_polygon_stroke( self, - snapshot: PolygonProvisionalSnapshot, + snapshot: &PolygonProvisionalSnapshot, ) -> ProvisionalToolStroke<'static> { - debug_assert_eq!(self, snapshot.tool); + debug_assert_eq!( + self, snapshot.tool, + "preview a polygon stroke with the tool that captured its snapshot" + ); let Some(template) = self.polygon_template() else { debug_assert!(false, "non-polygon tool cannot preview a polygon stroke"); return ProvisionalToolStroke::None; @@ -41,7 +47,7 @@ impl Tool { } fn finish_polygon( - snapshot: PolygonStrokeSnapshot, + snapshot: &PolygonStrokeSnapshot, usage: ToolUsage, template: PolygonTemplate, ) -> FinishedToolStroke { @@ -69,7 +75,7 @@ fn finish_polygon( } fn provisional_polygon( - snapshot: PolygonProvisionalSnapshot, + snapshot: &PolygonProvisionalSnapshot, template: PolygonTemplate, ) -> ProvisionalToolStroke<'static> { let points = generated_points( diff --git a/src/input/tool/live_shape.rs b/src/input/tool/live_shape.rs index 91584c18d..697300a94 100644 --- a/src/input/tool/live_shape.rs +++ b/src/input/tool/live_shape.rs @@ -1,9 +1,13 @@ //! Recognize lines and closed shapes from a pen path. Ambiguous ink stays ink. +#[cfg(test)] +use std::cell::Cell; use std::cell::RefCell; +use std::f64::consts; +use crate::config; use crate::domain::{BoardGrid, BoardGridKind}; -use crate::draw::{Color, Shape}; +use crate::draw::{Color, Shape, shape}; mod ellipse; mod grid; @@ -21,7 +25,7 @@ mod triangle; pub(crate) struct LiveShapeMemo { last: RefCell)>>, #[cfg(test)] - runs: std::cell::Cell, + runs: Cell, } #[derive(Debug, Clone, Copy, PartialEq)] @@ -70,7 +74,7 @@ impl LiveShapeMemo { self.runs.set(self.runs.get() + 1); // Quantize only when recognition runs. Preview, bounds and readout // reuse the cached result without allocating another path buffer. - let rounded = crate::draw::shape::quantize_path(points); + let rounded = shape::quantize_path(points); let shape = recognize(&rounded, color, thick, fill, grid, sensitivity); *self.last.borrow_mut() = Some((key, shape.clone())); shape @@ -99,7 +103,7 @@ pub(super) fn recognize( .windows(2) .map(|pair| distance(pair[0], pair[1])) .sum(); - let sensitivity = sensitivity.min(crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY); + let sensitivity = sensitivity.min(config::MAX_SHAPE_RECOGNITION_SENSITIVITY); if points.len() >= 8 && bounds.width >= 24.0 @@ -240,7 +244,7 @@ fn fit_closed_lap( // Winding around the box center can cross a triangle's edge, so it fits // its own sides. Every candidate competes on the same completed lap. let (ellipse, rectangle) = if winding.is_some_and(|coverage| { - (0.8..=1.2).contains(&(coverage.winding.abs() / std::f64::consts::TAU)) + (0.8..=1.2).contains(&(coverage.winding.abs() / consts::TAU)) && coverage.travel <= coverage.winding.abs() * 1.5 }) { ( @@ -393,7 +397,7 @@ fn distance(a: (i32, i32), b: (i32, i32)) -> f64 { } fn resampled_length(points: &[(i32, i32)]) -> f64 { - let Some(&first) = points.first() else { + let (Some(&first), Some(&last)) = (points.first(), points.last()) else { return 0.0; }; let mut anchor = first; @@ -408,5 +412,5 @@ fn resampled_length(points: &[(i32, i32)]) -> f64 { } } - length + distance(anchor, *points.last().unwrap()) + length + distance(anchor, last) } diff --git a/src/input/tool/live_shape/ellipse.rs b/src/input/tool/live_shape/ellipse.rs index f0175aee1..8ca39c6e1 100644 --- a/src/input/tool/live_shape/ellipse.rs +++ b/src/input/tool/live_shape/ellipse.rs @@ -1,5 +1,7 @@ //! Oval fitting and shared polar geometry for angular coverage. +use std::f64::consts; + use super::{Bounds, ClosedFit}; use crate::draw::{Color, Shape}; @@ -20,13 +22,13 @@ pub(super) fn angular_travel(points: &[(i32, i32)], bounds: Bounds) -> Option std::f64::consts::FRAC_PI_2 + 0.1 { + if delta.abs() > consts::FRAC_PI_2 + 0.1 { return None; } winding += delta; travel += delta.abs(); // Allow only floating-point summation error at the lap boundary. - if first_lap_end.is_none() && winding.abs() >= std::f64::consts::TAU - 1e-9 { + if first_lap_end.is_none() && winding.abs() >= consts::TAU - 1e-9 { first_lap_end = Some(index); } } @@ -55,8 +57,7 @@ pub(super) fn fit_ellipse( return None; } - let circumference = - std::f64::consts::PI * (3.0 * (rx + ry) - ((3.0 * rx + ry) * (rx + 3.0 * ry)).sqrt()); + let circumference = consts::PI * (3.0 * (rx + ry) - ((3.0 * rx + ry) * (rx + 3.0 * ry)).sqrt()); if !(circumference * (0.74 - 0.03 * level)..=circumference * (1.3 + 0.05 * level)) .contains(&length) { @@ -137,6 +138,5 @@ fn normalized_point(point: (i32, i32), bounds: Bounds) -> (f64, f64) { } pub(super) fn angle_delta(previous: f64, angle: f64) -> f64 { - (angle - previous + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - - std::f64::consts::PI + (angle - previous + consts::PI).rem_euclid(consts::TAU) - consts::PI } diff --git a/src/input/tool/live_shape/overlap.rs b/src/input/tool/live_shape/overlap.rs index c1660b4ad..81aa7975c 100644 --- a/src/input/tool/live_shape/overlap.rs +++ b/src/input/tool/live_shape/overlap.rs @@ -1,5 +1,7 @@ //! Compare an oval's overlapping tail with the first lap at the same angle. +use std::f64::consts; + use super::ellipse::{angle_delta, polar}; use super::{Bounds, ellipse}; @@ -12,7 +14,7 @@ pub(super) fn follows_first_lap(points: &[(i32, i32)], lap_end: usize, bounds: B let Some(coverage) = ellipse::angular_travel(points, bounds) else { return false; }; - if coverage.winding.abs() > std::f64::consts::TAU * (MAX_OVAL_TURNS + WINDING_MARGIN_TURNS) + if coverage.winding.abs() > consts::TAU * (MAX_OVAL_TURNS + WINDING_MARGIN_TURNS) || coverage.travel > coverage.winding.abs() * 1.5 { return false; @@ -33,8 +35,8 @@ pub(super) fn follows_first_lap(points: &[(i32, i32)], lap_end: usize, bounds: B } previous_angle = angle; } - if last_position < std::f64::consts::TAU { - profile.push((std::f64::consts::TAU, start_radius)); + if last_position < consts::TAU { + profile.push((consts::TAU, start_radius)); } // Keep the overlap tolerance independent of the one-lap fit sensitivity. @@ -43,7 +45,7 @@ pub(super) fn follows_first_lap(points: &[(i32, i32)], lap_end: usize, bounds: B let margin = (bounds.width.min(bounds.height) / 2.0 * OVERLAP_RADIUS_TOLERANCE).max(2.0); for &point in &points[lap_end + 1..] { let (angle, radius) = polar(point, bounds); - let progress = ((angle - start_angle) * direction).rem_euclid(std::f64::consts::TAU); + let progress = ((angle - start_angle) * direction).rem_euclid(consts::TAU); let upper = profile.partition_point(|&(position, _)| position < progress); let expected = if upper == 0 { start_radius diff --git a/src/input/tool/live_shape/tests.rs b/src/input/tool/live_shape/tests.rs index ff95e53f7..f391077f8 100644 --- a/src/input/tool/live_shape/tests.rs +++ b/src/input/tool/live_shape/tests.rs @@ -1,3 +1,6 @@ +use std::f64::consts; + +use crate::config; use crate::domain::{BoardGrid, BoardGridKind}; use crate::draw::{BLACK, PolygonKind, Shape}; @@ -55,7 +58,7 @@ fn trace(corners: &[(f64, f64)], start: f64, step: f64, wobble: f64) -> Vec<(i32 fn regular(sides: usize, radius: (f64, f64), turn: f64) -> Vec<(f64, f64)> { (0..sides) .map(|index| { - let angle = turn + std::f64::consts::TAU * index as f64 / sides as f64; + let angle = turn + consts::TAU * index as f64 / sides as f64; ( 200.0 + radius.0 * angle.cos(), 200.0 + radius.1 * angle.sin(), @@ -69,7 +72,7 @@ fn open_arcs_spirals_repeated_loops_and_departing_tails_stay_ink() { let arc = |turns: f64, growth: f64| -> Vec<(i32, i32)> { (0..=(turns * 96.0).round() as usize) .map(|step| { - let angle = std::f64::consts::TAU * step as f64 / 96.0; + let angle = consts::TAU * step as f64 / 96.0; let radius = 60.0 + growth * step as f64 / 96.0; ( (200.0 + radius * angle.cos()).round() as i32, @@ -90,7 +93,7 @@ fn open_arcs_spirals_repeated_loops_and_departing_tails_stay_ink() { ("retraced loop", retraced), ("departing tail", tail), ] { - for level in 0..=crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY { + for level in 0..=config::MAX_SHAPE_RECOGNITION_SENSITIVITY { assert!(recognize(&path, level).is_none(), "{name} at level {level}"); } } @@ -147,7 +150,7 @@ fn overlap_allows_small_radius_drift_but_rejects_spirals() { let path: Vec<_> = (0..=(turns * 360.0) as usize) .map(|step| { let fraction = step as f64 / 360.0; - let angle = std::f64::consts::TAU * fraction; + let angle = consts::TAU * fraction; let radius = 60.0 + growth * fraction / turns; ( (200.0 + radius * angle.cos()).round() as i32, @@ -155,7 +158,7 @@ fn overlap_allows_small_radius_drift_but_rejects_spirals() { ) }) .collect(); - for level in 0..=crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY { + for level in 0..=config::MAX_SHAPE_RECOGNITION_SENSITIVITY { let shape = recognize(&path, level); if expected_ellipse { assert!( @@ -179,7 +182,7 @@ fn overlap_rejects_inward_curls() { let path: Vec<_> = (0..=468) .map(|step| { let fraction = step as f64 / 360.0; - let angle = std::f64::consts::TAU * fraction; + let angle = consts::TAU * fraction; let radius = 60.0 * (1.0 - inward * ((fraction - 1.1) / 0.2).clamp(0.0, 1.0)); ( (200.0 + radius * angle.cos()).round() as i32, @@ -187,7 +190,7 @@ fn overlap_rejects_inward_curls() { ) }) .collect(); - for level in 0..=crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY { + for level in 0..=config::MAX_SHAPE_RECOGNITION_SENSITIVITY { assert!( recognize(&path, level).is_none(), "inward curl {inward}, level {level}" @@ -204,7 +207,7 @@ fn circle_with_offset_overlap( ) -> Vec<(i32, i32)> { (0..=(360.0 * (1.0 + extra)).round() as usize) .map(|step| { - let angle = direction * std::f64::consts::TAU * step as f64 / 360.0; + let angle = direction * consts::TAU * step as f64 / 360.0; // The second pass differs from the first, even at the same angle. let radius = radius + if step > 360 { offset } else { 0.0 }; ( @@ -227,7 +230,7 @@ fn short_offset_overshoots_preserve_closed_circle_recognition() { for direction in [-1.0, 1.0] { for (extra, min_level) in [(0.03, 0), (0.08, 3)] { let path = circle_with_offset_overlap(radius, extra, offset, direction); - for level in min_level..=crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY { + for level in min_level..=config::MAX_SHAPE_RECOGNITION_SENSITIVITY { assert!( matches!(recognize(&path, level), Some(Shape::Ellipse { .. })), "radius {radius}, offset {offset}, extra {extra}, direction {direction}, level {level}" @@ -253,7 +256,7 @@ fn long_offset_overlaps_allow_large_circle_retracing_but_reject_small_circle_dri ] { for direction in [-1.0, 1.0] { let path = circle_with_offset_overlap(radius, 0.3, offset, direction); - for level in 0..=crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY { + for level in 0..=config::MAX_SHAPE_RECOGNITION_SENSITIVITY { let shape = recognize(&path, level); if expected_ellipse { assert!( @@ -375,7 +378,7 @@ fn pixel_staircases_on_dense_diagonals_do_not_inflate_the_outline() { fn round_and_four_sided_strokes_never_become_triangles() { let lobed: Vec<_> = (0..64) .map(|index| { - let angle = std::f64::consts::TAU * f64::from(index) / 64.0; + let angle = consts::TAU * f64::from(index) / 64.0; let wobble = 1.0 + 0.13 * (3.0 * angle).sin(); ( 200.0 + 50.0 * wobble * angle.cos(), @@ -406,17 +409,14 @@ fn round_and_four_sided_strokes_never_become_triangles() { (140.0, 170.0), ], ), - ( - "pentagon", - regular(5, (70.0, 70.0), -std::f64::consts::FRAC_PI_2), - ), + ("pentagon", regular(5, (70.0, 70.0), -consts::FRAC_PI_2)), ("hexagon", regular(6, (70.0, 70.0), 0.0)), ]; for (name, outline) in outlines { for wobble in [0.0, 2.0] { let path = trace(&outline, 0.1, 3.0, wobble); - for sensitivity in 0..=crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY { + for sensitivity in 0..=config::MAX_SHAPE_RECOGNITION_SENSITIVITY { assert_eq!( triangle_points(recognize(&path, sensitivity)), None, @@ -504,7 +504,7 @@ const QUICK_RECTANGLES: [&[(f64, f64)]; 3] = [ #[test] fn quick_rectangles_with_leaning_sides_are_rectangles_by_default() { - let default = crate::config::DEFAULT_SHAPE_RECOGNITION_SENSITIVITY; + let default = config::DEFAULT_SHAPE_RECOGNITION_SENSITIVITY; for corners in QUICK_RECTANGLES { let path = trace(corners, 0.05, 2.0, 1.0); @@ -535,7 +535,7 @@ fn leaning_quadrilaterals_need_more_sensitivity_the_more_they_lean() { ]; let path = trace(&trapezoid, 0.1, 3.0, 0.0); - let kinds: Vec<_> = (0..=crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY) + let kinds: Vec<_> = (0..=config::MAX_SHAPE_RECOGNITION_SENSITIVITY) .map(|level| recognize(&path, level).map(|shape| shape.kind_name())) .collect(); @@ -565,7 +565,7 @@ fn diamonds_kites_and_round_strokes_never_become_rectangles() { for (name, outline) in outlines { for wobble in [0.0, 2.0] { let path = trace(&outline, 0.1, 3.0, wobble); - for level in 0..=crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY { + for level in 0..=config::MAX_SHAPE_RECOGNITION_SENSITIVITY { assert!( !matches!(recognize(&path, level), Some(Shape::Rect { .. })), "{name} with wobble {wobble} at level {level}" diff --git a/src/input/tool/presets.rs b/src/input/tool/presets.rs index f1bd34eff..2f444669b 100644 --- a/src/input/tool/presets.rs +++ b/src/input/tool/presets.rs @@ -5,6 +5,7 @@ use crate::config::{ColorSpec, PresetToolSettingConfig, PresetToolStatesConfig}; use crate::domain::Tool; impl PresetToolSettingConfig { + #[must_use] pub fn from_runtime(settings: ToolDrawingSettings) -> Self { Self { color: settings.color.into(), @@ -12,12 +13,14 @@ impl PresetToolSettingConfig { } } + #[must_use] pub fn to_runtime(&self) -> ToolDrawingSettings { ToolDrawingSettings::new(self.color.to_color(), self.size) } } impl PresetToolStatesConfig { + #[must_use] pub fn from_runtime(settings: &PerToolDrawingSettings, eraser_size: f64) -> Self { Self { pen: PresetToolSettingConfig::from_runtime(settings.pen), @@ -32,6 +35,7 @@ impl PresetToolStatesConfig { } } + #[must_use] pub fn to_runtime(&self) -> PerToolDrawingSettings { PerToolDrawingSettings { pen: self.pen.to_runtime(), @@ -45,10 +49,12 @@ impl PresetToolStatesConfig { } } + #[must_use] pub fn color_spec_for_tool(&self, tool: Tool) -> ColorSpec { self.setting_for_slot(tool.settings_slot()).color.clone() } + #[must_use] pub fn size_for_tool(&self, tool: Tool) -> f64 { let profile = tool.profile(); match profile.size_source { diff --git a/src/input/tool/settings.rs b/src/input/tool/settings.rs index 81b4744f6..18fa96752 100644 --- a/src/input/tool/settings.rs +++ b/src/input/tool/settings.rs @@ -11,6 +11,7 @@ pub struct ToolDrawingSettings { } impl ToolDrawingSettings { + #[must_use] pub fn new(color: Color, thickness: f64) -> Self { Self { color, thickness } } @@ -30,6 +31,7 @@ pub struct PerToolDrawingSettings { } impl PerToolDrawingSettings { + #[must_use] pub fn new(color: Color, thickness: f64) -> Self { let settings = ToolDrawingSettings::new(color, thickness); Self { @@ -44,19 +46,21 @@ impl PerToolDrawingSettings { } } + #[must_use] pub fn settings_tool(tool: Tool) -> Tool { tool.settings_tool() } + #[must_use] pub fn get(&self, tool: Tool) -> &ToolDrawingSettings { - self.get_slot(tool.settings_slot()) + self.slot(tool.settings_slot()) } pub fn get_mut(&mut self, tool: Tool) -> &mut ToolDrawingSettings { - self.get_slot_mut(tool.settings_slot()) + self.slot_mut(tool.settings_slot()) } - pub(crate) fn get_slot(&self, slot: ToolSettingsSlot) -> &ToolDrawingSettings { + pub(crate) fn slot(&self, slot: ToolSettingsSlot) -> &ToolDrawingSettings { match slot { ToolSettingsSlot::Pen => &self.pen, ToolSettingsSlot::Line => &self.line, @@ -69,7 +73,7 @@ impl PerToolDrawingSettings { } } - pub(crate) fn get_slot_mut(&mut self, slot: ToolSettingsSlot) -> &mut ToolDrawingSettings { + pub(crate) fn slot_mut(&mut self, slot: ToolSettingsSlot) -> &mut ToolDrawingSettings { match slot { ToolSettingsSlot::Pen => &mut self.pen, ToolSettingsSlot::Line => &mut self.line, @@ -82,9 +86,10 @@ impl PerToolDrawingSettings { } } + #[must_use] pub fn clamp_thicknesses(mut self, min: f64, max: f64) -> Self { for slot in ToolSettingsSlot::ALL { - let settings = self.get_slot_mut(slot); + let settings = self.slot_mut(slot); settings.thickness = settings.thickness.clamp(min, max); } self diff --git a/src/input/tool/tests.rs b/src/input/tool/tests.rs index 592eff23e..82947363f 100644 --- a/src/input/tool/tests.rs +++ b/src/input/tool/tests.rs @@ -1,3 +1,5 @@ +use crate::draw::TextMeasurer; + use super::drawing::marker_color_with_opacity; use super::*; use crate::config::Action; @@ -181,8 +183,8 @@ fn marker_opacity_helper_preserves_current_alpha_clamp() { #[test] fn provisional_polygon_bounds_include_extra_preview_padding() { - let measurer = crate::draw::TextMeasurer::default(); - let stroke = Tool::Triangle.provisional_polygon_stroke(PolygonProvisionalSnapshot { + let measurer = TextMeasurer::default(); + let stroke = Tool::Triangle.provisional_polygon_stroke(&PolygonProvisionalSnapshot { tool: Tool::Triangle, start: (10, 10), current: (60, 50), diff --git a/src/launch_environment.rs b/src/launch_environment.rs index 36f1a6b6f..addbd8be6 100644 --- a/src/launch_environment.rs +++ b/src/launch_environment.rs @@ -12,6 +12,7 @@ use std::ffi::OsString; use std::os::unix::ffi::OsStringExt; use std::path::Path; +use std::{env, fs}; use crate::env_vars::{DESKTOP_STARTUP_ID_ENV, XDG_ACTIVATION_TOKEN_ENV}; @@ -30,11 +31,11 @@ pub(crate) fn var_os(name: &str) -> Option { } fn var_os_in(launch_environment: &Path, name: &str) -> Option { - match std::fs::read(launch_environment) { + match fs::read(launch_environment) { Ok(environment) => { value_in(&environment, name).map(|value| OsString::from_vec(value.to_vec())) } - Err(_) => std::env::var_os(name), + Err(_) => env::var_os(name), } } diff --git a/src/launch_environment/tests.rs b/src/launch_environment/tests.rs index eb5182144..19ec1b590 100644 --- a/src/launch_environment/tests.rs +++ b/src/launch_environment/tests.rs @@ -1,3 +1,7 @@ +use std::process::Command; + +use crate::test_env; + use super::*; use std::ffi::OsStr; @@ -65,11 +69,11 @@ fn the_startup_token_is_trimmed_and_falls_back_to_the_startup_id() { /// features or the system's GTK. #[test] fn a_launch_token_survives_its_removal_from_the_live_environment() { - if std::env::var_os(CHILD_ENV).is_some() { + if env::var_os(CHILD_ENV).is_some() { let removed = STARTUP_NOTIFICATION_VARIABLES.map(|name| (name, None::<&OsStr>)); - crate::test_env::with_env_vars(&removed, || { + test_env::with_env_vars(&removed, || { for name in STARTUP_NOTIFICATION_VARIABLES { - assert_eq!(std::env::var_os(name), None, "{name} is still live"); + assert_eq!(env::var_os(name), None, "{name} is still live"); } assert_eq!(startup_activation_token().as_deref(), Some(LAUNCH_TOKEN)); assert_eq!( @@ -86,7 +90,7 @@ fn a_launch_token_survives_its_removal_from_the_live_environment() { ) .strip_prefix(concat!(env!("CARGO_CRATE_NAME"), "::")) .expect("test path contains the crate prefix"); - let output = std::process::Command::new(std::env::current_exe().expect("test binary")) + let output = Command::new(env::current_exe().expect("test binary")) .args([test_name, "--exact", "--test-threads=1"]) .env(CHILD_ENV, "1") .env(XDG_ACTIVATION_TOKEN_ENV, LAUNCH_TOKEN) @@ -107,7 +111,7 @@ fn a_launch_token_survives_its_removal_from_the_live_environment() { fn without_a_launch_environment_the_live_one_is_read() { const NAME: &str = "WAYSCRIBER_TEST_LAUNCH_ENVIRONMENT_FALLBACK"; - crate::test_env::with_env_var(NAME, Some(OsStr::new("live")), || { + test_env::with_env_var(NAME, Some(OsStr::new("live")), || { assert_eq!( var_os_in(Path::new("/nonexistent/environ"), NAME).as_deref(), Some(OsStr::new("live")) diff --git a/src/lib.rs b/src/lib.rs index 7a1af38ce..709a3ec18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,10 +77,15 @@ use cli::CliOutcome; static RUN_ENTRY_LEASE: Mutex<()> = Mutex::new(()); /// Run wayscriber using the current process arguments and return its process exit status. +/// +/// # Panics +/// +/// Panics if an earlier call in this process panicked while it ran, which +/// leaves the run entry lease poisoned. pub fn run_from_env() -> ExitCode { let _run_entry = RUN_ENTRY_LEASE .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("an earlier run panicked while holding the run entry lease"); if let Some(exit_code) = process_broker::run_internal_broker_if_requested() { return exit_code; } diff --git a/src/logger.rs b/src/logger.rs index 7e3fff6ba..e36ad17c1 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -2,7 +2,7 @@ mod file; mod filter; mod panic; -use std::cell::Cell; +use std::cell::{Cell, RefCell}; use std::fmt::Write as _; use std::io::{self, Write as IoWrite}; use std::sync::Mutex; @@ -17,8 +17,11 @@ const INITIAL_RECORD_CAPACITY: usize = 256; const MAX_RETAINED_RECORD_CAPACITY: usize = 64 * 1024; thread_local! { - /// Whether this thread is inside [`SimpleLogger::log`], holding its lock. + /// Whether this thread is inside [`SimpleLogger::log`]. static WRITING_RECORD: Cell = const { Cell::new(false) }; + + /// This thread's reusable line buffer for [`SimpleLogger::log`]. + static RECORD_BUFFER: RefCell = RefCell::new(RecordBuffer::new()); } pub(crate) fn init(log_to_file: bool) { @@ -34,10 +37,7 @@ pub(crate) fn init(log_to_file: bool) { let logger = SimpleLogger { filter, - state: Mutex::new(LoggerState { - sink, - record_buffer: String::with_capacity(INITIAL_RECORD_CAPACITY), - }), + sink: Mutex::new(sink), include_timestamp: log_to_file, }; @@ -49,41 +49,44 @@ pub(crate) fn init(log_to_file: bool) { struct SimpleLogger { filter: LogFilter, - state: Mutex, + sink: Mutex, include_timestamp: bool, } -struct LoggerState { - sink: LogSink, - record_buffer: String, -} +/// One formatted log line, reused for the next record on the same thread. +struct RecordBuffer(String); -impl LoggerState { - fn write_record(&mut self, record: &Record<'_>, include_timestamp: bool) { - self.record_buffer.clear(); +impl RecordBuffer { + fn new() -> Self { + Self(String::with_capacity(INITIAL_RECORD_CAPACITY)) + } + + /// Formats `record` as one physical line, replacing the previous one. + fn format(&mut self, record: &Record<'_>, include_timestamp: bool) -> &[u8] { + let line = &mut self.0; + line.clear(); if include_timestamp { let _ = write!( - self.record_buffer, + line, "{} {} {}: ", timestamp_millis(), record.level(), record.target() ); } else { - let _ = write!( - self.record_buffer, - "{} {}: ", - record.level(), - record.target() - ); + let _ = write!(line, "{} {}: ", record.level(), record.target()); } - let _ = write!(self.record_buffer, "{}", record.args()); - escape_record_line_breaks(&mut self.record_buffer); - self.record_buffer.push('\n'); + let _ = write!(line, "{}", record.args()); + escape_record_line_breaks(line); + line.push('\n'); + + line.as_bytes() + } - self.sink.write_record(self.record_buffer.as_bytes()); - if self.record_buffer.capacity() > MAX_RETAINED_RECORD_CAPACITY { - self.record_buffer = String::with_capacity(INITIAL_RECORD_CAPACITY); + /// Releases an allocation one oversized record grew, instead of keeping it. + fn shrink_oversized(&mut self) { + if self.0.capacity() > MAX_RETAINED_RECORD_CAPACITY { + self.0 = String::with_capacity(INITIAL_RECORD_CAPACITY); } } } @@ -122,17 +125,25 @@ impl Log for SimpleLogger { return; }; - let mut state = match self.state.lock() { - Ok(state) => state, - Err(poisoned) => poisoned.into_inner(), - }; + RECORD_BUFFER.with_borrow_mut(|buffer| { + // Formatting runs `Display` impls, which may panic. Doing it + // before taking the lock keeps such a panic from poisoning the + // sink, so only a sink that panicked mid-write is ever poisoned. + let line = buffer.format(record, self.include_timestamp); + + // A poisoned sink is in an unknown state, so the record is + // dropped instead of written through it. + if let Ok(mut sink) = self.sink.lock() { + sink.write_record(line); + } - state.write_record(record, self.include_timestamp); + buffer.shrink_oversized(); + }); } fn flush(&self) { - if let Ok(mut state) = self.state.lock() { - state.sink.flush(); + if let Ok(mut sink) = self.sink.lock() { + sink.flush(); } } } @@ -192,9 +203,15 @@ impl LogSink { #[cfg(test)] mod tests { + use std::fmt; + use std::fmt::{Display, Formatter}; + + use crate::test_env; + use super::*; use log::Level; use std::sync::{Arc, Mutex}; + use std::thread; #[derive(Clone, Default)] struct SharedWriter { @@ -333,15 +350,11 @@ mod tests { #[test] fn file_record_keeps_timestamp_and_message_in_one_line() { - let output = SharedWriter::default(); - let mut state = LoggerState { - sink: LogSink::single(Box::new(output.clone())), - record_buffer: String::with_capacity(INITIAL_RECORD_CAPACITY), - }; + let mut buffer = RecordBuffer::new(); - state.write_record(&warning_record(), true); + let line = buffer.format(&warning_record(), true); - let line = String::from_utf8(output.bytes()).expect("valid log output"); + let line = String::from_utf8(line.to_vec()).expect("valid log output"); let (timestamp, message) = line.split_once(' ').expect("timestamp separator"); assert!(timestamp.parse::().is_ok()); @@ -350,45 +363,36 @@ mod tests { #[test] fn multiline_record_is_escaped_to_one_physical_line() { - let output = SharedWriter::default(); - let mut state = LoggerState { - sink: LogSink::single(Box::new(output.clone())), - record_buffer: String::with_capacity(INITIAL_RECORD_CAPACITY), - }; + let mut buffer = RecordBuffer::new(); let record = Record::builder() .args(format_args!("first\r\nsecond\nthird\r")) .level(Level::Error) .target("wayscriber::ocr\nworker") .build(); - state.write_record(&record, true); + let line = buffer.format(&record, true); - let line = String::from_utf8(output.bytes()).expect("valid log output"); + let line = String::from_utf8(line.to_vec()).expect("valid log output"); assert_eq!(line.lines().count(), 1); assert!(line.ends_with("ERROR wayscriber::ocr\\nworker: first\\r\\nsecond\\nthird\\r\n")); } #[test] fn a_record_logged_while_the_thread_writes_one_is_dropped_instead_of_deadlocking() { - struct LogsWhileFormatting<'a>(&'a SimpleLogger); + struct LogsWhileFormatting<'logger>(&'logger SimpleLogger); - impl std::fmt::Display for LogsWhileFormatting<'_> { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + impl Display for LogsWhileFormatting<'_> { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { self.0.log(&warning_record()); formatter.write_str("outer record") } } let output = SharedWriter::default(); - let filter = crate::test_env::with_env_var("RUST_LOG", Some("warn".as_ref()), || { - LogFilter::from_env() - }); + let filter = test_env::with_env_var("RUST_LOG", Some("warn".as_ref()), LogFilter::from_env); let logger = SimpleLogger { filter, - state: Mutex::new(LoggerState { - sink: LogSink::single(Box::new(output.clone())), - record_buffer: String::with_capacity(INITIAL_RECORD_CAPACITY), - }), + sink: Mutex::new(LogSink::single(Box::new(output.clone()))), include_timestamp: false, }; @@ -413,14 +417,53 @@ mod tests { #[test] fn oversized_record_buffer_is_not_retained() { + let mut buffer = RecordBuffer(String::with_capacity(MAX_RETAINED_RECORD_CAPACITY + 1)); + + buffer.format(&warning_record(), false); + buffer.shrink_oversized(); + + assert!(buffer.0.capacity() <= MAX_RETAINED_RECORD_CAPACITY); + } + + #[test] + fn a_record_whose_formatting_panics_does_not_stop_later_records() { + struct PanicsWhileFormatting; + + impl Display for PanicsWhileFormatting { + fn fmt(&self, _formatter: &mut Formatter<'_>) -> fmt::Result { + panic!("formatting panic"); + } + } + let output = SharedWriter::default(); - let mut state = LoggerState { - sink: LogSink::single(Box::new(output)), - record_buffer: String::with_capacity(MAX_RETAINED_RECORD_CAPACITY + 1), + let filter = test_env::with_env_var("RUST_LOG", Some("warn".as_ref()), LogFilter::from_env); + let logger = SimpleLogger { + filter, + sink: Mutex::new(LogSink::single(Box::new(output.clone()))), + include_timestamp: false, }; - state.write_record(&warning_record(), false); + let panicked = thread::scope(|scope| { + scope + .spawn(|| { + logger.log( + &Record::builder() + .args(format_args!("{}", PanicsWhileFormatting)) + .level(Level::Warn) + .target("wayscriber::daemon") + .build(), + ); + }) + .join() + .is_err() + }); + logger.log(&warning_record()); - assert!(state.record_buffer.capacity() <= MAX_RETAINED_RECORD_CAPACITY); + assert!(panicked); + let output = String::from_utf8(output.bytes()).expect("valid log output"); + assert_eq!( + output.lines().collect::>(), + ["WARN wayscriber::daemon: sink failure record"] + ); } } diff --git a/src/logger/file.rs b/src/logger/file.rs index 21e3f336e..62eddc779 100644 --- a/src/logger/file.rs +++ b/src/logger/file.rs @@ -1,8 +1,8 @@ -use std::env; use std::ffi::OsString; -use std::fs; use std::io::{self, Write}; +use std::os::unix::fs::OpenOptionsExt; use std::path::{Path, PathBuf}; +use std::{env, fs}; use crate::env_vars::{LOG_FILE_ENV, LOG_MAX_SIZE_ENV}; use crate::{paths, time_utils}; @@ -176,7 +176,7 @@ impl DailyFileWriter { let mut open_options = fs::OpenOptions::new(); open_options.create(true).append(true); #[cfg(unix)] - std::os::unix::fs::OpenOptionsExt::mode(&mut open_options, 0o600); + OpenOptionsExt::mode(&mut open_options, 0o600); match open_options.open(&path) { Ok(file) => { self.file = Some(file); @@ -255,6 +255,11 @@ impl Write for DailyFileWriter { #[cfg(test)] mod tests { + use crate::test_temp; + + use std::fs; + use std::path::Path; + use super::LogFileTarget; use std::path::PathBuf; @@ -264,7 +269,7 @@ mod tests { use std::io::Write; use std::os::unix::fs::PermissionsExt; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let data = temp.path().join("wayscriber"); let logs = data.join("logs"); let mut writer = super::DailyFileWriter::new(LogFileTarget { @@ -276,9 +281,8 @@ mod tests { writer.write_all(b"session saved\n").unwrap(); writer.flush().unwrap(); - let mode = - |path: &std::path::Path| std::fs::metadata(path).unwrap().permissions().mode() & 0o777; - let log_files: Vec<_> = std::fs::read_dir(&logs) + let mode = |path: &Path| fs::metadata(path).unwrap().permissions().mode() & 0o777; + let log_files: Vec<_> = fs::read_dir(&logs) .unwrap() .map(|entry| entry.unwrap().path()) .collect(); diff --git a/src/logger/panic.rs b/src/logger/panic.rs index a01f60f94..2af5f01ba 100644 --- a/src/logger/panic.rs +++ b/src/logger/panic.rs @@ -3,6 +3,7 @@ use std::backtrace::Backtrace; use std::fmt::Display; use std::panic::{self, Location, PanicHookInfo}; +use std::thread; use log::{Level, Log, Record}; @@ -27,7 +28,7 @@ fn chain_hook(report: impl Fn(&PanicHookInfo<'_>) + Send + Sync + 'static) { } fn report_panic(logger: &dyn Log, info: &PanicHookInfo<'_>) { - let thread = std::thread::current(); + let thread = thread::current(); let report = panic_report( thread.name(), info.location(), @@ -62,6 +63,8 @@ fn panic_report( #[cfg(test)] mod tests { + use std::thread::Builder; + use super::*; use log::Metadata; use std::sync::{Arc, Mutex}; @@ -73,10 +76,7 @@ mod tests { impl CapturingLogger { fn records(&self) -> Vec<(Level, String)> { - self.records - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .clone() + self.records.lock().expect("captured log records").clone() } } @@ -88,7 +88,7 @@ mod tests { fn log(&self, record: &Record<'_>) { self.records .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .expect("captured log records") .push((record.level(), record.args().to_string())); } @@ -120,13 +120,13 @@ mod tests { panic::set_hook(Box::new(move |info| { replaced_payloads .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .expect("replaced hook payloads") .push(info.payload_as_str().unwrap_or_default().to_owned()); })); let hook_logger = logger.clone(); chain_hook(move |info| report_panic(&hook_logger, info)); - let result = std::thread::Builder::new() + let result = Builder::new() .name("panic-hook-probe".into()) .spawn(|| panic!("panic hook probe payload")) .unwrap() @@ -147,7 +147,7 @@ mod tests { assert!( replaced .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .expect("replaced hook payloads") .iter() .any(|payload| payload == "panic hook probe payload") ); diff --git a/src/main.rs b/src/main.rs index b7cb12ed4..353c01404 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ -fn main() -> std::process::ExitCode { +use std::process::ExitCode; + +fn main() -> ExitCode { wayscriber::run_from_env() } diff --git a/src/notification.rs b/src/notification.rs index 52bbef6b9..0c577afdc 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -11,8 +11,8 @@ pub enum NotificationError { Delivery(String), } -impl std::fmt::Display for NotificationError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for NotificationError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { match self { Self::Unavailable => formatter.write_str("desktop notifications are unavailable"), Self::Delivery(message) => formatter.write_str(message), @@ -20,12 +20,14 @@ impl std::fmt::Display for NotificationError { } } -impl std::error::Error for NotificationError {} +impl error::Error for NotificationError {} #[cfg(feature = "dbus")] mod real { use std::collections::HashMap; + use tokio::runtime::Handle; + use zbus::zvariant::Value; use zbus::{Connection, proxy}; use super::NotificationError; @@ -59,7 +61,7 @@ mod real { summary: &str, body: &str, actions: Vec<&str>, - hints: HashMap<&str, zbus::zvariant::Value<'_>>, + hints: HashMap<&str, Value<'_>>, expire_timeout: i32, ) -> zbus::Result; } @@ -109,7 +111,7 @@ mod real { } pub fn send_notification_async( - runtime_handle: &tokio::runtime::Handle, + runtime_handle: &Handle, summary: String, body: String, icon: Option, @@ -123,7 +125,7 @@ mod real { } pub fn send_notification_with_timeout_async( - runtime_handle: &tokio::runtime::Handle, + runtime_handle: &Handle, summary: String, body: String, icon: Option, @@ -142,6 +144,8 @@ mod real { #[cfg(not(feature = "dbus"))] mod real { + use tokio::runtime::Handle; + use super::NotificationError; #[cfg_attr(not(feature = "dbus"), allow(dead_code))] @@ -165,7 +169,7 @@ mod real { #[cfg_attr(not(feature = "dbus"), allow(dead_code))] pub fn send_notification_async( - _runtime_handle: &tokio::runtime::Handle, + _runtime_handle: &Handle, _summary: String, _body: String, _icon: Option, @@ -175,7 +179,7 @@ mod real { #[cfg_attr(not(feature = "dbus"), allow(dead_code))] pub fn send_notification_with_timeout_async( - _runtime_handle: &tokio::runtime::Handle, + _runtime_handle: &Handle, _summary: String, _body: String, _icon: Option, @@ -185,6 +189,9 @@ mod real { } } +use std::fmt::{Display, Formatter}; +use std::{error, fmt}; + #[allow(unused_imports)] pub use real::{ send_notification, send_notification_async, send_notification_with_timeout, @@ -193,11 +200,13 @@ pub use real::{ #[cfg(all(test, not(feature = "dbus")))] mod tests { + use tokio::runtime::Builder; + use super::*; #[test] fn notification_without_dbus_reports_unavailable() { - let runtime = tokio::runtime::Builder::new_current_thread() + let runtime = Builder::new_current_thread() .enable_all() .build() .expect("test runtime"); diff --git a/src/ocr/controller.rs b/src/ocr/controller.rs index 8f629808c..27106d19a 100644 --- a/src/ocr/controller.rs +++ b/src/ocr/controller.rs @@ -6,9 +6,12 @@ //! is deliberate — a queued request would recognize a screen region the user //! has already moved on from. -use std::fmt; +use std::any::Any; use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::mpsc; use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, TrySendError}; +use std::thread::Builder; +use std::{fmt, io}; use crate::backend::wayland::RuntimeWakeHandle; @@ -132,19 +135,23 @@ impl OcrController { publisher: impl OcrTextPublisher + Send + 'static, ) -> Result { self.try_submit_with_spawner(request, recognizer, publisher, |job| { - std::thread::Builder::new() + Builder::new() .name("wayscriber-ocr".to_string()) .spawn(job) .map(drop) }) } + #[expect( + clippy::disallowed_methods, + reason = "a panicking recognizer must still publish a terminal failure carrying its panic message; the exit guard only reports a bare disconnect" + )] fn try_submit_with_spawner( &mut self, request: OcrRequest, recognizer: impl TextRecognizer + Send + 'static, publisher: impl OcrTextPublisher + Send + 'static, - spawn: impl FnOnce(Box) -> std::io::Result<()>, + spawn: impl FnOnce(Box) -> io::Result<()>, ) -> Result { if !self.healthy { return Err(OcrSubmitError::Unhealthy); @@ -157,12 +164,12 @@ impl OcrController { let value = self.next_id.ok_or(OcrSubmitError::IdentityExhausted)?; let id = OcrRequestId(value); - let (sender, receiver) = std::sync::mpsc::sync_channel(1); + let (sender, receiver) = mpsc::sync_channel(1); let runtime_wake = self.runtime_wake.clone(); let job = Box::new(move || { let guard = WorkerExitGuard::new(id, sender, runtime_wake); let message = match catch_unwind(AssertUnwindSafe(|| { - run_request(request, &recognizer, &publisher) + run_request(&request, &recognizer, &publisher) })) { Ok(outcome) => WorkerMessage::Ready { id, outcome }, Err(payload) => WorkerMessage::Panicked { @@ -219,7 +226,7 @@ impl OcrController { } } -fn panic_reason(payload: &Box) -> String { +fn panic_reason(payload: &Box) -> String { if let Some(message) = payload.downcast_ref::<&'static str>() { (*message).to_string() } else if let Some(message) = payload.downcast_ref::() { @@ -287,7 +294,7 @@ impl Drop for WorkerExitGuard { #[cfg(test)] mod tests { - use std::sync::mpsc; + use std::sync::{Mutex, mpsc}; use std::time::Duration; use super::*; @@ -311,7 +318,7 @@ mod tests { struct BlockingRecognizer { started: mpsc::Sender<()>, - release: std::sync::Mutex>, + release: Mutex>, } impl TextRecognizer for BlockingRecognizer { @@ -355,13 +362,13 @@ mod tests { fn request() -> OcrRequest { OcrRequest { - pixels: PackedArgb32::new(2, 2, 8, vec![0xFF; 16]).unwrap(), + pixels: PackedArgb32::new(2, 2, 8, vec![0xff; 16]).unwrap(), languages: OcrLanguages::from_validated("eng".to_string()), } } fn controller() -> (RuntimeWakeSource, OcrController) { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let controller = OcrController::new(wake.handle()); (wake, controller) } @@ -417,7 +424,7 @@ mod tests { request(), BlockingRecognizer { started: started_tx, - release: std::sync::Mutex::new(release_rx), + release: Mutex::new(release_rx), }, NoopPublisher, ) @@ -496,7 +503,7 @@ mod tests { fn a_worker_that_exits_without_publishing_is_reported_as_lost() { let (wake, mut controller) = controller(); let id = OcrRequestId(9); - let (sender, receiver) = std::sync::mpsc::sync_channel(1); + let (sender, receiver) = mpsc::sync_channel(1); controller.active = Some(ActiveRequest { id, receiver }); drop(WorkerExitGuard::new(id, sender, wake.handle())); @@ -518,7 +525,7 @@ mod tests { outcome: || Err(OcrFailure::EngineFailed), }, NoopPublisher, - |_job| Err(std::io::Error::other("injected spawn failure")), + |_job| Err(io::Error::other("injected spawn failure")), ) .unwrap_err(); diff --git a/src/ocr/mod.rs b/src/ocr/mod.rs index dc551bb9d..d37049af4 100644 --- a/src/ocr/mod.rs +++ b/src/ocr/mod.rs @@ -162,7 +162,7 @@ pub(crate) type OcrOutcome = Result; /// is created and consumed inside a single stack frame and is dropped before /// the outcome travels back to the event loop. fn run_request( - request: OcrRequest, + request: &OcrRequest, recognizer: &dyn TextRecognizer, publisher: &dyn OcrTextPublisher, ) -> OcrOutcome { @@ -191,6 +191,9 @@ pub(crate) struct RecognizedOutput { #[cfg(test)] mod tests { + use std::collections::BTreeSet; + use std::sync::Mutex; + use super::*; struct StubRecognizer(fn() -> Result); @@ -205,7 +208,7 @@ mod tests { } } - struct RecordingPublisher(std::sync::Mutex>); + struct RecordingPublisher(Mutex>); impl OcrTextPublisher for RecordingPublisher { fn publish(&self, text: &str) -> Result<(), OcrFailure> { @@ -227,7 +230,7 @@ mod tests { width, height, (width * 4) as i32, - vec![0xFF; (width * height * 4) as usize], + vec![0xff; (width * height * 4) as usize], ) .unwrap() } @@ -260,9 +263,9 @@ mod tests { #[test] fn empty_recognition_does_not_publish_an_empty_clipboard_value() { - let publisher = RecordingPublisher(std::sync::Mutex::new(Vec::new())); + let publisher = RecordingPublisher(Mutex::new(Vec::new())); let outcome = run_request( - request(), + &request(), &StubRecognizer(|| { Ok(RecognizedOutput { text: RecognizedText::trimmed(" \n\t "), @@ -278,9 +281,9 @@ mod tests { #[test] fn successful_recognition_publishes_text_but_reports_only_a_character_count() { - let publisher = RecordingPublisher(std::sync::Mutex::new(Vec::new())); + let publisher = RecordingPublisher(Mutex::new(Vec::new())); let outcome = run_request( - request(), + &request(), &StubRecognizer(|| { Ok(RecognizedOutput { text: RecognizedText::trimmed("hello\n"), @@ -304,7 +307,7 @@ mod tests { #[test] fn clipboard_failure_is_distinct_from_engine_failure() { let outcome = run_request( - request(), + &request(), &StubRecognizer(|| { Ok(RecognizedOutput { text: RecognizedText::trimmed("hello"), @@ -337,7 +340,7 @@ mod tests { OcrFailure::ClipboardFailed, ] .map(|failure| failure.message()); - let unique: std::collections::BTreeSet<_> = messages.iter().collect(); + let unique: BTreeSet<_> = messages.iter().collect(); assert_eq!(unique.len(), messages.len()); assert!(messages[1].contains("deu")); @@ -349,9 +352,9 @@ mod tests { pixels: pixels(0, 4), languages: OcrLanguages::from_validated("eng".to_string()), }; - let publisher = RecordingPublisher(std::sync::Mutex::new(Vec::new())); + let publisher = RecordingPublisher(Mutex::new(Vec::new())); let outcome = run_request( - request, + &request, &StubRecognizer(|| panic!("empty pixels must not reach the recognizer")), &publisher, ); diff --git a/src/ocr/tesseract.rs b/src/ocr/tesseract.rs index b56e1a084..a17dbbead 100644 --- a/src/ocr/tesseract.rs +++ b/src/ocr/tesseract.rs @@ -1,14 +1,15 @@ //! Production adapters: local Tesseract for recognition, `wl-copy` for publication. use std::ffi::{OsStr, OsString}; -use std::fs; use std::io::{ErrorKind, Write}; use std::os::unix::fs::DirBuilderExt; use std::path::{Path, PathBuf}; use std::time::Duration; +use std::{env, fs}; use crate::env_vars::XDG_RUNTIME_DIR_ENV; use crate::process_broker::{HelperKind, STDOUT_CAP_EXCEEDED}; +use crate::{clipboard_text, process_broker}; use super::{ OcrFailure, OcrLanguages, OcrTextPublisher, RecognizedOutput, RecognizedText, TextRecognizer, @@ -48,8 +49,8 @@ fn with_temporary_png( png: &[u8], operation: impl FnOnce(&Path) -> Result, ) -> Result { - let Some(directory) = private_runtime_directory(std::env::var_os(XDG_RUNTIME_DIR_ENV)) else { - return with_temporary_png_in(png, &std::env::temp_dir(), operation); + let Some(directory) = private_runtime_directory(env::var_os(XDG_RUNTIME_DIR_ENV)) else { + return with_temporary_png_in(png, &env::temp_dir(), operation); }; remove_stale_temporary_pngs(&directory); @@ -134,7 +135,7 @@ fn with_temporary_png_in( } fn run_tesseract(input: &Path, languages: &OcrLanguages) -> Result { - let output = crate::process_broker::current() + let output = process_broker::current() .and_then(|broker| { broker.run( HelperKind::Tesseract, @@ -166,7 +167,7 @@ fn run_tesseract(input: &Path, languages: &OcrLanguages) -> Result OcrFailure { /// Checked before invoking so "not installed" is its own actionable message /// rather than a generic spawn failure. fn program_on_path(program: &str) -> bool { - std::env::var_os("PATH").is_some_and(|path| program_in_search_path(program, &path)) + env::var_os("PATH").is_some_and(|path| program_in_search_path(program, &path)) } /// The search itself, over an explicit path list. Split out so tests can probe @@ -232,11 +233,11 @@ fn program_on_path(program: &str) -> bool { fn program_in_search_path(program: &str, search_path: &OsStr) -> bool { use std::os::unix::fs::PermissionsExt; - std::env::split_paths(search_path).any(|directory| { + env::split_paths(search_path).any(|directory| { if directory.as_os_str().is_empty() { return false; } - std::fs::metadata(directory.join(program)) + fs::metadata(directory.join(program)) .map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) .unwrap_or(false) }) @@ -246,7 +247,7 @@ pub(crate) struct WlCopyPublisher; impl OcrTextPublisher for WlCopyPublisher { fn publish(&self, text: &str) -> Result<(), OcrFailure> { - crate::clipboard_text::copy_text_via_command(text).map_err(|err| { + clipboard_text::copy_text_via_command(text).map_err(|err| { log::warn!("wl-copy failed for recognized text: {err}"); OcrFailure::ClipboardFailed }) @@ -255,6 +256,11 @@ impl OcrTextPublisher for WlCopyPublisher { #[cfg(test)] mod tests { + use std::fs::Permissions; + use std::thread; + + use crate::{test_env, test_temp}; + use super::*; fn languages() -> OcrLanguages { @@ -315,9 +321,9 @@ mod tests { fn path_probe_accepts_only_executable_files() { use std::os::unix::fs::PermissionsExt; - let directory = crate::test_temp::tempdir().unwrap(); + let directory = test_temp::tempdir().unwrap(); let program = directory.path().join("wayscriber-fake-ocr"); - std::fs::write(&program, b"").unwrap(); + fs::write(&program, b"").unwrap(); let search_path = directory.path().as_os_str(); assert!( @@ -325,7 +331,7 @@ mod tests { "a non-executable file is not a usable engine" ); - std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap(); + fs::set_permissions(&program, Permissions::from_mode(0o755)).unwrap(); assert!(program_in_search_path("wayscriber-fake-ocr", search_path)); assert!(!program_in_search_path( "wayscriber-absent-ocr", @@ -337,16 +343,15 @@ mod tests { fn path_probe_skips_empty_entries_and_searches_every_directory() { use std::os::unix::fs::PermissionsExt; - let directory = crate::test_temp::tempdir().unwrap(); + let directory = test_temp::tempdir().unwrap(); let program = directory.path().join("wayscriber-fake-ocr"); - std::fs::write(&program, b"").unwrap(); - std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap(); + fs::write(&program, b"").unwrap(); + fs::set_permissions(&program, Permissions::from_mode(0o755)).unwrap(); // An empty entry means "current directory" to some shells; treating it // as one would make the probe depend on the working directory. let search_path = - std::env::join_paths(["".as_ref(), Path::new("/nonexistent"), directory.path()]) - .unwrap(); + env::join_paths(["".as_ref(), Path::new("/nonexistent"), directory.path()]).unwrap(); assert!(program_in_search_path("wayscriber-fake-ocr", &search_path)); } @@ -373,7 +378,7 @@ mod tests { fn runtime_directory_is_created_private_under_an_absolute_runtime_dir() { use std::os::unix::fs::PermissionsExt; - let runtime = crate::test_temp::tempdir().unwrap(); + let runtime = test_temp::tempdir().unwrap(); let directory = private_runtime_directory(Some(runtime.path().into())).unwrap(); @@ -387,7 +392,7 @@ mod tests { #[test] fn production_input_goes_to_the_runtime_directory_after_removing_stale_inputs() { - let runtime = crate::test_temp::tempdir().unwrap(); + let runtime = test_temp::tempdir().unwrap(); let directory = runtime.path().join("wayscriber"); fs::create_dir(&directory).unwrap(); let stale = directory.join("wayscriber-ocr-stale.png"); @@ -395,7 +400,7 @@ mod tests { fs::write(&stale, b"screen crop").unwrap(); fs::write(&unrelated, b"").unwrap(); - let result = crate::test_env::with_env_var( + let result = test_env::with_env_var( XDG_RUNTIME_DIR_ENV, Some(runtime.path().as_os_str()), || { @@ -414,11 +419,11 @@ mod tests { #[test] fn temporary_input_is_removed_after_success() { - let directory = crate::test_temp::tempdir().unwrap(); + let directory = test_temp::tempdir().unwrap(); let result = with_temporary_png_in(b"png", directory.path(), |path| { assert!(path.exists()); - assert_eq!(std::fs::read(path).unwrap(), b"png"); + assert_eq!(fs::read(path).unwrap(), b"png"); Ok("recognized") }); @@ -428,7 +433,7 @@ mod tests { #[test] fn temporary_input_is_removed_after_operation_failure() { - let directory = crate::test_temp::tempdir().unwrap(); + let directory = test_temp::tempdir().unwrap(); let result = with_temporary_png_in(b"png", directory.path(), |path| { assert!(path.exists()); @@ -441,22 +446,26 @@ mod tests { #[test] fn temporary_input_is_removed_while_unwinding_from_a_panic() { - let directory = crate::test_temp::tempdir().unwrap(); - - let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let _: Result<(), OcrFailure> = - with_temporary_png_in(b"png", directory.path(), |path| { - assert!(path.exists()); - panic!("simulated OCR adapter panic"); - }); - })); + let directory = test_temp::tempdir().unwrap(); + + let panic = thread::scope(|scope| { + scope + .spawn(|| { + let _: Result<(), OcrFailure> = + with_temporary_png_in(b"png", directory.path(), |path| { + assert!(path.exists()); + panic!("simulated OCR adapter panic"); + }); + }) + .join() + }); assert!(panic.is_err()); assert!(temporary_ocr_files(directory.path()).is_empty()); } - fn temporary_ocr_files(directory: &Path) -> Vec { - let Ok(entries) = std::fs::read_dir(directory) else { + fn temporary_ocr_files(directory: &Path) -> Vec { + let Ok(entries) = fs::read_dir(directory) else { return Vec::new(); }; let mut paths: Vec<_> = entries diff --git a/src/onboarding.rs b/src/onboarding.rs index b3278c55b..83321d00d 100644 --- a/src/onboarding.rs +++ b/src/onboarding.rs @@ -1,5 +1,13 @@ +use std::fmt::{Display, Formatter}; +use std::{error, fmt, io, process}; + +use toml::ser; + use crate::domain::OnboardingTip; -use crate::durable_io::{AtomicWriteOptions, OverwriteMode, PermissionPolicy, SymlinkPolicy}; +use crate::durable_io; +use crate::durable_io::{ + AtomicWriteOptions, DurableIoError, OverwriteMode, PermissionPolicy, SymlinkPolicy, +}; use crate::paths::data_dir; use log::warn; use serde::{Deserialize, Serialize}; @@ -354,17 +362,17 @@ pub(crate) enum OnboardingSaveError { Unavailable, CreateDirectory { path: PathBuf, - source: std::io::Error, + source: io::Error, }, - Serialize(toml::ser::Error), + Serialize(ser::Error), Write { path: PathBuf, - source: crate::durable_io::DurableIoError, + source: DurableIoError, }, } -impl std::fmt::Display for OnboardingSaveError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for OnboardingSaveError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { match self { Self::Unavailable => formatter.write_str("no user data directory is available"), Self::CreateDirectory { path, source } => write!( @@ -384,7 +392,7 @@ impl std::fmt::Display for OnboardingSaveError { } } -impl std::error::Error for OnboardingSaveError {} +impl error::Error for OnboardingSaveError {} impl OnboardingStore { pub fn load() -> Self { @@ -596,7 +604,7 @@ impl OnboardingStore { return Err(error); } }; - if let Err(source) = crate::durable_io::write_text_atomic( + if let Err(source) = durable_io::write_text_atomic( path, &contents, AtomicWriteOptions { @@ -831,7 +839,7 @@ fn backup_path(path: &Path) -> PathBuf { .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_nanos(); - let pid = std::process::id(); + let pid = process::id(); let mut candidate = path.with_extension(format!("bak.{nanos}.{pid}")); if !candidate.exists() { return candidate; diff --git a/src/onboarding/tests.rs b/src/onboarding/tests.rs index c363f34ed..83d23d7ef 100644 --- a/src/onboarding/tests.rs +++ b/src/onboarding/tests.rs @@ -1,9 +1,12 @@ +use crate::test_temp; +use crate::test_temp::TempDir; + use super::*; use std::fs; #[test] fn onboarding_defaults_when_missing() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); let mut store = OnboardingStore::load_from_path(path.clone()); assert!(!store.state().welcome_shown); @@ -17,7 +20,7 @@ fn onboarding_defaults_when_missing() { #[test] fn onboarding_persists_flags() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); let mut store = OnboardingStore::load_from_path(path.clone()); store.state_mut().welcome_shown = true; @@ -25,7 +28,7 @@ fn onboarding_persists_flags() { store.state_mut().used_help_overlay = true; store.save().expect("updated state should persist"); - let reloaded = OnboardingStore::load_from_path(path.clone()); + let reloaded = OnboardingStore::load_from_path(path); assert!(reloaded.state().welcome_shown); assert!(reloaded.state().toolbar_hint_shown); assert!(reloaded.state().used_help_overlay); @@ -36,7 +39,7 @@ fn onboarding_persists_flags() { fn rejected_persistence_disables_automatic_onboarding_for_the_session() { use std::os::unix::fs::symlink; - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let target = tmp.path().join("real-onboarding.toml"); fs::write(&target, "version = 5\n").expect("seed should be writable"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); @@ -59,7 +62,7 @@ fn rejected_persistence_disables_automatic_onboarding_for_the_session() { #[test] fn unwritable_state_location_disables_automatic_onboarding_for_the_session() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let blocker = tmp.path().join("not-a-directory"); fs::write(&blocker, "occupied").expect("blocker file should be created"); let path = blocker.join(ONBOARDING_FILE); @@ -78,7 +81,7 @@ fn unwritable_state_location_disables_automatic_onboarding_for_the_session() { #[test] fn startup_notice_acknowledgement_survives_reload_and_is_content_specific() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); let mut store = OnboardingStore::load_from_path(path.clone()); @@ -97,7 +100,7 @@ fn startup_notice_acknowledgement_survives_reload_and_is_content_specific() { #[test] fn deferred_hint_cap_survives_repeated_process_launches() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); for expected_count in 0..DEFERRED_HINT_REPEAT_MAX { @@ -127,7 +130,7 @@ fn deferred_hint_cap_survives_repeated_process_launches() { #[test] fn using_surface_features_stops_their_tips_from_rearming() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); let mut store = OnboardingStore::load_from_path(path); let state = store.state_mut(); @@ -150,12 +153,12 @@ fn using_surface_features_stops_their_tips_from_rearming() { #[test] fn acknowledging_one_tip_survives_reload_without_suppressing_other_tips() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); let mut store = OnboardingStore::load_from_path(path.clone()); store - .acknowledge_tip(crate::domain::OnboardingTip::StatusBar) + .acknowledge_tip(OnboardingTip::StatusBar) .expect("tip acknowledgement should persist"); let reloaded = OnboardingStore::load_from_path(path); @@ -172,7 +175,7 @@ fn acknowledging_one_tip_survives_reload_without_suppressing_other_tips() { fn failed_tip_acknowledgement_suppresses_this_session_without_claiming_persistence() { use std::os::unix::fs::symlink; - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let target = tmp.path().join("real-onboarding.toml"); fs::write( &target, @@ -185,11 +188,7 @@ fn failed_tip_acknowledgement_suppresses_this_session_without_claiming_persisten symlink(&target, &path).expect("test symlink should be created"); let mut store = OnboardingStore::load_from_path(path.clone()); - assert!( - store - .acknowledge_tip(crate::domain::OnboardingTip::ZoomChip) - .is_err() - ); + assert!(store.acknowledge_tip(OnboardingTip::ZoomChip).is_err()); assert!(store.state().hint_zoom_chip_shown); assert_eq!(store.state().hint_zoom_chip_count, DEFERRED_HINT_REPEAT_MAX); assert!(!store.persistence_available()); @@ -204,7 +203,7 @@ fn failed_tip_acknowledgement_suppresses_this_session_without_claiming_persisten #[test] fn disabled_automatic_guidance_does_not_activate_first_run() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); let mut store = OnboardingStore::load_from_path(path); @@ -218,7 +217,7 @@ fn disabled_automatic_guidance_does_not_activate_first_run() { #[test] fn onboarding_recovers_from_parse_error() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create onboarding dir"); @@ -249,7 +248,7 @@ fn onboarding_recovers_from_parse_error() { #[test] fn onboarding_version_bump_saves() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create onboarding dir"); @@ -271,7 +270,7 @@ fn onboarding_version_bump_saves() { #[test] fn retired_radial_flick_step_resumes_at_reference() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create onboarding dir"); @@ -291,7 +290,7 @@ fn retired_radial_flick_step_resumes_at_reference() { #[test] fn v3_file_migrates_to_current_version_preserving_completion() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create onboarding dir"); @@ -330,7 +329,7 @@ used_command_palette = true #[test] fn completed_pre_v6_profile_does_not_receive_new_surface_tips() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create onboarding dir"); @@ -380,7 +379,7 @@ coach_hint_count = 1 #[test] fn m9_surface_hint_fields_persist_and_reconcile() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); let mut store = OnboardingStore::load_from_path(path.clone()); store.state_mut().hint_status_bar_count = 2; @@ -410,7 +409,7 @@ fn m9_surface_hint_fields_persist_and_reconcile() { #[test] fn coach_bookkeeping_reconciles_capped_count_to_learned_flag() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create onboarding dir"); @@ -427,14 +426,8 @@ fn coach_bookkeeping_reconciles_capped_count_to_learned_flag() { } /// Writes `seed` as the onboarding file and loads it through migration. -fn load_seed( - seed: &str, -) -> ( - OnboardingStore, - std::path::PathBuf, - crate::test_temp::TempDir, -) { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); +fn load_seed(seed: &str) -> (OnboardingStore, PathBuf, TempDir) { + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); fs::create_dir_all(path.parent().expect("parent")).expect("create onboarding dir"); fs::write(&path, seed).expect("write seed"); @@ -443,7 +436,7 @@ fn load_seed( #[test] fn a_fresh_tour_starts_by_drawing_not_by_background_mode() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join(ONBOARDING_DIR).join(ONBOARDING_FILE); let mut store = OnboardingStore::load_from_path(path); diff --git a/src/palette_recents.rs b/src/palette_recents.rs index 00633b01e..ef9a0cce5 100644 --- a/src/palette_recents.rs +++ b/src/palette_recents.rs @@ -7,6 +7,7 @@ //! treated as an empty history, never an error. use crate::domain::Action; +use crate::durable_io; use crate::durable_io::{AtomicWriteOptions, OverwriteMode, PermissionPolicy, SymlinkPolicy}; use crate::paths::data_dir; use log::warn; @@ -51,6 +52,7 @@ pub struct PaletteRecentsStore { } impl PaletteRecentsStore { + #[must_use] pub fn load() -> Self { let Some(path) = palette_recents_path() else { return Self { @@ -84,6 +86,7 @@ impl PaletteRecentsStore { } } + #[must_use] pub fn recents(&self) -> &[Action] { &self.recents } @@ -149,7 +152,7 @@ impl PaletteRecentsStore { return false; } }; - if let Err(err) = crate::durable_io::write_text_atomic( + if let Err(err) = durable_io::write_text_atomic( &path, &contents, AtomicWriteOptions { @@ -204,12 +207,14 @@ fn palette_recents_path() -> Option { #[cfg(test)] mod tests { + use crate::test_temp; + use super::*; use std::fs; #[test] fn missing_file_yields_empty_recents() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp .path() .join(PALETTE_RECENTS_DIR) @@ -220,7 +225,7 @@ mod tests { #[test] fn recents_round_trip_preserves_order_dedupes_and_caps() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp .path() .join(PALETTE_RECENTS_DIR) @@ -262,7 +267,7 @@ mod tests { #[test] fn corrupt_file_degrades_to_empty_without_error() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp .path() .join(PALETTE_RECENTS_DIR) @@ -285,7 +290,7 @@ mod tests { #[test] fn identical_update_after_no_change_is_a_successful_noop() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp .path() .join(PALETTE_RECENTS_DIR) @@ -304,14 +309,14 @@ mod tests { // masked when the in-memory value already matches. A later identical // update has to re-attempt the write (and fail again) instead of // early-returning success and silently dropping the pending state. - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); // Put a regular file where the store's parent directory must go, so // `create_dir_all` on the parent fails deterministically. let blocker = tmp.path().join(PALETTE_RECENTS_DIR); fs::write(&blocker, "not a directory").expect("write blocker file"); let path = blocker.join(PALETTE_RECENTS_FILE); - let mut store = PaletteRecentsStore::load_from_path(path.clone()); + let mut store = PaletteRecentsStore::load_from_path(path); // First write fails (parent is a file, not a directory). assert!( @@ -330,7 +335,7 @@ mod tests { #[test] fn unknown_action_ids_are_skipped_not_fatal() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp .path() .join(PALETTE_RECENTS_DIR) diff --git a/src/palette_recents/worker.rs b/src/palette_recents/worker.rs index 1f297444c..b23b261d1 100644 --- a/src/palette_recents/worker.rs +++ b/src/palette_recents/worker.rs @@ -43,7 +43,7 @@ impl PaletteRecentsWriter { let worker_shutdown = Arc::clone(&shutdown); let worker = thread::Builder::new() .name("palette-recents".to_string()) - .spawn(move || run_writer(store, worker_pending, worker_shutdown, receiver)); + .spawn(move || run_writer(store, &worker_pending, &worker_shutdown, &receiver)); match worker { Ok(worker) => Self { @@ -83,7 +83,7 @@ impl PaletteRecentsWriter { *self .pending .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(latest); + .expect("the pending recents slot is never locked across a panic") = Some(latest); match wake.try_send(()) { Ok(()) | Err(TrySendError::Full(())) => true, @@ -109,9 +109,9 @@ impl Drop for PaletteRecentsWriter { fn run_writer( mut store: PaletteRecentsStore, - pending: Arc>>>, - shutdown: Arc, - receiver: Receiver<()>, + pending: &Mutex>>, + shutdown: &AtomicBool, + receiver: &Receiver<()>, ) { let mut retry_delay = INITIAL_RETRY_DELAY; @@ -119,7 +119,7 @@ fn run_writer( loop { let desired = pending .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .expect("the pending recents slot is never locked across a panic") .take(); let Some(desired) = desired else { break; @@ -134,7 +134,7 @@ fn run_writer( // otherwise restore the failed snapshot for the timed retry. let mut queued = pending .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("the pending recents slot is never locked across a panic"); if queued.is_none() { *queued = Some(desired); } @@ -158,12 +158,14 @@ fn run_writer( #[cfg(test)] mod tests { + use crate::test_temp; + use super::*; use std::path::PathBuf; #[test] fn writer_flushes_the_latest_queued_snapshot_before_drop_returns() { - let tmp = crate::test_temp::tempdir().expect("tempdir should succeed"); + let tmp = test_temp::tempdir().expect("tempdir should succeed"); let path = tmp.path().join("wayscriber").join("palette_recents.toml"); let store = PaletteRecentsStore::load_from_path(path.clone()); let writer = PaletteRecentsWriter::new(store); diff --git a/src/paths/mod.rs b/src/paths/mod.rs index c9c6960fc..5e97da704 100644 --- a/src/paths/mod.rs +++ b/src/paths/mod.rs @@ -1,6 +1,6 @@ use std::env; use std::ffi::OsString; -use std::path::PathBuf; +use std::path::{Component, Path, PathBuf}; use crate::env_vars::{ HOME_ENV, USERPROFILE_ENV, XDG_CACHE_HOME_ENV, XDG_CONFIG_HOME_ENV, XDG_DATA_DIRS_ENV, @@ -16,6 +16,7 @@ pub fn home_dir() -> Option { } /// Resolve the XDG config directory, falling back to `~/.config`. +#[must_use] pub fn config_dir() -> Option { if let Some(dir) = env::var_os(XDG_CONFIG_HOME_ENV) && !dir.is_empty() @@ -26,6 +27,7 @@ pub fn config_dir() -> Option { } /// Resolve the XDG cache directory, falling back to `~/.cache`. +#[must_use] pub fn cache_dir() -> Option { if let Some(dir) = env::var_os(XDG_CACHE_HOME_ENV) && !dir.is_empty() @@ -37,6 +39,7 @@ pub fn cache_dir() -> Option { /// Location for the last update-check result. Cache, not data: it is /// regenerable, and deleting it only costs one extra request. +#[must_use] pub fn update_check_cache_file() -> PathBuf { cache_dir() .unwrap_or_else(|| home_dir().unwrap_or_else(fallback_runtime_root)) @@ -45,6 +48,7 @@ pub fn update_check_cache_file() -> PathBuf { } /// Resolve the XDG data directory, falling back to `~/.local/share`. +#[must_use] pub fn data_dir() -> Option { if let Some(dir) = env::var_os(XDG_DATA_HOME_ENV) && !dir.is_empty() @@ -60,6 +64,7 @@ pub fn data_dir() -> Option { /// /// Unlike [`data_dir`], this ignores relative values, as the spec requires, so /// the result does not depend on the current directory. +#[must_use] pub fn searched_data_dirs() -> Vec { searched_data_dirs_from( env::var_os(XDG_DATA_HOME_ENV), @@ -111,6 +116,7 @@ pub(crate) fn runtime_ui_state_file() -> PathBuf { } /// Best-effort pictures directory (XDG), falling back to `~/Pictures`. +#[must_use] pub fn pictures_dir() -> Option { if let Some(dir) = env::var_os(XDG_PICTURES_DIR_ENV) && !dir.is_empty() @@ -121,6 +127,7 @@ pub fn pictures_dir() -> Option { } /// Expand a path string that may start with `~/` into an absolute path. +#[must_use] pub fn expand_tilde(path: &str) -> PathBuf { if let Some(stripped) = path.strip_prefix("~/") && let Some(home) = home_dir() @@ -134,17 +141,18 @@ pub fn expand_tilde(path: &str) -> PathBuf { /// /// Rejects empty names, `.`, `..`, NULs, slashes, and anything `Path::join` /// would treat as leaving or replacing the parent directory. +#[must_use] pub fn is_single_path_component(name: &str) -> bool { if name.is_empty() || name.contains('\0') || name.contains('/') || name.contains('\\') { return false; } - let path = std::path::Path::new(name); + let path = Path::new(name); if path.is_absolute() { return false; } let mut components = path.components(); match components.next() { - Some(std::path::Component::Normal(os_name)) => { + Some(Component::Normal(os_name)) => { components.next().is_none() && os_name.to_str().is_some_and(|component| { component == name && component != "." && component != ".." @@ -155,7 +163,7 @@ pub fn is_single_path_component(name: &str) -> bool { } fn fallback_runtime_root() -> PathBuf { - std::env::temp_dir().join("wayscriber") + env::temp_dir().join("wayscriber") } fn runtime_root() -> PathBuf { @@ -174,35 +182,41 @@ fn runtime_root() -> PathBuf { /// Location for transient tray commands. /// Uses [`XDG_RUNTIME_DIR_ENV`] when available; falls back to data/home/temp. +#[must_use] pub fn tray_action_file() -> PathBuf { runtime_root().join("tray_action") } /// Location for queued transient tray commands. /// Uses [`XDG_RUNTIME_DIR_ENV`] when available; falls back to data/home/temp. +#[must_use] pub fn tray_action_dir() -> PathBuf { runtime_root().join("tray-actions") } /// Location for transient daemon toggle requests. /// Uses [`XDG_RUNTIME_DIR_ENV`] when available; falls back to data/home/temp. +#[must_use] pub fn daemon_command_file() -> PathBuf { runtime_root().join("daemon_command.json") } /// Location for queued daemon toggle requests. /// Uses [`XDG_RUNTIME_DIR_ENV`] when available; falls back to data/home/temp. +#[must_use] pub fn daemon_command_dir() -> PathBuf { runtime_root().join("daemon-commands") } /// Location for the running daemon PID. /// Uses [`XDG_RUNTIME_DIR_ENV`] when available; falls back to data/home/temp. +#[must_use] pub fn daemon_pid_file() -> PathBuf { runtime_root().join("wayscriber.pid") } /// Location for persistent logs. +#[must_use] pub fn log_dir() -> PathBuf { data_dir() .unwrap_or_else(|| home_dir().unwrap_or_else(fallback_runtime_root)) @@ -211,11 +225,13 @@ pub fn log_dir() -> PathBuf { } /// Location for the daemon single-instance lock. +#[must_use] pub fn daemon_lock_file() -> PathBuf { runtime_root().join("wayscriber.lock") } /// Location for the active-overlay single-instance lock. +#[must_use] pub fn overlay_lock_file() -> PathBuf { runtime_root().join("wayscriber-overlay.lock") } diff --git a/src/paths/tests.rs b/src/paths/tests.rs index 5411d1b90..4b8aea763 100644 --- a/src/paths/tests.rs +++ b/src/paths/tests.rs @@ -1,3 +1,5 @@ +use crate::{test_env, test_temp}; + use super::*; use crate::env_vars::{ HOME_ENV, USERPROFILE_ENV, XDG_CACHE_HOME_ENV, XDG_CONFIG_HOME_ENV, XDG_DATA_HOME_ENV, @@ -8,8 +10,8 @@ use std::env; #[test] #[cfg(unix)] fn tray_action_prefers_runtime_dir_when_set() { - let _guard = crate::test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let tmp = test_temp::tempdir().unwrap(); let prev = env::var_os(XDG_RUNTIME_DIR_ENV); // SAFETY: serialised via ENV_MUTEX unsafe { @@ -24,10 +26,12 @@ fn tray_action_prefers_runtime_dir_when_set() { assert!(daemon_pid_file().starts_with(tmp.path())); if let Some(prev) = prev { + // SAFETY: this test still holds the environment mutex in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, prev); } } else { + // SAFETY: this test still holds the environment mutex in `_guard`. unsafe { env::remove_var(XDG_RUNTIME_DIR_ENV); } @@ -36,13 +40,14 @@ fn tray_action_prefers_runtime_dir_when_set() { #[test] fn config_dir_prefers_xdg_config_home_when_set() { - let _guard = crate::test_env::lock(); + let _guard = test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let prev_home = env::var_os(HOME_ENV); let prev_userprofile = env::var_os(USERPROFILE_ENV); let prev_xdg = env::var_os(XDG_CONFIG_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_CONFIG_HOME_ENV, tmp.path()); env::remove_var(HOME_ENV); @@ -53,29 +58,33 @@ fn config_dir_prefers_xdg_config_home_when_set() { .unwrap_or_else(|| panic!("config_dir should resolve from {XDG_CONFIG_HOME_ENV}")); assert_eq!(dir, tmp.path()); - match prev_xdg { - Some(v) => unsafe { env::set_var(XDG_CONFIG_HOME_ENV, v) }, - None => unsafe { env::remove_var(XDG_CONFIG_HOME_ENV) }, - } - match prev_home { - Some(v) => unsafe { env::set_var(HOME_ENV, v) }, - None => unsafe { env::remove_var(HOME_ENV) }, - } - match prev_userprofile { - Some(v) => unsafe { env::set_var(USERPROFILE_ENV, v) }, - None => unsafe { env::remove_var(USERPROFILE_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev_xdg { + Some(v) => env::set_var(XDG_CONFIG_HOME_ENV, v), + None => env::remove_var(XDG_CONFIG_HOME_ENV), + } + match prev_home { + Some(v) => env::set_var(HOME_ENV, v), + None => env::remove_var(HOME_ENV), + } + match prev_userprofile { + Some(v) => env::set_var(USERPROFILE_ENV, v), + None => env::remove_var(USERPROFILE_ENV), + } } } #[test] fn update_check_cache_follows_the_cache_directory() { - let _guard = crate::test_env::lock(); + let _guard = test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let prev_home = env::var_os(HOME_ENV); let prev_userprofile = env::var_os(USERPROFILE_ENV); let prev_xdg = env::var_os(XDG_CACHE_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_CACHE_HOME_ENV, tmp.path()); env::remove_var(HOME_ENV); @@ -90,6 +99,7 @@ fn update_check_cache_follows_the_cache_directory() { // Falling back to HOME keeps the result inside ~/.cache, never in data or // config directories. + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::remove_var(XDG_CACHE_HOME_ENV); env::set_var(HOME_ENV, tmp.path()); @@ -102,29 +112,33 @@ fn update_check_cache_follows_the_cache_directory() { .join("update-check.json") ); - match prev_xdg { - Some(v) => unsafe { env::set_var(XDG_CACHE_HOME_ENV, v) }, - None => unsafe { env::remove_var(XDG_CACHE_HOME_ENV) }, - } - match prev_home { - Some(v) => unsafe { env::set_var(HOME_ENV, v) }, - None => unsafe { env::remove_var(HOME_ENV) }, - } - match prev_userprofile { - Some(v) => unsafe { env::set_var(USERPROFILE_ENV, v) }, - None => unsafe { env::remove_var(USERPROFILE_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev_xdg { + Some(v) => env::set_var(XDG_CACHE_HOME_ENV, v), + None => env::remove_var(XDG_CACHE_HOME_ENV), + } + match prev_home { + Some(v) => env::set_var(HOME_ENV, v), + None => env::remove_var(HOME_ENV), + } + match prev_userprofile { + Some(v) => env::set_var(USERPROFILE_ENV, v), + None => env::remove_var(USERPROFILE_ENV), + } } } #[test] fn config_dir_falls_back_to_home_config() { - let _guard = crate::test_env::lock(); + let _guard = test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let prev_home = env::var_os(HOME_ENV); let prev_userprofile = env::var_os(USERPROFILE_ENV); let prev_xdg = env::var_os(XDG_CONFIG_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(HOME_ENV, tmp.path()); env::remove_var(USERPROFILE_ENV); @@ -134,29 +148,33 @@ fn config_dir_falls_back_to_home_config() { let dir = config_dir().expect("config_dir should resolve from HOME"); assert_eq!(dir, tmp.path().join(".config")); - match prev_xdg { - Some(v) => unsafe { env::set_var(XDG_CONFIG_HOME_ENV, v) }, - None => unsafe { env::remove_var(XDG_CONFIG_HOME_ENV) }, - } - match prev_home { - Some(v) => unsafe { env::set_var(HOME_ENV, v) }, - None => unsafe { env::remove_var(HOME_ENV) }, - } - match prev_userprofile { - Some(v) => unsafe { env::set_var(USERPROFILE_ENV, v) }, - None => unsafe { env::remove_var(USERPROFILE_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev_xdg { + Some(v) => env::set_var(XDG_CONFIG_HOME_ENV, v), + None => env::remove_var(XDG_CONFIG_HOME_ENV), + } + match prev_home { + Some(v) => env::set_var(HOME_ENV, v), + None => env::remove_var(HOME_ENV), + } + match prev_userprofile { + Some(v) => env::set_var(USERPROFILE_ENV, v), + None => env::remove_var(USERPROFILE_ENV), + } } } #[test] fn data_dir_prefers_xdg_data_home_when_set() { - let _guard = crate::test_env::lock(); + let _guard = test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let prev_home = env::var_os(HOME_ENV); let prev_userprofile = env::var_os(USERPROFILE_ENV); let prev_xdg = env::var_os(XDG_DATA_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_DATA_HOME_ENV, tmp.path()); env::remove_var(HOME_ENV); @@ -171,29 +189,33 @@ fn data_dir_prefers_xdg_data_home_when_set() { tmp.path().join("wayscriber/runtime-ui.toml") ); - match prev_xdg { - Some(v) => unsafe { env::set_var(XDG_DATA_HOME_ENV, v) }, - None => unsafe { env::remove_var(XDG_DATA_HOME_ENV) }, - } - match prev_home { - Some(v) => unsafe { env::set_var(HOME_ENV, v) }, - None => unsafe { env::remove_var(HOME_ENV) }, - } - match prev_userprofile { - Some(v) => unsafe { env::set_var(USERPROFILE_ENV, v) }, - None => unsafe { env::remove_var(USERPROFILE_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev_xdg { + Some(v) => env::set_var(XDG_DATA_HOME_ENV, v), + None => env::remove_var(XDG_DATA_HOME_ENV), + } + match prev_home { + Some(v) => env::set_var(HOME_ENV, v), + None => env::remove_var(HOME_ENV), + } + match prev_userprofile { + Some(v) => env::set_var(USERPROFILE_ENV, v), + None => env::remove_var(USERPROFILE_ENV), + } } } #[test] fn data_dir_falls_back_to_home_share() { - let _guard = crate::test_env::lock(); + let _guard = test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let prev_home = env::var_os(HOME_ENV); let prev_userprofile = env::var_os(USERPROFILE_ENV); let prev_xdg = env::var_os(XDG_DATA_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(HOME_ENV, tmp.path()); env::remove_var(USERPROFILE_ENV); @@ -203,17 +225,20 @@ fn data_dir_falls_back_to_home_share() { let dir = data_dir().expect("data_dir should resolve from HOME"); assert_eq!(dir, tmp.path().join(".local").join("share")); - match prev_xdg { - Some(v) => unsafe { env::set_var(XDG_DATA_HOME_ENV, v) }, - None => unsafe { env::remove_var(XDG_DATA_HOME_ENV) }, - } - match prev_home { - Some(v) => unsafe { env::set_var(HOME_ENV, v) }, - None => unsafe { env::remove_var(HOME_ENV) }, - } - match prev_userprofile { - Some(v) => unsafe { env::set_var(USERPROFILE_ENV, v) }, - None => unsafe { env::remove_var(USERPROFILE_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev_xdg { + Some(v) => env::set_var(XDG_DATA_HOME_ENV, v), + None => env::remove_var(XDG_DATA_HOME_ENV), + } + match prev_home { + Some(v) => env::set_var(HOME_ENV, v), + None => env::remove_var(HOME_ENV), + } + match prev_userprofile { + Some(v) => env::set_var(USERPROFILE_ENV, v), + None => env::remove_var(USERPROFILE_ENV), + } } } @@ -264,13 +289,14 @@ fn searched_data_dirs_follow_the_base_directory_spec() { #[test] fn pictures_dir_prefers_xdg_when_set() { - let _guard = crate::test_env::lock(); + let _guard = test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let prev_home = env::var_os(HOME_ENV); let prev_userprofile = env::var_os(USERPROFILE_ENV); let prev_xdg = env::var_os(XDG_PICTURES_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_PICTURES_DIR_ENV, tmp.path()); env::remove_var(HOME_ENV); @@ -281,29 +307,33 @@ fn pictures_dir_prefers_xdg_when_set() { .unwrap_or_else(|| panic!("pictures_dir should resolve from {XDG_PICTURES_DIR_ENV}")); assert_eq!(dir, tmp.path()); - match prev_xdg { - Some(v) => unsafe { env::set_var(XDG_PICTURES_DIR_ENV, v) }, - None => unsafe { env::remove_var(XDG_PICTURES_DIR_ENV) }, - } - match prev_home { - Some(v) => unsafe { env::set_var(HOME_ENV, v) }, - None => unsafe { env::remove_var(HOME_ENV) }, - } - match prev_userprofile { - Some(v) => unsafe { env::set_var(USERPROFILE_ENV, v) }, - None => unsafe { env::remove_var(USERPROFILE_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev_xdg { + Some(v) => env::set_var(XDG_PICTURES_DIR_ENV, v), + None => env::remove_var(XDG_PICTURES_DIR_ENV), + } + match prev_home { + Some(v) => env::set_var(HOME_ENV, v), + None => env::remove_var(HOME_ENV), + } + match prev_userprofile { + Some(v) => env::set_var(USERPROFILE_ENV, v), + None => env::remove_var(USERPROFILE_ENV), + } } } #[test] fn pictures_dir_falls_back_to_home() { - let _guard = crate::test_env::lock(); + let _guard = test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let prev_home = env::var_os(HOME_ENV); let prev_userprofile = env::var_os(USERPROFILE_ENV); let prev_xdg = env::var_os(XDG_PICTURES_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(HOME_ENV, tmp.path()); env::remove_var(USERPROFILE_ENV); @@ -313,27 +343,31 @@ fn pictures_dir_falls_back_to_home() { let dir = pictures_dir().expect("pictures_dir should resolve from HOME"); assert_eq!(dir, tmp.path().join("Pictures")); - match prev_xdg { - Some(v) => unsafe { env::set_var(XDG_PICTURES_DIR_ENV, v) }, - None => unsafe { env::remove_var(XDG_PICTURES_DIR_ENV) }, - } - match prev_home { - Some(v) => unsafe { env::set_var(HOME_ENV, v) }, - None => unsafe { env::remove_var(HOME_ENV) }, - } - match prev_userprofile { - Some(v) => unsafe { env::set_var(USERPROFILE_ENV, v) }, - None => unsafe { env::remove_var(USERPROFILE_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev_xdg { + Some(v) => env::set_var(XDG_PICTURES_DIR_ENV, v), + None => env::remove_var(XDG_PICTURES_DIR_ENV), + } + match prev_home { + Some(v) => env::set_var(HOME_ENV, v), + None => env::remove_var(HOME_ENV), + } + match prev_userprofile { + Some(v) => env::set_var(USERPROFILE_ENV, v), + None => env::remove_var(USERPROFILE_ENV), + } } } #[test] fn expand_tilde_replaces_home() { - let _guard = crate::test_env::lock(); + let _guard = test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let tmp = test_temp::tempdir().unwrap(); let prev_home = env::var_os(HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(HOME_ENV, tmp.path()); } @@ -341,9 +375,12 @@ fn expand_tilde_replaces_home() { let path = expand_tilde("~/test"); assert_eq!(path, tmp.path().join("test")); - match prev_home { - Some(v) => unsafe { env::set_var(HOME_ENV, v) }, - None => unsafe { env::remove_var(HOME_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev_home { + Some(v) => env::set_var(HOME_ENV, v), + None => env::remove_var(HOME_ENV), + } } } diff --git a/src/process_broker/bootstrap.rs b/src/process_broker/bootstrap.rs index ba70e2ba4..72b23df73 100644 --- a/src/process_broker/bootstrap.rs +++ b/src/process_broker/bootstrap.rs @@ -3,13 +3,25 @@ use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; +#[cfg(not(test))] +use std::env; #[cfg(not(test))] use std::ffi::CString; #[cfg(not(test))] use std::os::unix::ffi::OsStrExt; +#[cfg(not(test))] +use std::ptr; +use std::thread; +#[cfg(test)] +use std::thread::Builder; +use std::time::{Duration, Instant}; use anyhow::{Context, Result}; +use crate::daemon::protocol_v2::ProtocolToken; +#[cfg(not(test))] +use crate::env_vars; + use super::client::{BrokerInner, ProcessBroker}; use super::wire::BrokerOperation; #[cfg(not(test))] @@ -21,9 +33,9 @@ use super::wire::{ pub(super) fn start() -> Result { let (parent_socket, child_socket) = socket_pair("test broker")?; let (shutdown_writer, shutdown_reader) = socket_pair("test broker shutdown")?; - let token = crate::daemon::protocol_v2::ProtocolToken::generate()?.to_string(); + let token = ProtocolToken::generate()?.to_string(); let thread_token = token.clone(); - let thread = std::thread::Builder::new() + let thread = Builder::new() .name("wayscriber-process-broker-test".into()) .spawn(move || { let _socket = child_socket; @@ -56,18 +68,18 @@ pub(super) fn start() -> Result { pub(super) fn start() -> Result { let (parent_socket, child_socket) = socket_pair("broker")?; let (shutdown_writer, shutdown_reader) = socket_pair("broker shutdown")?; - let token = crate::daemon::protocol_v2::ProtocolToken::generate() + let token = ProtocolToken::generate() .context("failed to generate broker authentication token")? .to_string(); - let exe = std::env::current_exe().context("failed to resolve broker executable")?; + let exe = env::current_exe().context("failed to resolve broker executable")?; let exe = CString::new(exe.as_os_str().as_bytes())?; - let argv = [exe.as_ptr(), std::ptr::null()]; - let mut environment = std::env::vars_os() + let argv = [exe.as_ptr(), ptr::null()]; + let mut environment = env::vars_os() .filter(|(name, _)| { name != BROKER_FD_ENV && name != BROKER_TOKEN_ENV && name != BROKER_SHUTDOWN_FD_ENV - && name != crate::env_vars::DAEMON_WATCHDOG_FD_ENV + && name != env_vars::DAEMON_WATCHDOG_FD_ENV }) .map(|(name, value)| { let mut bytes = name.as_bytes().to_vec(); @@ -85,7 +97,7 @@ pub(super) fn start() -> Result { .iter() .map(|value| value.as_ptr()) .collect::>(); - envp.push(std::ptr::null()); + envp.push(ptr::null()); let child_socket_exec = duplicate_for_exec(&child_socket)?; let shutdown_exec = duplicate_for_exec(&shutdown_reader)?; @@ -100,6 +112,13 @@ pub(super) fn start() -> Result { if pid == 0 { // Raw-clone child stub: no allocation, formatting, logging, // unwinding, Rust destructors, or dynamic loader calls are allowed. + // SAFETY: the clone gave this child a private copy of the parent's + // memory with only the calling thread, so `exe`, the `environment` + // strings, and the null-terminated `argv` and `envp` arrays built + // above are still valid here. `child_fd` and `shutdown_fd` are open + // descriptors inherited from the parent. Only raw syscalls run, so no + // lock held by another parent thread is touched, and the block never + // returns: execve replaces the image or exit_group/_exit end the child. unsafe { if child_fd == BROKER_FD { let _ = libc::syscall(libc::SYS_fcntl, BROKER_FD, libc::F_SETFD, 0); @@ -179,12 +198,12 @@ fn socket_pair(label: &str) -> Result<(OwnedFd, OwnedFd)> { } /// Whether the broker process exits within `grace`, reaping it if it does. -pub(super) fn broker_exits_within(child_pid: libc::pid_t, grace: std::time::Duration) -> bool { +pub(super) fn broker_exits_within(child_pid: libc::pid_t, grace: Duration) -> bool { if child_pid <= 0 { return true; } - let deadline = std::time::Instant::now() + grace; + let deadline = Instant::now() + grace; let mut status = 0; loop { // SAFETY: child_pid names the raw-clone broker child owned by its guard. @@ -199,10 +218,10 @@ pub(super) fn broker_exits_within(child_pid: libc::pid_t, grace: std::time::Dura // Already reaped: nothing is left to wait for. return true; } - if std::time::Instant::now() >= deadline { + if Instant::now() >= deadline { return false; } - std::thread::sleep(std::time::Duration::from_millis(10)); + thread::sleep(Duration::from_millis(10)); } } diff --git a/src/process_broker/client.rs b/src/process_broker/client.rs index a73d1d0f9..0cfdd0a8c 100644 --- a/src/process_broker/client.rs +++ b/src/process_broker/client.rs @@ -1,12 +1,18 @@ use std::collections::VecDeque; use std::ffi::{OsStr, OsString}; +use std::io; +use std::io::ErrorKind; use std::os::fd::{AsRawFd, OwnedFd, RawFd}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, OnceLock, TryLockError, Weak}; +#[cfg(test)] +use std::thread::JoinHandle; use std::time::Duration; use anyhow::{Context, Result, anyhow, bail}; +use crate::daemon::protocol_v2::ProtocolId; + use super::error::{BrokerError, BrokerErrorKind}; use super::execution::supports_retained_publication; use super::transport::{ @@ -27,7 +33,7 @@ pub(super) struct BrokerInner { pub(super) exchange_lock: Mutex<()>, pub(super) healthy: AtomicBool, #[cfg(test)] - pub(super) test_thread: Mutex>>, + pub(super) test_thread: Mutex>>, } #[derive(Clone, Debug)] @@ -96,14 +102,15 @@ pub(crate) fn start_for_runtime() -> Result { let broker = super::bootstrap::start()?; *active_slot() .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) = Arc::downgrade(&broker.inner); + .expect("the active broker slot is never locked across a panic") = + Arc::downgrade(&broker.inner); Ok(ProcessBrokerGuard { broker }) } pub(crate) fn current() -> Result { let inner = active_slot() .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .expect("the active broker slot is never locked across a panic") .upgrade() .ok_or_else(|| anyhow!("runtime process broker is not active"))?; Ok(ProcessBroker { inner }) @@ -134,7 +141,7 @@ impl Drop for ProcessBrokerGuard { .inner .test_thread .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .expect("the test broker thread slot is never locked across a panic") .take() { let _ = thread.join(); @@ -146,7 +153,7 @@ impl Drop for ProcessBrokerGuard { let mut slot = active_slot() .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("the active broker slot is never locked across a panic"); if slot .upgrade() .is_some_and(|active| Arc::ptr_eq(&active, &self.broker.inner)) @@ -156,7 +163,7 @@ impl Drop for ProcessBrokerGuard { } } -fn signal_shutdown(descriptor: RawFd) -> std::io::Result<()> { +fn signal_shutdown(descriptor: RawFd) -> io::Result<()> { let byte = [GRACEFUL_SHUTDOWN_BYTE]; loop { // SAFETY: descriptor is the broker shutdown socket and byte is readable. @@ -171,14 +178,13 @@ fn signal_shutdown(descriptor: RawFd) -> std::io::Result<()> { if written == 1 { return Ok(()); } - if written < 0 && std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted - { + if written < 0 && io::Error::last_os_error().kind() == ErrorKind::Interrupted { continue; } return Err(if written < 0 { - std::io::Error::last_os_error() + io::Error::last_os_error() } else { - std::io::Error::new(std::io::ErrorKind::WriteZero, "short broker shutdown write") + io::Error::new(ErrorKind::WriteZero, "short broker shutdown write") }); } } @@ -206,24 +212,33 @@ impl ProcessBroker { } fn lock_exchange(&self, wait: ExchangeWait) -> Result> { - match wait { - ExchangeWait::Queue => Ok(self - .inner - .exchange_lock - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner())), + let locked = match wait { + ExchangeWait::Queue => self.inner.exchange_lock.lock(), // One attempt, never a timed retry: sleeping here would be the very // stall this policy exists to avoid. ExchangeWait::Immediate => match self.inner.exchange_lock.try_lock() { Ok(guard) => Ok(guard), - Err(TryLockError::Poisoned(poisoned)) => Ok(poisoned.into_inner()), - Err(TryLockError::WouldBlock) => Err(BrokerError::new( - BrokerErrorKind::Busy, - "process broker is busy running another helper", - ) - .into()), + Err(TryLockError::Poisoned(poisoned)) => Err(poisoned), + Err(TryLockError::WouldBlock) => { + return Err(BrokerError::new( + BrokerErrorKind::Busy, + "process broker is busy running another helper", + ) + .into()); + } }, - } + }; + + // An exchange that panicked may have left the stream mid-message, so + // the broker is retired as a failed exchange retires it. + locked.map_err(|_| { + self.inner.healthy.store(false, Ordering::Release); + BrokerError::new( + BrokerErrorKind::Transport, + "process broker exchange panicked earlier; the broker is no longer used", + ) + .into() + }) } pub(super) fn request(&self, operation: BrokerOperation) -> Result { @@ -241,7 +256,7 @@ impl ProcessBroker { descriptors: &[RawFd], wait: ExchangeWait, ) -> Result<(BrokerOutcome, Vec)> { - let request_id = crate::daemon::protocol_v2::ProtocolId::generate()?.to_string(); + let request_id = ProtocolId::generate()?.to_string(); let exchange_timeout = broker_exchange_timeout(&operation); let packet = serde_json::to_vec(&BrokerRequest { token: self.inner.token.clone(), diff --git a/src/process_broker/error.rs b/src/process_broker/error.rs index 99783e85a..e30e7cc8c 100644 --- a/src/process_broker/error.rs +++ b/src/process_broker/error.rs @@ -1,4 +1,5 @@ -use std::fmt; +use std::io::ErrorKind; +use std::{error, fmt, io}; use serde::{Deserialize, Serialize}; @@ -28,10 +29,10 @@ impl BrokerError { } } - pub(super) fn spawn(error: std::io::Error) -> anyhow::Error { + pub(super) fn spawn(error: io::Error) -> anyhow::Error { let kind = match error.kind() { - std::io::ErrorKind::NotFound => BrokerErrorKind::MissingExecutable, - std::io::ErrorKind::PermissionDenied => BrokerErrorKind::Denied, + ErrorKind::NotFound => BrokerErrorKind::MissingExecutable, + ErrorKind::PermissionDenied => BrokerErrorKind::Denied, _ => BrokerErrorKind::Rejected, }; anyhow::Error::new(error).context(Self::new(kind, "broker helper spawn failed")) @@ -44,7 +45,7 @@ impl fmt::Display for BrokerError { } } -impl std::error::Error for BrokerError {} +impl error::Error for BrokerError {} pub(crate) fn error_kind(error: &anyhow::Error) -> Option { error.downcast_ref::().map(|error| error.kind) diff --git a/src/process_broker/execution.rs b/src/process_broker/execution.rs index ab1052d9a..2a7c3029e 100644 --- a/src/process_broker/execution.rs +++ b/src/process_broker/execution.rs @@ -1,13 +1,18 @@ +use std::collections::BTreeMap; use std::io::{self, Read, Write}; -use std::os::fd::AsRawFd; +use std::mem::MaybeUninit; +use std::os::fd::{AsRawFd, RawFd}; use std::os::unix::process::CommandExt; -use std::process::{Child, Command, ExitStatus, Stdio}; +use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; +use std::{mem, thread}; use anyhow::{Context, Result, anyhow}; +use crate::daemon::protocol_v2::{BootClock, BootDeadline}; + use super::transport::shutdown_requested; use super::wire::{HelperKind, MAX_STDERR_BYTES, OutputMode, STDOUT_CAP_EXCEEDED}; @@ -50,16 +55,14 @@ pub(super) fn supports_retained_publication(kind: HelperKind) -> bool { false } -pub(super) fn terminate_owned_children( - children: &mut std::collections::BTreeMap, -) { - for (_, mut child) in std::mem::take(children) { +pub(super) fn terminate_owned_children(children: &mut BTreeMap) { + for (_, mut child) in mem::take(children) { kill_child_process_group(&mut child); let _ = child.wait(); } } -pub(super) fn kill_child_process_group(child: &mut std::process::Child) { +pub(super) fn kill_child_process_group(child: &mut Child) { let pid = child.id(); if let Ok(group) = i32::try_from(pid) { // SAFETY: broker children use a fresh process group equal to their PID. @@ -141,8 +144,8 @@ impl Drop for OwnedProcess { } } -fn child_status_unreaped(child: &std::process::Child) -> io::Result> { - let mut info = std::mem::MaybeUninit::::zeroed(); +fn child_status_unreaped(child: &Child) -> io::Result> { + let mut info = MaybeUninit::::zeroed(); // WNOWAIT pins the zombie leader and therefore its process-group identity. // SAFETY: info is writable and child is owned by this broker. if unsafe { @@ -158,6 +161,9 @@ fn child_status_unreaped(child: &std::process::Child) -> io::Result> } // SAFETY: waitid initialized info after success. let info = unsafe { info.assume_init() }; + // SAFETY: info was zeroed before the call and waitid only writes a SIGCHLD + // record into it, so the pid field it reads is initialized; it stays zero + // when WNOHANG found no waitable child. if unsafe { info.si_pid() } == 0 { return Ok(None); } @@ -180,13 +186,12 @@ pub(super) fn publish_bounded( mut command: Command, input: Vec, timeout: Duration, - shutdown_fd: std::os::fd::RawFd, + shutdown_fd: RawFd, ) -> Result { if shutdown_requested(shutdown_fd)? { return Err(anyhow!("broker publication cancelled during shutdown")); } - let deadline = crate::daemon::protocol_v2::BootClock::now()? - .checked_add(timeout.max(Duration::from_millis(1)))?; + let deadline = BootClock::now()?.checked_add(timeout.max(Duration::from_millis(1)))?; command .process_group(0) .stdin(Stdio::piped()) @@ -199,7 +204,7 @@ pub(super) fn publish_bounded( .context("broker publication helper spawn failed")?, ); let stdin = child.child_mut().stdin.take(); - let stdin_writer = std::thread::spawn(move || match stdin { + let stdin_writer = thread::spawn(move || match stdin { Some(stdin) => write_input_until(stdin, &input, deadline, shutdown_fd), None => Ok(()), }); @@ -225,7 +230,7 @@ pub(super) fn publish_bounded( retained: Some(child.into_child()), }); } - if crate::daemon::protocol_v2::BootClock::now()? >= deadline { + if BootClock::now()? >= deadline { child.terminate(); let status = status_code(child.wait()?); let _ = stdin_writer.join(); @@ -241,19 +246,21 @@ pub(super) fn publish_bounded( let _ = stdin_writer.join(); return Err(anyhow!("broker publication cancelled during shutdown")); } - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } } fn write_input_until( - mut stdin: std::process::ChildStdin, + mut stdin: ChildStdin, input: &[u8], - deadline: crate::daemon::protocol_v2::BootDeadline, - shutdown_fd: std::os::fd::RawFd, + deadline: BootDeadline, + shutdown_fd: RawFd, ) -> io::Result<()> { let descriptor = stdin.as_raw_fd(); // SAFETY: fcntl reads and updates descriptor-local flags. let flags = unsafe { libc::fcntl(descriptor, libc::F_GETFL) }; + // SAFETY: `stdin` keeps the descriptor open, and F_SETFL only adds + // O_NONBLOCK to the flags just read from it. if flags < 0 || unsafe { libc::fcntl(descriptor, libc::F_SETFL, flags | libc::O_NONBLOCK) } != 0 { return Err(io::Error::last_os_error()); @@ -271,15 +278,13 @@ fn write_input_until( Ok(written) => offset += written, Err(error) if error.kind() == io::ErrorKind::Interrupted => continue, Err(error) if error.kind() == io::ErrorKind::WouldBlock => { - if crate::daemon::protocol_v2::BootClock::now().map_err(io::Error::other)? - >= deadline - { + if BootClock::now().map_err(io::Error::other)? >= deadline { return Err(io::Error::new( io::ErrorKind::TimedOut, "publication input deadline expired", )); } - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } Err(error) => return Err(error), } @@ -293,13 +298,12 @@ pub(super) fn run_bounded( timeout: Duration, output_cap: usize, output_mode: OutputMode, - shutdown_fd: std::os::fd::RawFd, + shutdown_fd: RawFd, ) -> Result { if shutdown_requested(shutdown_fd)? { return Err(anyhow!("broker helper cancelled during shutdown")); } - let deadline = crate::daemon::protocol_v2::BootClock::now()? - .checked_add(timeout.max(Duration::from_millis(1)))?; + let deadline = BootClock::now()?.checked_add(timeout.max(Duration::from_millis(1)))?; command.process_group(0); command .stdin(Stdio::piped()) @@ -324,7 +328,7 @@ pub(super) fn run_bounded( let stdout_reader = { let limit_reached = Arc::clone(&stdout_limit_reached); let stdout = AbandonablePipe::new(stdout, &abandon_pipes); - std::thread::spawn(move || match output_mode { + thread::spawn(move || match output_mode { OutputMode::Complete => read_capped(stdout, output_cap, &limit_reached), OutputMode::Prefix => read_prefix(stdout, output_cap, &limit_reached), }) @@ -332,13 +336,13 @@ pub(super) fn run_bounded( let stderr_reader = { let overflow = Arc::clone(&stderr_overflow); let stderr = AbandonablePipe::new(stderr, &abandon_pipes); - std::thread::spawn(move || read_capped(stderr, output_cap.min(MAX_STDERR_BYTES), &overflow)) + thread::spawn(move || read_capped(stderr, output_cap.min(MAX_STDERR_BYTES), &overflow)) }; let stdin_writer = { let stdin = stdin .take() .map(|stdin| AbandonablePipe::new(stdin, &abandon_pipes)); - std::thread::spawn(move || { + thread::spawn(move || { stdin.and_then(|mut stdin| { stdin .write_all(&input) @@ -352,7 +356,7 @@ pub(super) fn run_bounded( child.terminate(); break (child.wait()?, false, false); } - if crate::daemon::protocol_v2::BootClock::now()? >= deadline { + if BootClock::now()? >= deadline { child.terminate(); break (child.wait()?, true, false); } @@ -364,20 +368,19 @@ pub(super) fn run_bounded( child.terminate(); break (child.wait()?, false, cancelled); } - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); }; - let drain_deadline = - crate::daemon::protocol_v2::BootClock::now()?.checked_add(PIPE_DRAIN_GRACE)?; + let drain_deadline = BootClock::now()?.checked_add(PIPE_DRAIN_GRACE)?; while !(stdout_reader.is_finished() && stderr_reader.is_finished() && stdin_writer.is_finished()) { - if crate::daemon::protocol_v2::BootClock::now()? >= drain_deadline { + if BootClock::now()? >= drain_deadline { abandon_pipes.store(true, Ordering::Release); break; } - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } let stdout = stdout_reader diff --git a/src/process_broker/manifest.rs b/src/process_broker/manifest.rs index 0bd1a66bb..666e76465 100644 --- a/src/process_broker/manifest.rs +++ b/src/process_broker/manifest.rs @@ -1,8 +1,12 @@ +use std::env; use std::ffi::OsStr; +use std::path::Path; use std::process::Command; use anyhow::{Result, anyhow, bail}; +use crate::{env_vars, update_check}; + use super::wire::{HelperKind, MAX_ARGUMENT_BYTES, MAX_ARGUMENTS, MAX_INPUT_BYTES, OsWire}; pub(super) fn input_cap(kind: HelperKind) -> usize { @@ -39,7 +43,7 @@ pub(super) fn validate( bail!("broker request exceeds manifest bounds"); } let program = program.clone().into_os(); - let basename = std::path::Path::new(&program) + let basename = Path::new(&program) .file_name() .and_then(OsStr::to_str) .ok_or_else(|| anyhow!("broker program has no UTF-8 basename"))? @@ -63,11 +67,10 @@ pub(super) fn validate( HelperKind::SessionKdialog => basename == "kdialog", HelperKind::Gsettings => basename == "gsettings", HelperKind::Systemctl => basename == "systemctl", - HelperKind::Configurator => std::env::var_os(crate::env_vars::CONFIGURATOR_ENV) - .map_or_else( - || basename.contains("configurator"), - |configured| configured == program, - ), + HelperKind::Configurator => env::var_os(env_vars::CONFIGURATOR_ENV).map_or_else( + || basename.contains("configurator"), + |configured| configured == program, + ), HelperKind::DesktopOpen => matches!(basename.as_str(), "xdg-open" | "open"), HelperKind::UpdateFetcher => matches!(basename.as_str(), "curl" | "wget"), #[cfg(test)] @@ -89,7 +92,7 @@ pub(super) fn validate( } validate_arguments(kind, &basename, arguments)?; for (name, _) in environment { - let name = std::str::from_utf8(&name.0)?; + let name = str::from_utf8(&name.0)?; if !matches!( name, "WAYSCRIBER_NO_DETACH" @@ -178,10 +181,8 @@ fn validate_arguments(kind: HelperKind, basename: &str, arguments: &[OsWire]) -> if target.0.starts_with(b"-") { bail!("desktop opener target must not be an option"); } - match std::str::from_utf8(&target.0) { - Ok(target) - if looks_like_uri(target) && !crate::update_check::is_trusted_url(target) => - { + match str::from_utf8(&target.0) { + Ok(target) if looks_like_uri(target) && !update_check::is_trusted_url(target) => { bail!("desktop opener URL is not a trusted Wayscriber HTTPS URL"); } // xdg-open parses schemes bytewise. Undecodable targets that @@ -282,14 +283,14 @@ fn looks_like_uri_bytes(value: &[u8]) -> bool { /// and curl. A wayscriber started anywhere in those trees then took itself /// for a daemon child, or took the daemon's `--language`. const INTERNAL_PROCESS_MARKERS: [&str; 8] = [ - crate::env_vars::OVERLAY_CHILD_GENERATION_ENV, - crate::env_vars::DETACHED_ENV, + env_vars::OVERLAY_CHILD_GENERATION_ENV, + env_vars::DETACHED_ENV, crate::RESUME_SESSION_ENV, - crate::env_vars::DAEMON_WATCHDOG_FD_ENV, - crate::env_vars::OVERLAY_SESSION_REPORTS_ENV, - crate::env_vars::OVERLAY_HOME_SESSION_ENV, - crate::env_vars::OVERLAY_PREFERRED_SESSION_ENV, - crate::env_vars::OVERLAY_LANGUAGE_ENV, + env_vars::DAEMON_WATCHDOG_FD_ENV, + env_vars::OVERLAY_SESSION_REPORTS_ENV, + env_vars::OVERLAY_HOME_SESSION_ENV, + env_vars::OVERLAY_PREFERRED_SESSION_ENV, + env_vars::OVERLAY_LANGUAGE_ENV, ]; /// Whether `kind` relaunches wayscriber itself, the only helpers that keep diff --git a/src/process_broker/server.rs b/src/process_broker/server.rs index b70d81522..bde855b12 100644 --- a/src/process_broker/server.rs +++ b/src/process_broker/server.rs @@ -1,12 +1,16 @@ use std::collections::{BTreeMap, VecDeque}; -use std::io; use std::os::fd::{AsRawFd, OwnedFd, RawFd}; use std::os::unix::process::CommandExt; -use std::process::{ExitCode, Stdio}; +use std::process::{Child, ExitCode, Stdio}; +use std::thread::Builder; use std::time::Duration; +use std::{env, io, mem}; use anyhow::{Context, Result, anyhow, bail}; +use crate::daemon::protocol_v2::ProtocolId; +use crate::env_vars; + use super::execution::{ OwnedProcess, kill_child_process_group, publish_bounded, run_bounded, status_code, supports_retained_publication, terminate_owned_children, @@ -23,12 +27,12 @@ use super::wire::{ }; pub(crate) fn run_internal_broker_if_requested() -> Option { - let fd = std::env::var(BROKER_FD_ENV).ok()?.parse::().ok()?; - let shutdown_fd = std::env::var(BROKER_SHUTDOWN_FD_ENV) + let fd = env::var(BROKER_FD_ENV).ok()?.parse::().ok()?; + let shutdown_fd = env::var(BROKER_SHUTDOWN_FD_ENV) .ok()? .parse::() .ok()?; - let token = std::env::var(BROKER_TOKEN_ENV).ok()?; + let token = env::var(BROKER_TOKEN_ENV).ok()?; if fd != BROKER_FD || shutdown_fd != BROKER_SHUTDOWN_FD || !canonical_lower_hex(&token, 64) @@ -46,9 +50,9 @@ pub(crate) fn run_internal_broker_if_requested() -> Option { } // SAFETY: this entry runs before application worker threads start. unsafe { - std::env::remove_var(BROKER_FD_ENV); - std::env::remove_var(BROKER_SHUTDOWN_FD_ENV); - std::env::remove_var(BROKER_TOKEN_ENV); + env::remove_var(BROKER_FD_ENV); + env::remove_var(BROKER_SHUTDOWN_FD_ENV); + env::remove_var(BROKER_TOKEN_ENV); } Some(match broker_loop(fd, shutdown_fd, &token) { Ok(()) => ExitCode::SUCCESS, @@ -58,7 +62,7 @@ pub(crate) fn run_internal_broker_if_requested() -> Option { fn validate_broker_socket(fd: RawFd) -> io::Result<()> { let mut socket_type = 0_i32; - let mut length = std::mem::size_of::() as libc::socklen_t; + let mut length = mem::size_of::() as libc::socklen_t; // SAFETY: the output slots are valid for getsockopt. if unsafe { libc::getsockopt( @@ -199,13 +203,13 @@ pub(super) fn run_loop_for_test(socket: RawFd, shutdown_fd: RawFd, token: &str) #[derive(Default)] struct BrokerOwnership { - children: BTreeMap, + children: BTreeMap, /// At most one regular clipboard provider is current for this runtime. - retained_publication: Option, + retained_publication: Option, } impl BrokerOwnership { - fn replace_retained_publication(&mut self, child: std::process::Child) { + fn replace_retained_publication(&mut self, child: Child) { if let Some(mut previous) = self.retained_publication.replace(child) { kill_child_process_group(&mut previous); let _ = previous.wait(); @@ -404,7 +408,7 @@ struct SpawnRequest { fn spawn_helper( request: SpawnRequest, descriptors: &mut VecDeque, - children: &mut BTreeMap, + children: &mut BTreeMap, shutdown_fd: RawFd, ) -> Result { let SpawnRequest { @@ -440,7 +444,7 @@ fn spawn_helper( .ok_or_else(|| anyhow!("checked watchdog descriptor disappeared"))?; set_cloexec(&descriptor, false)?; command.env( - crate::env_vars::DAEMON_WATCHDOG_FD_ENV, + env_vars::DAEMON_WATCHDOG_FD_ENV, descriptor.as_raw_fd().to_string(), ); Some(descriptor) @@ -457,7 +461,7 @@ fn spawn_helper( .stdout(Stdio::null()) .stderr(Stdio::null()); let handle = loop { - let candidate = crate::daemon::protocol_v2::ProtocolId::generate()?.to_string(); + let candidate = ProtocolId::generate()?.to_string(); if !children.contains_key(&candidate) { break candidate; } @@ -480,7 +484,7 @@ fn spawn_helper( children.insert(handle.clone(), child.into_child()); } HelperLifetime::DetachedAfterExec => { - std::thread::Builder::new() + Builder::new() .name(format!("wayscriber-detached-reaper-{pid}")) .spawn(move || { let mut child = child; @@ -510,6 +514,8 @@ fn set_cloexec(descriptor: &OwnedFd, enabled: bool) -> Result<()> { } else { current & !libc::FD_CLOEXEC }; + // SAFETY: `descriptor` is borrowed open for the call, and `updated` only + // changes FD_CLOEXEC in the flags just read from it. if unsafe { libc::fcntl(descriptor.as_raw_fd(), libc::F_SETFD, updated) } != 0 { return Err(io::Error::last_os_error()).context("failed to update descriptor flags"); } @@ -543,13 +549,15 @@ fn canonical_lower_hex(value: &str, length: usize) -> bool { #[cfg(test)] mod tests { + use std::process::Command; + use super::*; use std::os::fd::FromRawFd; fn test_socket_pair() -> (OwnedFd, OwnedFd) { let mut sockets = [0; 2]; - // SAFETY: sockets has room for both returned descriptors. assert_eq!( + // SAFETY: sockets has room for both returned descriptors. unsafe { libc::socketpair( libc::AF_UNIX, @@ -589,8 +597,8 @@ mod tests { let (_control_writer, control_reader) = test_socket_pair(); let (shutdown_writer, shutdown_reader) = test_socket_pair(); let signal = [super::super::transport::GRACEFUL_SHUTDOWN_BYTE]; - // SAFETY: both the descriptor and one-byte buffer are valid. assert_eq!( + // SAFETY: both the descriptor and one-byte buffer are valid. unsafe { libc::send( shutdown_writer.as_raw_fd(), @@ -614,8 +622,8 @@ mod tests { let (_control_writer, control_reader) = test_socket_pair(); let (shutdown_writer, shutdown_reader) = test_socket_pair(); let invalid = [2_u8]; - // SAFETY: both the descriptor and one-byte buffer are valid. assert_eq!( + // SAFETY: both the descriptor and one-byte buffer are valid. unsafe { libc::send( shutdown_writer.as_raw_fd(), @@ -641,8 +649,8 @@ mod tests { let (_control_writer, control_reader) = test_socket_pair(); let (shutdown_writer, shutdown_reader) = test_socket_pair(); let invalid = [super::super::transport::GRACEFUL_SHUTDOWN_BYTE; 2]; - // SAFETY: both the descriptor and two-byte buffer are valid. assert_eq!( + // SAFETY: both the descriptor and two-byte buffer are valid. unsafe { libc::send( shutdown_writer.as_raw_fd(), @@ -665,7 +673,7 @@ mod tests { #[test] fn abnormal_ownership_drop_kills_a_retained_provider() { - let mut command = std::process::Command::new("sleep"); + let mut command = Command::new("sleep"); command.arg("30").process_group(0); let child = command.spawn().unwrap(); let pid = i32::try_from(child.id()).unwrap(); diff --git a/src/process_broker/tests.rs b/src/process_broker/tests.rs index 0674e20b5..acd5cd150 100644 --- a/src/process_broker/tests.rs +++ b/src/process_broker/tests.rs @@ -1,7 +1,15 @@ use std::ffi::OsStr; use std::os::fd::AsRawFd; use std::os::unix::process::CommandExt; +use std::path::Path; +use std::process::Command; +use std::sync::mpsc; use std::time::{Duration, Instant}; +use std::{env, fs, iter, thread}; + +use crate::daemon::protocol_v2; +use crate::test_fake_helper::Role; +use crate::{env_vars, process_broker, test_env, test_fake_helper, test_temp}; use super::*; @@ -12,15 +20,11 @@ fn wire_arguments(arguments: &[&str]) -> Vec { .collect() } -fn release_test_provider( - release_path: &std::path::Path, - proof_path: &std::path::Path, - provider_pid: i32, -) { - std::fs::write(release_path, b"release").unwrap(); +fn release_test_provider(release_path: &Path, proof_path: &Path, provider_pid: i32) { + fs::write(release_path, b"release").unwrap(); let deadline = Instant::now() + Duration::from_secs(1); loop { - if std::fs::read(proof_path).is_ok_and(|bytes| bytes == b"survived") { + if fs::read(proof_path).is_ok_and(|bytes| bytes == b"survived") { return; } if Instant::now() >= deadline { @@ -30,48 +34,43 @@ fn release_test_provider( } panic!("successful provider could not act after normal broker shutdown"); } - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } } #[test] fn configurator_manifest_preserves_arbitrary_explicit_override_name() { - let temp = crate::test_temp::tempdir().unwrap(); - let configured = crate::test_fake_helper::link(temp.path(), "open-wayscriber-settings"); + let temp = test_temp::tempdir().unwrap(); + let configured = test_fake_helper::link(temp.path(), "open-wayscriber-settings"); let output = temp.path().join("unused"); - let mut variables = vec![( - crate::env_vars::CONFIGURATOR_ENV, - Some(configured.as_os_str()), - )]; - variables.extend(crate::test_fake_helper::environment( + let mut variables = vec![(env_vars::CONFIGURATOR_ENV, Some(configured.as_os_str()))]; + variables.extend(test_fake_helper::environment( &configured, - crate::test_fake_helper::Role::Exit, + Role::Exit, &output, )); - let (result, broker_result, unexpected_result) = - crate::test_env::with_env_vars(&variables, || { - let program = super::wire::OsWire::from_os(configured.as_os_str()).unwrap(); - let result = - super::manifest::validate(HelperKind::Configurator, &program, &[], &[], &[]); - let broker_result = (|| -> anyhow::Result<()> { - let guard = start_for_runtime()?; - guard.broker().spawn( - HelperKind::Configurator, - HelperLifetime::DetachedAfterExec, - configured.as_os_str(), - std::iter::empty::<&OsStr>(), - Vec::new(), - )?; - Ok(()) - })(); - let unexpected = - super::wire::OsWire::from_os(OsStr::new("/tmp/unrelated-program")).unwrap(); - let unexpected_result = - super::manifest::validate(HelperKind::Configurator, &unexpected, &[], &[], &[]); - - (result, broker_result, unexpected_result) - }); + let (result, broker_result, unexpected_result) = test_env::with_env_vars(&variables, || { + let program = super::wire::OsWire::from_os(configured.as_os_str()).unwrap(); + let result = super::manifest::validate(HelperKind::Configurator, &program, &[], &[], &[]); + let broker_result = (|| -> anyhow::Result<()> { + let guard = start_for_runtime()?; + guard.broker().spawn( + HelperKind::Configurator, + HelperLifetime::DetachedAfterExec, + configured.as_os_str(), + iter::empty::<&OsStr>(), + Vec::new(), + )?; + Ok(()) + })(); + let unexpected = + super::wire::OsWire::from_os(OsStr::new("/tmp/unrelated-program")).unwrap(); + let unexpected_result = + super::manifest::validate(HelperKind::Configurator, &unexpected, &[], &[], &[]); + + (result, broker_result, unexpected_result) + }); result.unwrap(); broker_result.unwrap(); @@ -444,9 +443,9 @@ fn tesseract_reads_the_complete_recognized_output_rather_than_a_prefix() { fn only_the_callback_thread_spawn_declines_to_wait_for_the_transport() { let guard = start_for_runtime().unwrap(); let broker = guard.broker().clone(); - let (running_tx, running_rx) = std::sync::mpsc::channel(); + let (running_tx, running_rx) = mpsc::channel(); - let long_helper = std::thread::spawn(move || { + let long_helper = thread::spawn(move || { running_tx.send(()).unwrap(); broker.run( HelperKind::TestSleep, @@ -459,7 +458,7 @@ fn only_the_callback_thread_spawn_declines_to_wait_for_the_transport() { }); running_rx.recv_timeout(Duration::from_secs(5)).unwrap(); // The helper thread has to hold the exchange before the policy is testable. - std::thread::sleep(Duration::from_millis(200)); + thread::sleep(Duration::from_millis(200)); let started = Instant::now(); let declined = guard.broker().try_spawn( @@ -473,8 +472,7 @@ fn only_the_callback_thread_spawn_declines_to_wait_for_the_transport() { let error = declined.expect_err("try_spawn must not queue behind the long helper"); assert!( - crate::process_broker::error_kind(&error) - == Some(crate::process_broker::BrokerErrorKind::Busy), + process_broker::error_kind(&error) == Some(process_broker::BrokerErrorKind::Busy), "unexpected try_spawn failure: {error:#}" ); assert!( @@ -529,13 +527,13 @@ fn prelock_broker_runs_bounded_helpers_and_owns_reaping() { let deadline = Instant::now() + Duration::from_secs(1); while child.try_wait().unwrap().is_none() { assert!(Instant::now() < deadline); - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } } #[test] fn process_group_guard_cleans_up_before_ownership_transfer() { - let mut command = std::process::Command::new("sleep"); + let mut command = Command::new("sleep"); command.arg("30").process_group(0); let child = super::execution::OwnedProcess::process_group(command.spawn().unwrap()); let pid = i32::try_from(child.id()).unwrap(); @@ -548,16 +546,12 @@ fn process_group_guard_cleans_up_before_ownership_transfer() { #[test] fn initial_detach_child_remains_eligible_to_create_a_session() { - let temp = crate::test_temp::tempdir().unwrap(); - let helper = crate::test_fake_helper::link(temp.path(), "wayscriber-detach-probe"); + let temp = test_temp::tempdir().unwrap(); + let helper = test_fake_helper::link(temp.path(), "wayscriber-detach-probe"); let proof = temp.path().join("detach-state"); - let variables = crate::test_fake_helper::environment( - &helper, - crate::test_fake_helper::Role::ReportProcessGroup, - &proof, - ); + let variables = test_fake_helper::environment(&helper, Role::ReportProcessGroup, &proof); - let observed = crate::test_env::with_env_vars(&variables, || { + let observed = test_env::with_env_vars(&variables, || { let guard = start_for_runtime().unwrap(); let _child = guard .broker() @@ -565,18 +559,18 @@ fn initial_detach_child_remains_eligible_to_create_a_session() { HelperKind::InitialDetach, HelperLifetime::DetachedAfterExec, helper.as_os_str(), - std::iter::empty::<&OsStr>(), + iter::empty::<&OsStr>(), Vec::new(), ) .unwrap(); let deadline = Instant::now() + Duration::from_secs(5); loop { - if let Ok(value) = std::fs::read_to_string(&proof) { + if let Ok(value) = fs::read_to_string(&proof) { break value; } assert!(Instant::now() < deadline, "detach probe did not complete"); - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } }); @@ -624,7 +618,7 @@ fn broker_transfers_large_input_and_output_through_sealed_memfds() { .run( HelperKind::TestCat, OsStr::new("cat"), - std::iter::empty::<&OsStr>(), + iter::empty::<&OsStr>(), input.clone(), Duration::from_secs(2), input.len(), @@ -716,7 +710,7 @@ fn broker_prefix_read_returns_the_requested_prefix_without_weakening_strict_runs .run_prefix( HelperKind::TestCat, OsStr::new("cat"), - std::iter::empty::<&OsStr>(), + iter::empty::<&OsStr>(), Vec::new(), Duration::from_secs(1), 5, @@ -730,9 +724,9 @@ fn broker_prefix_read_returns_the_requested_prefix_without_weakening_strict_runs fn broker_guard_preempts_an_active_operation_and_kills_its_group() { let guard = start_for_runtime().unwrap(); let broker = guard.broker().clone(); - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let pid_path = temp.path().join("helper.pid"); - let run = std::thread::spawn({ + let run = thread::spawn({ let pid_path = pid_path.clone(); move || { broker.run( @@ -753,9 +747,9 @@ fn broker_guard_preempts_an_active_operation_and_kills_its_group() { let deadline = Instant::now() + Duration::from_secs(1); while !pid_path.exists() { assert!(Instant::now() < deadline, "helper did not start"); - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } - let helper_pid = std::fs::read_to_string(&pid_path) + let helper_pid = fs::read_to_string(&pid_path) .unwrap() .trim() .parse::() @@ -775,7 +769,7 @@ fn broker_guard_preempts_an_active_operation_and_kills_its_group() { #[test] fn owned_child_inherits_daemon_pidfd_without_leaking_broker_copy() { let guard = start_for_runtime().unwrap(); - let watchdog = crate::daemon::protocol_v2::open_daemon_watchdog().unwrap(); + let watchdog = protocol_v2::open_daemon_watchdog().unwrap(); let child = guard .broker() .spawn_with_watchdog( @@ -787,10 +781,10 @@ fn owned_child_inherits_daemon_pidfd_without_leaking_broker_copy() { watchdog.as_raw_fd(), ) .unwrap(); - let inherited_pidfd = std::fs::read_dir(format!("/proc/{}/fd", child.id())) + let inherited_pidfd = fs::read_dir(format!("/proc/{}/fd", child.id())) .unwrap() .filter_map(Result::ok) - .filter_map(|entry| std::fs::read_link(entry.path()).ok()) + .filter_map(|entry| fs::read_link(entry.path()).ok()) .any(|target| target.as_os_str() == "anon_inode:[pidfd]"); assert!(inherited_pidfd, "owned child should retain daemon pidfd"); child.kill_wait().unwrap(); @@ -799,16 +793,16 @@ fn owned_child_inherits_daemon_pidfd_without_leaking_broker_copy() { #[test] fn helpers_do_not_inherit_the_internal_process_markers() { const MARKERS: [&str; 4] = [ - crate::env_vars::OVERLAY_CHILD_GENERATION_ENV, - crate::env_vars::DETACHED_ENV, + env_vars::OVERLAY_CHILD_GENERATION_ENV, + env_vars::DETACHED_ENV, crate::RESUME_SESSION_ENV, - crate::env_vars::OVERLAY_LANGUAGE_ENV, + env_vars::OVERLAY_LANGUAGE_ENV, ]; - let _environment = crate::test_env::lock(); - let previous = MARKERS.map(std::env::var_os); + let _environment = test_env::lock(); + let previous = MARKERS.map(env::var_os); for marker in MARKERS { // SAFETY: serialized by the test environment mutex held above. - unsafe { std::env::set_var(marker, "inherited") }; + unsafe { env::set_var(marker, "inherited") }; } let guard = start_for_runtime().unwrap(); @@ -831,8 +825,8 @@ fn helpers_do_not_inherit_the_internal_process_markers() { // SAFETY: serialized by the test environment mutex held above. unsafe { match value { - Some(value) => std::env::set_var(marker, value), - None => std::env::remove_var(marker), + Some(value) => env::set_var(marker, value), + None => env::remove_var(marker), } } } @@ -861,7 +855,7 @@ fn operation_bound_run_terminates_descendants_that_retain_pipes() { assert!(!output.timed_out); assert!(started.elapsed() < Duration::from_secs(1)); assert!( - std::str::from_utf8(&output.stdout) + str::from_utf8(&output.stdout) .unwrap() .trim() .parse::() @@ -872,7 +866,7 @@ fn operation_bound_run_terminates_descendants_that_retain_pipes() { #[test] fn a_descendant_outside_the_helper_group_cannot_hold_the_broker_on_its_pipes() { let guard = start_for_runtime().unwrap(); - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let pid_path = temp.path().join("descendant.pid"); // The helper waits until the descendant has written its pid, which it // does only after setsid(). Exiting sooner lets the group kill reach the @@ -893,7 +887,7 @@ fn a_descendant_outside_the_helper_group_cannot_hold_the_broker_on_its_pipes() { 1024, ); let elapsed = started.elapsed(); - if let Some(pid) = std::fs::read_to_string(&pid_path) + if let Some(pid) = fs::read_to_string(&pid_path) .ok() .and_then(|pid| pid.trim().parse::().ok()) { @@ -926,7 +920,7 @@ fn a_descendant_outside_the_helper_group_cannot_hold_the_broker_on_its_pipes() { #[test] fn normal_broker_shutdown_releases_successful_provider_descendant() { let guard = start_for_runtime().unwrap(); - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let release_path = temp.path().join("release-provider"); let proof_path = temp.path().join("provider-survived"); let pid_path = temp.path().join("provider.pid"); @@ -951,7 +945,7 @@ fn normal_broker_shutdown_releases_successful_provider_descendant() { .unwrap(); assert_eq!(output.status, 0); assert!(!output.timed_out); - let provider_pid = std::fs::read_to_string(pid_path) + let provider_pid = fs::read_to_string(pid_path) .unwrap() .trim() .parse::() @@ -967,7 +961,7 @@ fn normal_broker_shutdown_releases_successful_provider_descendant() { #[test] fn shutdown_channel_peer_loss_kills_retained_provider() { let guard = start_for_runtime().unwrap(); - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let pid_path = temp.path().join("provider.pid"); let output = guard .broker() @@ -985,7 +979,7 @@ fn shutdown_channel_peer_loss_kills_retained_provider() { ) .unwrap(); assert_eq!(output.status, 0); - let provider_pid = std::fs::read_to_string(pid_path) + let provider_pid = fs::read_to_string(pid_path) .unwrap() .trim() .parse::() @@ -994,8 +988,8 @@ fn shutdown_channel_peer_loss_kills_retained_provider() { assert_eq!(unsafe { libc::kill(provider_pid, 0) }, 0); // Simulate abrupt parent loss without writing the graceful-shutdown packet. - // SAFETY: the descriptor belongs to this test's live broker guard. assert_eq!( + // SAFETY: the descriptor belongs to this test's live broker guard. unsafe { libc::shutdown(guard.broker().inner.shutdown.as_raw_fd(), libc::SHUT_RDWR) }, 0 ); @@ -1014,14 +1008,14 @@ fn shutdown_channel_peer_loss_kills_retained_provider() { } panic!("provider survived abnormal broker channel loss"); } - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } } #[test] fn retained_publication_replacement_disposes_the_previous_provider() { let guard = start_for_runtime().unwrap(); - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let first_pid_path = temp.path().join("first-provider.pid"); let second_pid_path = temp.path().join("second-provider.pid"); let second_release_path = temp.path().join("release-second-provider"); @@ -1063,12 +1057,12 @@ fn retained_publication_replacement_disposes_the_previous_provider() { .unwrap(); assert_eq!(second.status, 0); - let first_pid = std::fs::read_to_string(first_pid_path) + let first_pid = fs::read_to_string(first_pid_path) .unwrap() .trim() .parse::() .unwrap(); - let second_pid = std::fs::read_to_string(second_pid_path) + let second_pid = fs::read_to_string(second_pid_path) .unwrap() .trim() .parse::() @@ -1087,7 +1081,7 @@ fn retained_publication_replacement_disposes_the_previous_provider() { } panic!("replaced publication provider remained alive"); } - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } // SAFETY: signal zero only checks the current test-owned provider. assert_eq!(unsafe { libc::kill(second_pid, 0) }, 0); @@ -1099,7 +1093,7 @@ fn retained_publication_replacement_disposes_the_previous_provider() { #[test] fn failed_publication_replacement_preserves_the_current_provider() { let guard = start_for_runtime().unwrap(); - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current_pid_path = temp.path().join("current-provider.pid"); let failed_pid_path = temp.path().join("failed-provider.pid"); let current_release_path = temp.path().join("release-current-provider"); @@ -1142,12 +1136,12 @@ fn failed_publication_replacement_preserves_the_current_provider() { .unwrap(); assert_eq!(failed.status, 7); - let current_pid = std::fs::read_to_string(current_pid_path) + let current_pid = fs::read_to_string(current_pid_path) .unwrap() .trim() .parse::() .unwrap(); - let failed_pid = std::fs::read_to_string(failed_pid_path) + let failed_pid = fs::read_to_string(failed_pid_path) .unwrap() .trim() .parse::() @@ -1168,7 +1162,7 @@ fn failed_publication_replacement_preserves_the_current_provider() { } panic!("failed replacement provider survived cleanup"); } - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } drop(guard); @@ -1185,7 +1179,7 @@ fn retained_publication_kills_failed_or_input_stalled_provider_groups() { vec![b'x'; 1024 * 1024], ), ] { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let pid_path = temp.path().join("provider.pid"); let result = guard.broker().publish( HelperKind::TestShell, @@ -1199,7 +1193,7 @@ fn retained_publication_kills_failed_or_input_stalled_provider_groups() { input, Duration::from_millis(100), ); - let provider_pid = std::fs::read_to_string(pid_path) + let provider_pid = fs::read_to_string(pid_path) .unwrap() .trim() .parse::() @@ -1219,7 +1213,7 @@ fn retained_publication_kills_failed_or_input_stalled_provider_groups() { Instant::now() < deadline, "failed provider survived cleanup" ); - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } } } @@ -1242,7 +1236,7 @@ fn retained_publication_rejects_incomplete_input_after_successful_exit() { fn broker_shutdown_preempts_retained_publication_stdin_writer() { let guard = start_for_runtime().unwrap(); let broker = guard.broker().clone(); - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let current_pid_path = temp.path().join("current-provider.pid"); let current_release_path = temp.path().join("release-current-provider"); let current_proof_path = temp.path().join("current-provider-survived"); @@ -1267,12 +1261,12 @@ fn broker_shutdown_preempts_retained_publication_stdin_writer() { ) .unwrap(); assert_eq!(current.status, 0); - let current_pid = std::fs::read_to_string(current_pid_path) + let current_pid = fs::read_to_string(current_pid_path) .unwrap() .trim() .parse::() .unwrap(); - let publication = std::thread::spawn({ + let publication = thread::spawn({ let pid_path = pid_path.clone(); move || { broker.publish( @@ -1295,9 +1289,9 @@ fn broker_shutdown_preempts_retained_publication_stdin_writer() { Instant::now() < deadline, "publication helper did not start" ); - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } - let provider_pid = std::fs::read_to_string(pid_path) + let provider_pid = fs::read_to_string(pid_path) .unwrap() .trim() .parse::() @@ -1321,30 +1315,26 @@ fn broker_shutdown_preempts_retained_publication_stdin_writer() { Instant::now() < deadline, "publication provider survived broker shutdown" ); - std::thread::sleep(Duration::from_millis(5)); + thread::sleep(Duration::from_millis(5)); } } #[test] fn wl_copy_publication_accepts_capture_sized_input() { const PUBLICATION_BYTES: usize = 16 * 1024 * 1024 + 1; - let temp = crate::test_temp::tempdir().unwrap(); - let helper = crate::test_fake_helper::link(temp.path(), "wl-copy"); + let temp = test_temp::tempdir().unwrap(); + let helper = test_fake_helper::link(temp.path(), "wl-copy"); let count_path = temp.path().join("published-bytes"); - let variables = crate::test_fake_helper::environment( - &helper, - crate::test_fake_helper::Role::CountInput, - &count_path, - ); + let variables = test_fake_helper::environment(&helper, Role::CountInput, &count_path); - let output = crate::test_env::with_env_vars(&variables, || { + let output = test_env::with_env_vars(&variables, || { let guard = start_for_runtime().unwrap(); guard .broker() .publish( HelperKind::WlCopy, helper.as_os_str(), - std::iter::empty::<&OsStr>(), + iter::empty::<&OsStr>(), vec![b'x'; PUBLICATION_BYTES], Duration::from_secs(30), ) @@ -1354,7 +1344,7 @@ fn wl_copy_publication_accepts_capture_sized_input() { assert_eq!(output.status, 0); assert!(!output.timed_out); assert_eq!( - std::fs::read_to_string(count_path).unwrap().trim(), + fs::read_to_string(count_path).unwrap().trim(), PUBLICATION_BYTES.to_string() ); } @@ -1376,8 +1366,8 @@ fn missing_executable_category_survives_real_broker_transport_and_context() { .context("caller can add diagnostic context"); assert_eq!( - crate::process_broker::error_kind(&error), - Some(crate::process_broker::BrokerErrorKind::MissingExecutable) + process_broker::error_kind(&error), + Some(process_broker::BrokerErrorKind::MissingExecutable) ); assert!(format!("{error:#}").contains("broker helper spawn failed")); } @@ -1400,7 +1390,7 @@ fn rejected_diagnostic_with_missing_file_words_is_not_a_missing_executable() { assert!(format!("{error:#}").contains("No such file")); assert_eq!( - crate::process_broker::error_kind(&error), - Some(crate::process_broker::BrokerErrorKind::Rejected) + process_broker::error_kind(&error), + Some(process_broker::BrokerErrorKind::Rejected) ); } diff --git a/src/process_broker/transport.rs b/src/process_broker/transport.rs index 4d1404088..9298e1cd8 100644 --- a/src/process_broker/transport.rs +++ b/src/process_broker/transport.rs @@ -1,7 +1,8 @@ use std::collections::VecDeque; -use std::io; +use std::mem::MaybeUninit; use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::time::Duration; +use std::{io, mem, ptr}; use anyhow::{Context, Result, anyhow, bail}; @@ -27,7 +28,7 @@ pub(super) fn set_socket_timeout(fd: RawFd, timeout: Duration) -> io::Result<()> libc::SOL_SOCKET, option, (&value as *const libc::timeval).cast(), - std::mem::size_of::() as libc::socklen_t, + mem::size_of::() as libc::socklen_t, ) } != 0 { @@ -241,7 +242,7 @@ fn sealed_memfd(bytes: &[u8]) -> Result { } fn validate_sealed_memfd(descriptor: &OwnedFd, length: usize) -> Result<()> { - let mut stat = std::mem::MaybeUninit::::uninit(); + let mut stat = MaybeUninit::::uninit(); // SAFETY: stat points to writable storage for fstat. if unsafe { libc::fstat(descriptor.as_raw_fd(), stat.as_mut_ptr()) } != 0 { return Err(io::Error::last_os_error()).context("failed to inspect broker memfd"); @@ -274,11 +275,12 @@ pub(super) fn send_packet(fd: RawFd, packet: &[u8], descriptors: &[RawFd]) -> io let control_len = if descriptors.is_empty() { 0 } else { - unsafe { libc::CMSG_SPACE(std::mem::size_of_val(descriptors) as libc::c_uint) as usize } + // SAFETY: CMSG_SPACE only performs arithmetic on its argument. + unsafe { libc::CMSG_SPACE(mem::size_of_val(descriptors) as libc::c_uint) as usize } }; - let mut control = vec![0_usize; control_len.div_ceil(std::mem::size_of::())]; + let mut control = vec![0_usize; control_len.div_ceil(mem::size_of::())]; // SAFETY: zero is a valid initial state for msghdr. - let mut message: libc::msghdr = unsafe { std::mem::zeroed() }; + let mut message: libc::msghdr = unsafe { mem::zeroed() }; message.msg_iov = &mut iovec; message.msg_iovlen = 1; if !descriptors.is_empty() { @@ -290,8 +292,8 @@ pub(super) fn send_packet(fd: RawFd, packet: &[u8], descriptors: &[RawFd]) -> io (*header).cmsg_level = libc::SOL_SOCKET; (*header).cmsg_type = libc::SCM_RIGHTS; (*header).cmsg_len = - libc::CMSG_LEN(std::mem::size_of_val(descriptors) as libc::c_uint) as usize; - std::ptr::copy_nonoverlapping( + libc::CMSG_LEN(mem::size_of_val(descriptors) as libc::c_uint) as usize; + ptr::copy_nonoverlapping( descriptors.as_ptr(), libc::CMSG_DATA(header).cast::(), descriptors.len(), @@ -321,14 +323,14 @@ pub(super) fn recv_packet(fd: RawFd) -> io::Result<(Vec, Vec)> { iov_base: buffer.as_mut_ptr().cast(), iov_len: buffer.len(), }; + // SAFETY: CMSG_SPACE only performs arithmetic on its argument. let control_len = unsafe { - libc::CMSG_SPACE( - (MAX_PACKET_DESCRIPTORS * std::mem::size_of::()) as libc::c_uint, - ) as usize + libc::CMSG_SPACE((MAX_PACKET_DESCRIPTORS * mem::size_of::()) as libc::c_uint) + as usize }; - let mut control = vec![0_usize; control_len.div_ceil(std::mem::size_of::())]; + let mut control = vec![0_usize; control_len.div_ceil(mem::size_of::())]; // SAFETY: zero is a valid initial state for msghdr. - let mut message: libc::msghdr = unsafe { std::mem::zeroed() }; + let mut message: libc::msghdr = unsafe { mem::zeroed() }; message.msg_iov = &mut iovec; message.msg_iovlen = 1; message.msg_control = control.as_mut_ptr().cast(); @@ -358,13 +360,13 @@ pub(super) fn recv_packet(fd: RawFd) -> io::Result<(Vec, Vec)> { )); } let data_len = (*header).cmsg_len - base_len; - if !data_len.is_multiple_of(std::mem::size_of::()) { + if !data_len.is_multiple_of(mem::size_of::()) { return Err(io::Error::new( io::ErrorKind::InvalidData, "misaligned broker descriptor payload", )); } - let count = data_len / std::mem::size_of::(); + let count = data_len / mem::size_of::(); let data = libc::CMSG_DATA(header).cast::(); for index in 0..count { descriptors.push(OwnedFd::from_raw_fd(*data.add(index))); diff --git a/src/process_broker/wire.rs b/src/process_broker/wire.rs index 0226a6cea..a920ac3e7 100644 --- a/src/process_broker/wire.rs +++ b/src/process_broker/wire.rs @@ -1,4 +1,6 @@ use std::ffi::{OsStr, OsString}; +use std::os::fd::OwnedFd; +use std::os::unix::ffi::OsStrExt; use anyhow::{Result, bail}; use serde::{Deserialize, Serialize}; @@ -166,7 +168,7 @@ pub(super) enum BlobWire { pub(super) struct BrokerWireResponse { pub(super) outcome: BrokerOutcome, - pub(super) descriptors: Vec, + pub(super) descriptors: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -175,7 +177,7 @@ pub(super) struct OsWire(pub(super) Vec); impl OsWire { pub(super) fn from_os(value: &OsStr) -> Result { - let bytes = std::os::unix::ffi::OsStrExt::as_bytes(value); + let bytes = OsStrExt::as_bytes(value); if bytes.len() > MAX_ARGUMENT_BYTES || bytes.contains(&0) { bail!("broker argument is oversized or contains NUL"); } diff --git a/src/render_profiles.rs b/src/render_profiles.rs index 3e86afa42..05e0951e9 100644 --- a/src/render_profiles.rs +++ b/src/render_profiles.rs @@ -1,6 +1,7 @@ //! Runtime support for final-render color profile remapping. use std::collections::HashMap; +use std::ops::Range; use crate::config::{RenderProfileConfig, RenderProfileExportMode, RenderProfilesConfig}; use crate::util::Rect; @@ -27,14 +28,17 @@ pub struct RenderColorProfile { impl RenderColorProfile { #[allow(dead_code)] // Used by tests and consumers of the library API. + #[must_use] pub fn id(&self) -> &str { &self.id } + #[must_use] pub fn name(&self) -> &str { &self.name } + #[must_use] pub fn is_empty(&self) -> bool { self.mappings.is_empty() } @@ -225,7 +229,7 @@ fn for_each_disjoint_span( stride: i32, data_len: usize, regions: &[Rect], - mut visit: impl FnMut(std::ops::Range), + mut visit: impl FnMut(Range), ) { let stride = stride as usize; for band in disjoint_bands(width, height, regions) { @@ -291,10 +295,12 @@ impl RenderProfileSet { } } + #[must_use] pub fn active(&self) -> Option<&RenderColorProfile> { self.active_index.and_then(|index| self.profiles.get(index)) } + #[must_use] pub fn export_profile(&self) -> Option { match self.export_mode { RenderProfileExportMode::Off => None, @@ -306,18 +312,22 @@ impl RenderProfileSet { } } + #[must_use] pub fn generation(&self) -> u64 { self.generation } + #[must_use] pub fn applies_to_canvas(&self) -> bool { self.apply_to_canvas } + #[must_use] pub fn applies_to_ui(&self) -> bool { self.apply_to_ui } + #[must_use] pub fn is_empty(&self) -> bool { self.profiles.is_empty() } @@ -360,10 +370,12 @@ impl RenderProfileSet { } } +#[must_use] pub fn normalize_profile_id(value: &str) -> String { value.trim().to_ascii_lowercase() } +#[must_use] pub fn parse_hex_rgb(value: &str) -> Option { let trimmed = value.trim(); let hex = trimmed @@ -381,6 +393,7 @@ pub fn parse_hex_rgb(value: &str) -> Option { Some(Rgb8 { r, g, b }) } +#[must_use] pub fn format_hex_rgb(color: Rgb8) -> String { format!("#{:02X}{:02X}{:02X}", color.r, color.g, color.b) } diff --git a/src/runtime_capabilities.rs b/src/runtime_capabilities.rs index dafb46428..e6fcef687 100644 --- a/src/runtime_capabilities.rs +++ b/src/runtime_capabilities.rs @@ -11,6 +11,7 @@ pub struct RuntimeCapabilities { pub input_monitor: bool, } +#[must_use] pub fn current_runtime_capabilities() -> RuntimeCapabilities { RuntimeCapabilities { portal: portal_runtime_supported(), @@ -18,6 +19,7 @@ pub fn current_runtime_capabilities() -> RuntimeCapabilities { } } +#[must_use] pub fn render_runtime_capabilities(capabilities: RuntimeCapabilities) -> String { format!( "portal={}\ninput_monitor={}\n", @@ -30,11 +32,11 @@ pub fn parse_runtime_capabilities(output: &str) -> Result, path_effect: RuntimeStateFailurePathEffect, ) -> (Self, PersistenceIncidentId) { @@ -171,7 +186,7 @@ impl RuntimeUiStateController { } pub(crate) fn take_preview_resolutions(&mut self) -> Vec { - std::mem::take(&mut self.preview_resolution_outbox) + mem::take(&mut self.preview_resolution_outbox) } pub(in crate::runtime_ui_state) fn close_barrier_and_resolve_previews( diff --git a/src/runtime_ui_state/controller/internals.rs b/src/runtime_ui_state/controller/internals.rs index 631856724..99dbd63fd 100644 --- a/src/runtime_ui_state/controller/internals.rs +++ b/src/runtime_ui_state/controller/internals.rs @@ -1,4 +1,14 @@ -use super::*; +use super::super::{ + AbandonedPreviewResolutionReason, ActiveControllerBarrier, ControllerBarrierId, + ControllerBarrierOperation, ControllerBarrierPhase, IntegratedSourceMutation, + RuntimeStateObservedPathEffect, RuntimeStateRecoveryArtifact, RuntimeStateSourceObservation, + RuntimeUiFileStatus, RuntimeUiLiveState, RuntimeUiWireState, SourceMutationKind, + SourceMutationResult, WirePassthrough, +}; +use super::{ + ExternalReconciliationEvidence, RuntimeUiStateController, SubmitSourceMutationResult, + SupportedResetAuthoritySnapshot, SupportedResetAuthorityState, UnsupportedResetConfirmation, +}; impl RuntimeUiStateController { pub(super) fn canonical_wire(&self) -> RuntimeUiWireState { @@ -82,7 +92,7 @@ impl RuntimeUiStateController { pub(super) fn finish_supported_reset_success( &mut self, - integrated: IntegratedSourceMutation, + integrated: &IntegratedSourceMutation, ) -> SubmitSourceMutationResult { let recovery_artifacts = match &integrated.result { SourceMutationResult::Applied { @@ -94,7 +104,10 @@ impl RuntimeUiStateController { .supported_reset .take() .expect("matching reset acknowledgement requires transaction"); - debug_assert_eq!(transaction.through, integrated.request.accepted_through); + debug_assert_eq!( + transaction.through, integrated.request.accepted_through, + "a reset acknowledgement must cover the revision its transaction reset through" + ); self.model.clear(); self.passthrough = WirePassthrough::default(); self.live_only_overlay.clear(); diff --git a/src/runtime_ui_state/controller/lifecycle.rs b/src/runtime_ui_state/controller/lifecycle.rs index 897f47dff..a726cd5d4 100644 --- a/src/runtime_ui_state/controller/lifecycle.rs +++ b/src/runtime_ui_state/controller/lifecycle.rs @@ -1,4 +1,9 @@ -use super::*; +use super::super::{ + AbandonedPreviewResolutionReason, AcceptedStateRevision, ControllerBarrierPhase, + DurabilityOutcome, FlushOutcome, FlushRequestId, PipelineProtocolError, RuntimeUiFileStatus, + RuntimeUiLiveState, RuntimeUiWireState, ValidatedInteractionSeeds, +}; +use super::{RuntimeUiStateController, UpdateSeedsResult}; impl RuntimeUiStateController { #[cfg(test)] @@ -67,13 +72,13 @@ impl RuntimeUiStateController { pub(crate) fn drain_lifecycle_controls(&mut self) { while let Ok(control) = self.lifecycle_rx.try_recv() { - self.apply_lifecycle_control(control); + self.apply_lifecycle_control(&control); } } pub(super) fn apply_seed_update( &mut self, - seeds: ValidatedInteractionSeeds, + seeds: &ValidatedInteractionSeeds, ) -> UpdateSeedsResult { let mut next_seeds = self.seeds.clone(); let changed_targets = match next_seeds.update(seeds) { diff --git a/src/runtime_ui_state/controller/mutations.rs b/src/runtime_ui_state/controller/mutations.rs index 39ad2f875..9a5b1b167 100644 --- a/src/runtime_ui_state/controller/mutations.rs +++ b/src/runtime_ui_state/controller/mutations.rs @@ -1,9 +1,13 @@ -use super::*; +use super::super::{ + MutationShapeError, RuntimeUiFileStatus, RuntimeUiMutationPermit, RuntimeUiMutationScope, + RuntimeUiMutationValues, StagedSeedReload, ValidatedInteractionSeeds, +}; +use super::{BeginMutationError, CommitResult, RuntimeUiStateController, UpdateSeedsResult}; impl RuntimeUiStateController { pub(crate) fn begin_mutation( &self, - scope: RuntimeUiMutationScope, + scope: &RuntimeUiMutationScope, ) -> Result { if self.shutting_down { return Err(BeginMutationError::ShuttingDown); @@ -37,7 +41,7 @@ impl RuntimeUiStateController { pub(crate) fn commit( &mut self, permit: RuntimeUiMutationPermit, - desired_values: RuntimeUiMutationValues, + desired_values: &RuntimeUiMutationValues, ) -> CommitResult { self.drain_lifecycle_controls(); if let Some(barrier) = &self.active_barrier { @@ -80,7 +84,7 @@ impl RuntimeUiStateController { let previous_model = self.model.clone(); let previous_passthrough = self.passthrough.clone(); - if !self.model.apply(&permit.guards, &desired_values) { + if !self.model.apply(&permit.guards, desired_values) { return CommitResult::NoChange; } self.passthrough.reconcile_entries(&self.model); @@ -97,7 +101,7 @@ impl RuntimeUiStateController { } } - pub(crate) fn update_seeds(&mut self, seeds: ValidatedInteractionSeeds) -> UpdateSeedsResult { + pub(crate) fn update_seeds(&mut self, seeds: &ValidatedInteractionSeeds) -> UpdateSeedsResult { self.drain_lifecycle_controls(); if self.shutting_down { return UpdateSeedsResult::RejectedShuttingDown; diff --git a/src/runtime_ui_state/controller/resets.rs b/src/runtime_ui_state/controller/resets.rs index 86e20a050..a3c494ff0 100644 --- a/src/runtime_ui_state/controller/resets.rs +++ b/src/runtime_ui_state/controller/resets.rs @@ -1,4 +1,12 @@ -use super::*; +use super::super::{ + ActiveControllerBarrier, ControllerBarrierOperation, ControllerBarrierPhase, DurabilityOutcome, + RuntimeStateIoError, RuntimeUiFileStatus, UnsupportedResetConfirmationId, +}; +use super::{ + CancelUnsupportedResetConfirmationResult, ConfirmUnsupportedResetResult, RequestResetResult, + RuntimeUiStateController, SupportedResetAuthorityState, SupportedResetTransaction, + UnsupportedResetConfirmation, +}; impl RuntimeUiStateController { #[allow( @@ -86,7 +94,7 @@ impl RuntimeUiStateController { DurabilityOutcome::Failed(reset_error.clone()), ) { self.pipeline - .settle_failed([transaction.through], reset_error); + .settle_failed([transaction.through], &reset_error); } let held_by_reset = transaction.held_by_reset; if let Err(restore_error) = self @@ -95,7 +103,7 @@ impl RuntimeUiStateController { { self.pipeline.settle_held_failed( &held_by_reset, - RuntimeStateIoError::new(format!( + &RuntimeStateIoError::new(format!( "failed to restore reset-held state: {restore_error:?}" )), ); @@ -113,10 +121,10 @@ impl RuntimeUiStateController { pub(crate) fn cancel_unsupported_reset_confirmation( &mut self, - confirmation: UnsupportedResetConfirmation, + confirmation: &UnsupportedResetConfirmation, ) -> CancelUnsupportedResetConfirmationResult { self.drain_lifecycle_controls(); - if !self.unsupported_reset_confirmation_is_current(&confirmation) { + if !self.unsupported_reset_confirmation_is_current(confirmation) { return CancelUnsupportedResetConfirmationResult::RejectedToken; } self.pending_unsupported_reset_confirmation = None; @@ -183,7 +191,7 @@ impl RuntimeUiStateController { DurabilityOutcome::Failed(reset_error.clone()), ) { self.pipeline - .settle_failed([transaction.through], reset_error); + .settle_failed([transaction.through], &reset_error); } let held_by_reset = transaction.held_by_reset; if let Err(restore_error) = self @@ -192,7 +200,7 @@ impl RuntimeUiStateController { { self.pipeline.settle_held_failed( &held_by_reset, - RuntimeStateIoError::new(format!( + &RuntimeStateIoError::new(format!( "failed to restore unsupported-reset-held state: {restore_error:?}" )), ); diff --git a/src/runtime_ui_state/controller/source_mutation.rs b/src/runtime_ui_state/controller/source_mutation.rs index 0b87378b9..173e51343 100644 --- a/src/runtime_ui_state/controller/source_mutation.rs +++ b/src/runtime_ui_state/controller/source_mutation.rs @@ -1,4 +1,16 @@ -use super::*; +use super::super::{ + AbandonedPreviewResolutionReason, ControllerBarrierId, ControllerBarrierOperation, + ControllerBarrierPhase, HeldReplacementStage, RuntimeStateFailurePathEffect, + RuntimeStateIoError, RuntimeStateObservedEnvelope, RuntimeStateObservedPathEffect, + RuntimeStateSourceObservation, RuntimeUiFileStatus, RuntimeUiModel, RuntimeUiWireState, + SourceMutationKind, SourceMutationRequest, SourceMutationResult, StagedSeedReload, + WirePassthrough, +}; +use super::{ + ExternalAuthorityInstallError, ExternalAuthorityInstallResult, RuntimeUiStateController, + SubmitSourceMutationResult, file_status_matches_observation, +}; +use std::collections::BTreeSet; impl RuntimeUiStateController { pub(crate) fn take_source_mutation(&mut self) -> Option { @@ -28,7 +40,7 @@ impl RuntimeUiStateController { SourceMutationKind::ResetSupported { .. } | SourceMutationKind::ResetUnsupportedIfUnchanged { .. } ) { - return self.finish_supported_reset_success(integrated); + return self.finish_supported_reset_success(&integrated); } if self.supported_reset.is_some() { self.capture_reset_authority_after_prerequisite(); @@ -123,16 +135,16 @@ impl RuntimeUiStateController { SourceMutationKind::ResetSupported { .. } | SourceMutationKind::ResetUnsupportedIfUnchanged { .. } => { self.pipeline - .settle_failed(integrated.covered.iter().copied(), error.clone()); + .settle_failed(integrated.covered.iter().copied(), error); None } }; let incident = self.enter_persistence_incident( - error.clone(), + error, active.clone(), recovery_artifacts.clone(), path_effect.clone(), - failed_replacement, + failed_replacement.as_ref(), ); let barrier = self.active_barrier.as_ref().expect("barrier installed").id; if self.shutting_down { @@ -204,7 +216,7 @@ impl RuntimeUiStateController { self.live_only_overlay.clear(); self.rebuild_live_state(); let incident = self.enter_persistence_incident( - RuntimeStateIoError::new( + &RuntimeStateIoError::new( "external runtime-state authority is malformed or unreadable", ), Some(observation), @@ -247,7 +259,7 @@ impl RuntimeUiStateController { let mut canonical_model = model; canonical_model.remove_targets(&tombstoned_targets); canonical_model.reconcile(&next_seeds); - let mut canonical_passthrough = passthrough.clone(); + let mut canonical_passthrough = passthrough; canonical_passthrough.reconcile_entries(&canonical_model); let canonical_wire = RuntimeUiWireState { model: canonical_model.clone(), @@ -267,7 +279,7 @@ impl RuntimeUiStateController { self.model = canonical_model; self.passthrough = canonical_passthrough; self.pipeline - .install_acknowledged_authority(observation.revision.clone(), observed_wire); + .install_acknowledged_authority(observation.revision, observed_wire); if retains_live_only_authority { self.live_only_overlay.reconcile(&changed_targets); } else { diff --git a/src/runtime_ui_state/model.rs b/src/runtime_ui_state/model.rs index f22624308..96efaca0c 100644 --- a/src/runtime_ui_state/model.rs +++ b/src/runtime_ui_state/model.rs @@ -1,4 +1,5 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::iter; use super::{ ControllerId, InteractionSeedRegistry, InteractionSeedTarget, InteractionSeedValue, SeedGuard, @@ -218,7 +219,7 @@ impl RuntimeUiMutationScope { &self, ) -> Result, MutationShapeError> { let targets: BTreeSet = match self { - Self::Target(target) => std::iter::once(target.clone()).collect(), + Self::Target(target) => iter::once(target.clone()).collect(), Self::Batch(targets) => targets.iter().cloned().collect(), }; if targets.is_empty() { diff --git a/src/runtime_ui_state/pipeline.rs b/src/runtime_ui_state/pipeline.rs index 317f4ea0d..10f0f2c40 100644 --- a/src/runtime_ui_state/pipeline.rs +++ b/src/runtime_ui_state/pipeline.rs @@ -2,9 +2,9 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque}; use super::{ AcceptedStateRevision, ControllerBarrierId, FlushRequestId, RuntimeStateFailurePathEffect, - RuntimeStateIoError, RuntimeStateObservedPathEffect, RuntimeStatePostClaimPathEffect, - RuntimeStateRecoveryArtifact, RuntimeStateSourceObservation, RuntimeStateSourceRevision, - RuntimeUiWireState, SourceMutationId, + RuntimeStateIoError, RuntimeStatePostClaimPathEffect, RuntimeStateRecoveryArtifact, + RuntimeStateSourceObservation, RuntimeStateSourceRevision, RuntimeUiWireState, + SourceMutationId, }; mod integration; diff --git a/src/runtime_ui_state/pipeline/integration.rs b/src/runtime_ui_state/pipeline/integration.rs index 1117180fd..aa848be7b 100644 --- a/src/runtime_ui_state/pipeline/integration.rs +++ b/src/runtime_ui_state/pipeline/integration.rs @@ -1,4 +1,13 @@ -use super::*; +use super::super::{ + AcceptedStateRevision, FlushRequestId, RuntimeStateFailurePathEffect, RuntimeStateIoError, + RuntimeStateObservedPathEffect, RuntimeStatePostClaimPathEffect, RuntimeStateSourceRevision, + RuntimeUiWireState, +}; +use super::{ + DurabilityOutcome, FlushOutcome, HeldReplacementStage, IntegratedSourceMutation, + PersistencePipeline, PipelineProtocolError, SourceMutationKind, SourceMutationResult, +}; +use std::collections::BTreeSet; impl PersistencePipeline { pub(crate) fn integrate( @@ -152,7 +161,7 @@ impl PersistencePipeline { pub(crate) fn settle_failed( &mut self, revisions: impl IntoIterator, - error: RuntimeStateIoError, + error: &RuntimeStateIoError, ) { for revision in revisions { self.settle_receipt(revision, DurabilityOutcome::Failed(error.clone())); @@ -163,7 +172,7 @@ impl PersistencePipeline { pub(crate) fn settle_persisted( &mut self, revisions: impl IntoIterator, - source: RuntimeStateSourceRevision, + source: &RuntimeStateSourceRevision, ) { for revision in revisions { self.settle_receipt( @@ -189,7 +198,7 @@ impl PersistencePipeline { pub(crate) fn settle_held_failed( &mut self, stages: &[HeldReplacementStage], - error: RuntimeStateIoError, + error: &RuntimeStateIoError, ) { self.settle_failed( stages diff --git a/src/runtime_ui_state/pipeline/queue.rs b/src/runtime_ui_state/pipeline/queue.rs index 004ab89e4..ef291722d 100644 --- a/src/runtime_ui_state/pipeline/queue.rs +++ b/src/runtime_ui_state/pipeline/queue.rs @@ -1,5 +1,9 @@ +use super::super::{AcceptedStateRevision, FlushRequestId, SourceMutationId}; use super::integration::durability_satisfies_flush; -use super::*; +use super::{ + FlushOutcome, InFlightSourceMutation, PendingStage, PersistencePipeline, PipelineProtocolError, + SourceMutationKind, SourceMutationRequest, +}; impl PersistencePipeline { #[allow( diff --git a/src/runtime_ui_state/pipeline/staging.rs b/src/runtime_ui_state/pipeline/staging.rs index 0d58e17e2..bb772a691 100644 --- a/src/runtime_ui_state/pipeline/staging.rs +++ b/src/runtime_ui_state/pipeline/staging.rs @@ -1,4 +1,9 @@ -use super::*; +use super::super::{AcceptedStateRevision, RuntimeStateSourceRevision, RuntimeUiWireState}; +use super::{ + DurabilityOutcome, FlushOutcome, HeldReplacementStage, PendingReplacement, PendingStage, + PersistencePipeline, PipelineProtocolError, SourceMutationKind, SourceMutationRequest, +}; +use std::collections::VecDeque; impl PersistencePipeline { pub(crate) fn accept_replace( @@ -144,7 +149,8 @@ impl PersistencePipeline { stages: &[HeldReplacementStage], reset_through: AcceptedStateRevision, ) { - for revision in stages.iter().flat_map(|stage| &stage.covered) { + let covered_revisions = stages.iter().flat_map(|stage| &stage.covered); + for revision in covered_revisions { self.settle_receipt( *revision, DurabilityOutcome::SupersededByReset { reset_through }, @@ -154,7 +160,8 @@ impl PersistencePipeline { } pub(crate) fn settle_held_external(&mut self, stages: &[HeldReplacementStage]) { - for revision in stages.iter().flat_map(|stage| &stage.covered) { + let covered_revisions = stages.iter().flat_map(|stage| &stage.covered); + for revision in covered_revisions { self.settle_receipt(*revision, DurabilityOutcome::ExternalSourceWon); } self.advance_settled_through(); @@ -165,7 +172,8 @@ impl PersistencePipeline { stages: &[HeldReplacementStage], source: &RuntimeStateSourceRevision, ) { - for revision in stages.iter().flat_map(|stage| &stage.covered) { + let covered_revisions = stages.iter().flat_map(|stage| &stage.covered); + for revision in covered_revisions { self.settle_receipt( *revision, DurabilityOutcome::Persisted { @@ -236,7 +244,10 @@ impl PersistencePipeline { source: RuntimeStateSourceRevision, wire: RuntimeUiWireState, ) { - debug_assert!(self.in_flight.is_none()); + debug_assert!( + self.in_flight.is_none(), + "install acknowledged authority only after the in-flight mutation settles" + ); self.stable_source = source; self.acknowledged_wire = wire; } diff --git a/src/runtime_ui_state/preview.rs b/src/runtime_ui_state/preview.rs index 426896fbe..590e5dc9b 100644 --- a/src/runtime_ui_state/preview.rs +++ b/src/runtime_ui_state/preview.rs @@ -1,6 +1,12 @@ use std::collections::BTreeSet; +use std::mem; -use super::*; +use super::{ + AcceptedStateRevision, BeginMutationError, CommitResult, ControllerBarrierId, ControllerId, + InteractionSeedTarget, MutationShapeError, PreviewRollbackSnapshot, RuntimeUiFileStatus, + RuntimeUiLiveState, RuntimeUiMutationPermit, RuntimeUiMutationScope, RuntimeUiMutationValues, + RuntimeUiStateController, SeedGuard, SeedRegistryError, +}; #[derive(Debug)] pub(crate) struct RuntimeUiLiveOnlyGuard { @@ -133,7 +139,7 @@ impl RuntimeUiStateController { } RuntimeUiFileStatus::Invalid => Err(BeginPreviewError::InvalidAuthority), RuntimeUiFileStatus::Missing | RuntimeUiFileStatus::Supported => self - .begin_mutation(scope.clone()) + .begin_mutation(&scope) .map(|permit| { RuntimeUiPreviewSession::Persistent(RuntimePersistentPreviewSession { permit, @@ -166,7 +172,7 @@ impl RuntimeUiStateController { self.drain_lifecycle_controls(); if let Some(barrier) = &self.active_barrier { let barrier = barrier.id; - self.record_abandoned_preview(barrier, session, intent); + self.record_abandoned_preview(barrier, session, &intent); return PreviewFinishResult::AbandonedDuringBarrier { barrier }; } self.finish_runtime_preview(session, intent) @@ -180,7 +186,7 @@ impl RuntimeUiStateController { ) -> Vec { let mut retained = Vec::new(); let mut resolved = Vec::new(); - for abandoned in std::mem::take(&mut self.abandoned_previews) { + for abandoned in mem::take(&mut self.abandoned_previews) { if abandoned.barrier != barrier { retained.push(abandoned); continue; @@ -228,7 +234,7 @@ impl RuntimeUiStateController { scope, rollback, } = session; - match self.commit(permit, values) { + match self.commit(permit, &values) { CommitResult::Accepted { through } => { PreviewFinishResult::AcceptedRuntime { through } } @@ -300,7 +306,7 @@ impl RuntimeUiStateController { &mut self, barrier: ControllerBarrierId, session: RuntimeUiPreviewSession, - intent: RuntimePreviewFinishIntent, + intent: &RuntimePreviewFinishIntent, ) { let finish = match intent { RuntimePreviewFinishIntent::Commit(_) => AbandonedPreviewFinish::CommitRequested, diff --git a/src/runtime_ui_state/recovery.rs b/src/runtime_ui_state/recovery.rs index c6b30dbfb..d5ae97dcf 100644 --- a/src/runtime_ui_state/recovery.rs +++ b/src/runtime_ui_state/recovery.rs @@ -1,7 +1,16 @@ use std::collections::BTreeSet; use std::sync::mpsc::{Receiver, Sender}; -use super::*; +use super::{ + AcceptedStateRevision, ControllerBarrierId, ControllerId, HeldReplacementStage, + InteractionSeedRegistry, InteractionSeedTarget, PersistenceIncidentId, RecoveryAttemptId, + RecoveryCommandId, RecoveryHandleId, RecoveryLeaseNonce, RuntimeStateFailurePathEffect, + RuntimeStateInspectionError, RuntimeStateIoError, RuntimeStateObservedEnvelope, + RuntimeStateObservedPathEffect, RuntimeStatePostClaimPathEffect, RuntimeStateRecoveryArtifact, + RuntimeStateSourceObservation, RuntimeStateSourceRevision, RuntimeUiFileStatus, + RuntimeUiLiveState, RuntimeUiModel, RuntimeUiWireState, SourceMutationId, + SourceMutationRequest, SourceMutationResult, StagedSeedReload, WirePassthrough, +}; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct PersistenceAuthoritySnapshot { @@ -502,9 +511,3 @@ mod authority; mod lifecycle; mod preserve; mod protocol; - -use protocol::{ - allocate_counter, completion_protocol_error, evidence, merge_artifacts, - observation_matches_file_status, reinspection_writer_observation, - source_mutation_observation_for_protocol_error, -}; diff --git a/src/runtime_ui_state/recovery/attempt.rs b/src/runtime_ui_state/recovery/attempt.rs index d4eceab42..815fbcaf0 100644 --- a/src/runtime_ui_state/recovery/attempt.rs +++ b/src/runtime_ui_state/recovery/attempt.rs @@ -1,4 +1,20 @@ -use super::*; +use super::super::{ + ControllerBarrierPhase, RecoveryAttemptStep, RecoveryCommandId, RuntimeStateFailurePathEffect, + RuntimeStateInspectionError, RuntimeStateIoError, RuntimeStateObservedEnvelope, + RuntimeStateObservedPathEffect, RuntimeStateSourceObservation, RuntimeUiFileStatus, + RuntimeUiStateController, RuntimeUiWireState, SourceMutationId, SourceMutationKind, + SourceMutationRequest, SourceMutationResult, +}; +use super::protocol::{ + allocate_counter, merge_artifacts, reinspection_writer_observation, + source_mutation_observation_for_protocol_error, +}; +use super::{ + ActiveRecoveryCommand, PersistenceAuthoritySnapshot, RecoveryAttemptKind, + RecoveryCanonicalWritePurpose, RecoveryCleanupState, RecoveryCommandExpectation, + RecoveryInspection, RecoveryIoCommand, RecoveryIoOperation, RecoverySourceMutationKind, + SubmitPersistenceRecoveryResult, +}; impl RuntimeUiStateController { pub(super) fn integrate_recovery_inspection( @@ -236,7 +252,7 @@ impl RuntimeUiStateController { if !dirty { if let Some(cleanup) = cleanup { self.pipeline - .settle_persisted([cleanup], self.pipeline.stable_source().clone()); + .settle_persisted([cleanup], &self.pipeline.stable_source().clone()); } if let Some(incident) = &mut self.incident { incident.cleanup = RecoveryCleanupState::Clean; @@ -280,7 +296,10 @@ impl RuntimeUiStateController { retry_desired_through: retry, cleanup_through: cleanup, }; - debug_assert!(purpose.is_valid()); + debug_assert!( + purpose.is_valid(), + "a canonical recovery write must retry or clean up a revision, got {purpose:?}" + ); let result = self.dispatch_recovery_source_command( recovery_command_id.expect("dirty recovery allocated a command id"), request, @@ -473,7 +492,7 @@ impl RuntimeUiStateController { return self.finish_recovery_shutdown(); } let attempt = self.active_recovery.as_ref().expect("active attempt").id; - self.finish_cancelled_attempt(Some(observation.clone())); + self.finish_cancelled_attempt(Some(observation)); return SubmitPersistenceRecoveryResult::Terminal { attempt }; } if matches!( @@ -549,7 +568,7 @@ impl RuntimeUiStateController { incident.path_effect_history.push(path_effect); } self.pipeline - .settle_failed(integrated.covered.iter().copied(), error.clone()); + .settle_failed(integrated.covered.iter().copied(), &error); self.note_recovery_write_failed(purpose); if cancel_requested { if self.shutting_down { @@ -566,7 +585,7 @@ impl RuntimeUiStateController { pub(super) fn integrate_protocol_failure_source_completion( &mut self, - result: SourceMutationResult, + result: &SourceMutationResult, ) -> SubmitPersistenceRecoveryResult { let cancel_requested = self .active_recovery @@ -586,18 +605,18 @@ impl RuntimeUiStateController { } }); let active_observation = source_mutation_observation_for_protocol_error( - &result, + result, if preserve_invalid { RuntimeStateObservedEnvelope::PresentWithoutReadableVersion } else { RuntimeStateObservedEnvelope::Version(1) }, ); - self.retain_recovery_mutation_evidence(&result); + self.retain_recovery_mutation_evidence(result); if !preserve_invalid { match self.pipeline.integrate(result.clone()) { - Ok(integrated) => match &result { + Ok(integrated) => match result { SourceMutationResult::Applied { .. } => { if matches!(&integrated.request.kind, SourceMutationKind::Replace(_)) { self.file_status = RuntimeUiFileStatus::Supported; @@ -610,7 +629,7 @@ impl RuntimeUiStateController { } SourceMutationResult::Failed { error, .. } => { self.pipeline - .settle_failed(integrated.covered.iter().copied(), error.clone()); + .settle_failed(integrated.covered.iter().copied(), error); self.note_recovery_write_failed(purpose); } }, diff --git a/src/runtime_ui_state/recovery/authority.rs b/src/runtime_ui_state/recovery/authority.rs index 26eed9024..164979c7e 100644 --- a/src/runtime_ui_state/recovery/authority.rs +++ b/src/runtime_ui_state/recovery/authority.rs @@ -1,4 +1,18 @@ -use super::*; +use std::mem; +use std::path::PathBuf; + +use super::super::{ + AbandonedPreviewResolutionReason, ControllerBarrierPhase, RecoveryAttemptId, + RuntimeStateIoError, RuntimeStateObservedEnvelope, RuntimeStateObservedPathEffect, + RuntimeStatePostClaimPathEffect, RuntimeStateSourceObservation, RuntimeStateSourceRevision, + RuntimeUiFileStatus, RuntimeUiLiveState, RuntimeUiStateController, RuntimeUiWireState, + WirePassthrough, +}; +use super::protocol::evidence; +use super::{ + InvalidStateResetConfirmation, PersistenceIncident, PersistenceRecoveryResult, + RecoveryAttemptKind, RecoveryCleanupState, RecoveryInspection, SubmitPersistenceRecoveryResult, +}; impl RuntimeUiStateController { pub(super) fn apply_incident_staged_reload(&mut self) { @@ -288,7 +302,7 @@ impl RuntimeUiStateController { pub(super) fn finish_preserved_invalid( &mut self, observation: RuntimeStateSourceObservation, - recovery_path: std::path::PathBuf, + recovery_path: PathBuf, ) -> SubmitPersistenceRecoveryResult { self.install_preserved_invalid_authority(); let incident = self.incident.take().expect("incident"); @@ -323,7 +337,7 @@ impl RuntimeUiStateController { .map(|incident| { incident.retry_desired_through = None; incident.cleanup = RecoveryCleanupState::Clean; - std::mem::take(&mut incident.held_replacements) + mem::take(&mut incident.held_replacements) }) .unwrap_or_default(); self.pipeline.settle_held_external(&held); @@ -419,11 +433,11 @@ impl RuntimeUiStateController { let incident = self.incident.take()?; let error = RuntimeStateIoError::new("runtime-state persistence shut down"); self.pipeline - .settle_held_failed(&incident.held_replacements, error.clone()); + .settle_held_failed(&incident.held_replacements, &error); if let RecoveryCleanupState::Pending { through } | RecoveryCleanupState::InFlight { through, .. } = incident.cleanup { - self.pipeline.settle_failed([through], error); + self.pipeline.settle_failed([through], &error); } self.close_barrier_and_resolve_previews( incident.barrier, diff --git a/src/runtime_ui_state/recovery/lifecycle.rs b/src/runtime_ui_state/recovery/lifecycle.rs index 55ae133b8..600212b34 100644 --- a/src/runtime_ui_state/recovery/lifecycle.rs +++ b/src/runtime_ui_state/recovery/lifecycle.rs @@ -1,4 +1,30 @@ -use super::*; +use std::slice; +use std::sync::mpsc; + +use super::super::{ + AbandonedPreviewResolutionReason, ActiveControllerBarrier, ControllerBarrierId, + ControllerBarrierOperation, ControllerBarrierPhase, DurabilityOutcome, HeldReplacementStage, + PersistenceIncidentId, RecoveryAttemptId, RecoveryAttemptStep, RecoveryCommandId, + RecoveryHandleId, RecoveryLeaseNonce, RuntimeStateFailurePathEffect, RuntimeStateIoError, + RuntimeStateObservedPathEffect, RuntimeStateRecoveryArtifact, RuntimeStateSourceObservation, + RuntimeUiStateController, +}; +use super::protocol::{ + allocate_counter, completion_protocol_error, merge_artifacts, observation_matches_file_status, + reinspection_writer_observation, +}; +use super::{ + ActiveRecoveryAttempt, ActiveRecoveryCommand, BeginPersistenceRecoveryResult, + CancelPersistenceRecoveryResult, CheckoutPersistenceRecoveryHandleResult, LifecycleControl, + PersistenceAuthoritySnapshot, PersistenceIncident, PersistenceRecoveryAction, + PersistenceRecoveryHandle, PersistenceRecoveryRequest, RecoveryAttemptClient, + RecoveryAttemptKind, RecoveryBeginRejection, RecoveryCancellation, + RecoveryCancellationRejection, RecoveryCleanupState, RecoveryCommandExpectation, + RecoveryCompletionProtocolError, RecoveryCompletionReceiver, RecoveryHandleAvailability, + RecoveryHandleState, RecoveryIoCommand, RecoveryIoCompletion, RecoveryIoOperation, + RecoveryIoResult, SubmitPersistenceRecoveryResult, +}; +use std::collections::BTreeSet; impl RuntimeUiStateController { const RECOVERY_COMMAND_TOMBSTONE_LIMIT: usize = 1024; @@ -141,7 +167,7 @@ impl RuntimeUiStateController { RecoveryAttemptKind::ConfirmPreserveInvalidReset { confirmation } } }; - let (completion_tx, completion_rx) = std::sync::mpsc::channel(); + let (completion_tx, completion_rx) = mpsc::channel(); let active = ActiveRecoveryAttempt { id: attempt_id, incident: incident_id, @@ -236,24 +262,24 @@ impl RuntimeUiStateController { if active.incident != completion.incident || active.barrier != completion.barrier { return self.block_protocol_failure( RecoveryCompletionProtocolError::WrongIncidentOrBarrier, - completion, + &completion, ); } if active.id != completion.attempt { return self.block_protocol_failure( RecoveryCompletionProtocolError::UnknownAttempt, - completion, + &completion, ); } if active.current_command.id != completion.command_id { return self.block_protocol_failure( RecoveryCompletionProtocolError::UnknownCommand, - completion, + &completion, ); } let expectation = active.current_command.expected.clone(); if let Some(reason) = completion_protocol_error(&completion.result, &expectation) { - return self.block_protocol_failure(reason, completion); + return self.block_protocol_failure(reason, &completion); } let incident = active.incident; let attempt = active.id; @@ -269,7 +295,7 @@ impl RuntimeUiStateController { match completion.result { RecoveryIoResult::Inspected(result) => self.integrate_recovery_inspection(result), RecoveryIoResult::SourceMutation(result) if protocol_failure_pending => { - self.integrate_protocol_failure_source_completion(result) + self.integrate_protocol_failure_source_completion(&result) } RecoveryIoResult::SourceMutation(result) => { self.integrate_recovery_source_mutation(result) @@ -328,11 +354,11 @@ impl RuntimeUiStateController { pub(in crate::runtime_ui_state) fn enter_persistence_incident( &mut self, - error: RuntimeStateIoError, + error: &RuntimeStateIoError, active: Option, recovery_artifacts: Vec, path_effect: RuntimeStateFailurePathEffect, - failed_replacement: Option, + failed_replacement: Option<&HeldReplacementStage>, ) -> PersistenceIncidentId { let barrier = match self.active_barrier.as_ref() { Some(barrier) => barrier.id, @@ -367,10 +393,10 @@ impl RuntimeUiStateController { } else { self.pipeline.hold_all_pending_replacements() }; - let failed_desired_through = failed_replacement.as_ref().map(|stage| stage.through); - if let Some(failed_replacement) = &failed_replacement { + let failed_desired_through = failed_replacement.map(|stage| stage.through); + if let Some(failed_replacement) = failed_replacement { self.pipeline - .settle_held_failed(std::slice::from_ref(failed_replacement), error.clone()); + .settle_held_failed(slice::from_ref(failed_replacement), error); } let retry_desired_through = held_replacements .iter() @@ -450,9 +476,9 @@ impl RuntimeUiStateController { pub(in crate::runtime_ui_state) fn apply_lifecycle_control( &mut self, - control: LifecycleControl, + control: &LifecycleControl, ) { - match control { + match *control { LifecycleControl::ReturnRecoveryHandle { controller, incident, diff --git a/src/runtime_ui_state/recovery/preserve.rs b/src/runtime_ui_state/recovery/preserve.rs index 8431f513a..87d5291cc 100644 --- a/src/runtime_ui_state/recovery/preserve.rs +++ b/src/runtime_ui_state/recovery/preserve.rs @@ -1,4 +1,11 @@ -use super::*; +use super::super::{ + RuntimeStateFailurePathEffect, RuntimeStateIoError, RuntimeStateObservedEnvelope, + RuntimeStateObservedPathEffect, RuntimeStateSourceObservation, RuntimeStateSourceRevision, + RuntimeUiStateController, RuntimeUiWireState, SourceMutationResult, + validate_source_mutation_evidence, +}; +use super::protocol::{merge_artifacts, source_mutation_observation_for_protocol_error}; +use super::{RecoveryAttemptKind, SubmitPersistenceRecoveryResult}; impl RuntimeUiStateController { pub(super) fn integrate_preserve_invalid_source_mutation( @@ -55,7 +62,7 @@ impl RuntimeUiStateController { } let observation = if new_source.bytes().is_none() { RuntimeStateSourceObservation { - revision: new_source.clone(), + revision: new_source, envelope: RuntimeStateObservedEnvelope::Missing, } } else { diff --git a/src/runtime_ui_state/recovery/protocol.rs b/src/runtime_ui_state/recovery/protocol.rs index 3bec67065..0ab009440 100644 --- a/src/runtime_ui_state/recovery/protocol.rs +++ b/src/runtime_ui_state/recovery/protocol.rs @@ -1,10 +1,24 @@ -use super::*; +use super::super::{ + ControllerBarrierPhase, RecoveryAttemptId, RecoveryAttemptStep, RecoveryCommandId, + RecoveryHandleId, RecoveryLeaseNonce, RuntimeStateFailurePathEffect, + RuntimeStateObservedEnvelope, RuntimeStateObservedPathEffect, RuntimeStateRecoveryArtifact, + RuntimeStateSourceObservation, RuntimeUiFileStatus, RuntimeUiStateController, + SourceMutationResult, validate_source_mutation_evidence, +}; +use super::{ + ActiveRecoveryCommand, PersistenceIncident, PersistenceRecoveryEvidence, + PersistenceRecoveryHandle, PersistenceRecoveryResult, RecoveryAttemptKind, + RecoveryCleanupState, RecoveryCommandExpectation, RecoveryCompletionProtocolError, + RecoveryHandleAvailability, RecoveryHandleState, RecoveryIoCommand, RecoveryIoCompletion, + RecoveryIoOperation, RecoveryIoResult, SubmitPersistenceRecoveryResult, +}; +use std::collections::BTreeSet; impl RuntimeUiStateController { pub(super) fn block_protocol_failure( &mut self, reason: RecoveryCompletionProtocolError, - completion: RecoveryIoCompletion, + completion: &RecoveryIoCompletion, ) -> SubmitPersistenceRecoveryResult { self.record_rejected_recovery_completion(completion.clone()); if let RecoveryIoResult::SourceMutation(result) = &completion.result { diff --git a/src/runtime_ui_state/seeds.rs b/src/runtime_ui_state/seeds.rs index d4f9fd56a..109efece0 100644 --- a/src/runtime_ui_state/seeds.rs +++ b/src/runtime_ui_state/seeds.rs @@ -100,7 +100,7 @@ impl StagedSeedReload { pub(crate) fn stage( previous: Option<&Self>, current: &InteractionSeedRegistry, - seeds: ValidatedInteractionSeeds, + seeds: &ValidatedInteractionSeeds, ) -> Result { let mut registry = previous .map(|staged| staged.registry.clone()) @@ -185,7 +185,7 @@ impl InteractionSeedRegistry { pub(crate) fn update( &mut self, - seeds: ValidatedInteractionSeeds, + seeds: &ValidatedInteractionSeeds, ) -> Result, SeedRegistryError> { let mut next_registry = self.clone(); let changed = next_registry.update_in_place(seeds)?; @@ -195,7 +195,7 @@ impl InteractionSeedRegistry { fn update_in_place( &mut self, - seeds: ValidatedInteractionSeeds, + seeds: &ValidatedInteractionSeeds, ) -> Result, SeedRegistryError> { let mut targets = self.states.keys().cloned().collect::>(); targets.extend(seeds.values.keys().cloned()); @@ -214,7 +214,10 @@ impl InteractionSeedRegistry { changed.insert(target); } None => { - debug_assert!(next.is_some()); + debug_assert!( + next.is_some(), + "a target absent from the registry comes from the new seeds" + ); self.states.insert( target.clone(), SeedState { @@ -287,11 +290,11 @@ mod tests { fn staged_remove_then_same_seed_readd_retains_identity_tombstone() { let target = InteractionSeedTarget::BoardPin("board-4".to_string()); let current = InteractionSeedRegistry::from_validated(board_seeds(Some("board-4"), false)); - let removed = StagedSeedReload::stage(None, ¤t, board_seeds(None, false)).unwrap(); + let removed = StagedSeedReload::stage(None, ¤t, &board_seeds(None, false)).unwrap(); let restored = StagedSeedReload::stage( Some(&removed), ¤t, - board_seeds(Some("board-4"), false), + &board_seeds(Some("board-4"), false), ) .unwrap(); diff --git a/src/runtime_ui_state/source_revision.rs b/src/runtime_ui_state/source_revision.rs index 3696ea47e..3cda80867 100644 --- a/src/runtime_ui_state/source_revision.rs +++ b/src/runtime_ui_state/source_revision.rs @@ -1,5 +1,5 @@ use std::fmt; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -19,7 +19,7 @@ impl RuntimeStateResolvedParent { Self { path, identity } } - pub(crate) fn path(&self) -> &std::path::Path { + pub(crate) fn path(&self) -> &Path { &self.path } @@ -63,7 +63,7 @@ impl RuntimeStatePathIdentity { } } - pub(crate) fn source_path(&self) -> &std::path::Path { + pub(crate) fn source_path(&self) -> &Path { &self.source_path } @@ -198,10 +198,10 @@ pub(crate) struct RuntimeStateSourceObservation { impl RuntimeStateSourceObservation { pub(crate) fn missing(revision: RuntimeStateSourceRevision) -> Self { - debug_assert!(matches!( - revision, - RuntimeStateSourceRevision::Missing { .. } - )); + debug_assert!( + matches!(revision, RuntimeStateSourceRevision::Missing { .. }), + "a missing-source observation needs a missing revision, got {revision:?}" + ); Self { revision, envelope: RuntimeStateObservedEnvelope::Missing, diff --git a/src/runtime_ui_state/store.rs b/src/runtime_ui_state/store.rs index f89613b9a..2e5b9fd0b 100644 --- a/src/runtime_ui_state/store.rs +++ b/src/runtime_ui_state/store.rs @@ -52,7 +52,7 @@ impl RuntimeUiStateInspection { let (controller, incident) = RuntimeUiStateController::new_startup_unhealthy( seeds, self.observation, - RuntimeStateIoError::new("startup runtime-state file is malformed"), + &RuntimeStateIoError::new("startup runtime-state file is malformed"), Vec::new(), RuntimeStateFailurePathEffect::Known(RuntimeStateObservedPathEffect::Untouched), ); diff --git a/src/runtime_ui_state/store/fs.rs b/src/runtime_ui_state/store/fs.rs index 5754b22e4..5a2fad089 100644 --- a/src/runtime_ui_state/store/fs.rs +++ b/src/runtime_ui_state/store/fs.rs @@ -1,10 +1,12 @@ use std::ffi::{CString, OsStr, OsString}; use std::fs::{self, File, OpenOptions}; use std::io::{self, Write}; +use std::mem::MaybeUninit; use std::os::fd::{AsRawFd, FromRawFd}; use std::os::unix::ffi::OsStrExt; use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; use std::path::PathBuf; +use std::process; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; @@ -135,7 +137,7 @@ impl PinnedPath { fn exists_nofollow(&self) -> io::Result { let leaf = os_str_c_string(&self.leaf)?; - let mut stat = std::mem::MaybeUninit::::uninit(); + let mut stat = MaybeUninit::::uninit(); // SAFETY: the pointers are valid and stat points to writable storage. let result = unsafe { libc::fstatat( @@ -219,7 +221,7 @@ where let candidate = path.parent.join(OsString::from(format!( ".{}.wayscriber-{suffix}-{}-{id}", path.leaf.to_string_lossy(), - std::process::id() + process::id() )))?; if reserve(&candidate)? { return Ok(candidate); diff --git a/src/runtime_ui_state/store/inspection.rs b/src/runtime_ui_state/store/inspection.rs index 73facf009..b98e912f2 100644 --- a/src/runtime_ui_state/store/inspection.rs +++ b/src/runtime_ui_state/store/inspection.rs @@ -1,10 +1,12 @@ +use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; use std::io::{self, Read, Seek, SeekFrom}; use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; -use super::{MAX_RUNTIME_UI_FILE_BYTES, RuntimeUiStateInspection, fs::PinnedPath}; +use super::fs::PinnedPath; +use super::{MAX_RUNTIME_UI_FILE_BYTES, RuntimeUiStateInspection}; use crate::runtime_ui_state::{ RuntimeStateFileIdentity, RuntimeStateInspectionError, RuntimeStatePathIdentity, RuntimeStateResolvedParent, RuntimeStateSourceObservation, RuntimeStateSourceRevision, @@ -29,11 +31,11 @@ pub(super) fn inspect_path( RuntimeStateSourceRevision::missing(path_identity), )); } - Err(error) => return Err(inspection_error(&resolved_path, error)), + Err(error) => return Err(inspection_error(&resolved_path, &error)), }; let before = file .metadata() - .map_err(|error| inspection_error(&resolved_path, error))?; + .map_err(|error| inspection_error(&resolved_path, &error))?; if !before.file_type().is_file() { return Err(RuntimeStateInspectionError::new(format!( "runtime-state path is not a regular file: {}", @@ -55,13 +57,13 @@ pub(super) fn inspect_path( ))); } file.seek(SeekFrom::Start(0)) - .map_err(|error| inspection_error(&resolved_path, error))?; + .map_err(|error| inspection_error(&resolved_path, &error))?; let verification_bytes = read_bounded(&resolved_path, &mut file)?; let after = file .metadata() - .map_err(|error| inspection_error(&resolved_path, error))?; + .map_err(|error| inspection_error(&resolved_path, &error))?; let active = fs::symlink_metadata(&resolved_path) - .map_err(|error| inspection_error(&resolved_path, error))?; + .map_err(|error| inspection_error(&resolved_path, &error))?; if active.file_type().is_symlink() || !active.file_type().is_file() { return Err(RuntimeStateInspectionError::new(format!( "runtime-state path changed type during inspection: {}", @@ -113,11 +115,11 @@ pub(super) fn inspect_pinned( )), )); } - Err(error) => return Err(inspection_error(&reported_path, error)), + Err(error) => return Err(inspection_error(&reported_path, &error)), }; let before = file .metadata() - .map_err(|error| inspection_error(&reported_path, error))?; + .map_err(|error| inspection_error(&reported_path, &error))?; if !before.file_type().is_file() { return Err(RuntimeStateInspectionError::new(format!( "runtime-state path is not a regular file: {}", @@ -133,15 +135,15 @@ pub(super) fn inspect_pinned( let bytes = read_bounded(&reported_path, &mut file)?; file.seek(SeekFrom::Start(0)) - .map_err(|error| inspection_error(&reported_path, error))?; + .map_err(|error| inspection_error(&reported_path, &error))?; let verification_bytes = read_bounded(&reported_path, &mut file)?; let after = file .metadata() - .map_err(|error| inspection_error(&reported_path, error))?; + .map_err(|error| inspection_error(&reported_path, &error))?; let active = path .open_read() .and_then(|file| file.metadata()) - .map_err(|error| inspection_error(&reported_path, error))?; + .map_err(|error| inspection_error(&reported_path, &error))?; if !active.file_type().is_file() { return Err(RuntimeStateInspectionError::new(format!( "runtime-state path changed type during inspection: {}", @@ -177,7 +179,7 @@ pub(super) fn inspect_pinned( #[derive(Debug)] struct ResolvedParent { - resolved_path: std::path::PathBuf, + resolved_path: PathBuf, identity: RuntimeStateFileIdentity, } @@ -195,10 +197,10 @@ fn resolve_parent(path: &Path) -> Result, RuntimeStateIns let resolved_path = match fs::canonicalize(parent) { Ok(path) => path, Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None), - Err(error) => return Err(inspection_error(parent, error)), + Err(error) => return Err(inspection_error(parent, &error)), }; let metadata = - fs::metadata(&resolved_path).map_err(|error| inspection_error(&resolved_path, error))?; + fs::metadata(&resolved_path).map_err(|error| inspection_error(&resolved_path, &error))?; if !metadata.is_dir() { return Err(RuntimeStateInspectionError::new(format!( "runtime-state parent is not a directory: {}", @@ -236,7 +238,7 @@ fn source_parent(path: &Path) -> &Path { .unwrap_or_else(|| Path::new(".")) } -fn file_name(path: &Path) -> Result<&std::ffi::OsStr, RuntimeStateInspectionError> { +fn file_name(path: &Path) -> Result<&OsStr, RuntimeStateInspectionError> { path.file_name().ok_or_else(|| { RuntimeStateInspectionError::new(format!( "runtime-state path has no file name: {}", @@ -286,7 +288,7 @@ fn read_bounded(path: &Path, file: &mut File) -> Result, RuntimeStateIns file.by_ref() .take(MAX_RUNTIME_UI_FILE_BYTES + 1) .read_to_end(&mut bytes) - .map_err(|error| inspection_error(path, error))?; + .map_err(|error| inspection_error(path, &error))?; if bytes.len() as u64 > MAX_RUNTIME_UI_FILE_BYTES { return Err(RuntimeStateInspectionError::new(format!( "runtime-state file exceeds the {} byte limit", @@ -296,7 +298,7 @@ fn read_bounded(path: &Path, file: &mut File) -> Result, RuntimeStateIns Ok(bytes) } -fn inspection_error(path: &Path, error: io::Error) -> RuntimeStateInspectionError { +fn inspection_error(path: &Path, error: &io::Error) -> RuntimeStateInspectionError { RuntimeStateInspectionError::new(format!( "could not inspect runtime-state path {}: {error}", path.display() diff --git a/src/runtime_ui_state/store/mutation.rs b/src/runtime_ui_state/store/mutation.rs index cf5a2efa2..da8000b24 100644 --- a/src/runtime_ui_state/store/mutation.rs +++ b/src/runtime_ui_state/store/mutation.rs @@ -1,9 +1,7 @@ use super::{RuntimeUiStateInspection, RuntimeUiStateStore, fs as store_fs, inspection}; use crate::runtime_ui_state::{ - AcceptedStateRevision, RuntimeStateFailurePathEffect, RuntimeStateIoError, - RuntimeStateObservedPathEffect, RuntimeStatePathIdentity, RuntimeStatePostClaimPathEffect, - RuntimeStateRecoveryArtifact, RuntimeStateSourceObservation, RuntimeStateSourceRevision, - RuntimeUiFileStatus, SourceMutationId, SourceMutationKind, SourceMutationRequest, + AcceptedStateRevision, RuntimeStateFailurePathEffect, RuntimeStatePathIdentity, + RuntimeStateSourceRevision, SourceMutationId, SourceMutationKind, SourceMutationRequest, SourceMutationResult, encode_runtime_ui_file, }; @@ -12,7 +10,7 @@ mod replace; mod reset; mod result; -use result::*; +use result::{applied, failed, status_allowed, untouched}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MutationMode { @@ -32,9 +30,9 @@ struct MutationTarget { expected_path: RuntimeStatePathIdentity, } -struct PreparedReplacement<'a> { - temp: &'a mut store_fs::CleanupPath, - bytes: &'a [u8], +struct PreparedReplacement<'write> { + temp: &'write mut store_fs::CleanupPath, + bytes: &'write [u8], } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -108,7 +106,7 @@ impl RuntimeUiStateStore { accepted_through, expected_source, MutationMode::Replace, - Some(bytes), + Some(&bytes), hook, ) } @@ -148,7 +146,7 @@ impl RuntimeUiStateStore { applied_through: AcceptedStateRevision, expected: RuntimeStateSourceRevision, mode: MutationMode, - replacement: Option>, + replacement: Option<&[u8]>, hook: &mut dyn FnMut(MutationPoint), ) -> SourceMutationResult { if expected.path_identity().source_path() != self.path() { @@ -212,7 +210,7 @@ impl RuntimeUiStateStore { expected_path, }; - let mut temp = match replacement.as_deref() { + let mut temp = match replacement { Some(bytes) => match store_fs::create_synced_temp(&target.operation_path, bytes) { Ok(temp) => Some(temp), Err(error) => { @@ -250,9 +248,9 @@ impl RuntimeUiStateStore { id, applied_through, &target, - PreparedReplacement { + &mut PreparedReplacement { temp: temp.as_mut().expect("replacement temp"), - bytes: replacement.as_deref().expect("replacement bytes"), + bytes: replacement.expect("replacement bytes"), }, hook, ), @@ -342,9 +340,9 @@ impl RuntimeUiStateStore { quarantine, inspection: claimed, }, - PreparedReplacement { + &mut PreparedReplacement { temp: temp.as_mut().expect("replacement temp"), - bytes: replacement.as_deref().expect("replacement bytes"), + bytes: replacement.expect("replacement bytes"), }, hook, ), diff --git a/src/runtime_ui_state/store/mutation/conflict.rs b/src/runtime_ui_state/store/mutation/conflict.rs index 98ffd5a7f..a763c511d 100644 --- a/src/runtime_ui_state/store/mutation/conflict.rs +++ b/src/runtime_ui_state/store/mutation/conflict.rs @@ -1,4 +1,10 @@ -use super::*; +use super::super::{RuntimeUiStateInspection, RuntimeUiStateStore}; +use super::result::{artifact_for, failed, is_expected_missing, observation_changed_retained}; +use super::{ClaimedSource, MutationTarget, store_fs}; +use crate::runtime_ui_state::{ + RuntimeStateFailurePathEffect, RuntimeStateObservedPathEffect, RuntimeStatePostClaimPathEffect, + SourceMutationId, SourceMutationResult, +}; impl RuntimeUiStateStore { pub(super) fn restore_or_retain_changed_claim( @@ -14,13 +20,13 @@ impl RuntimeUiStateStore { match self.inspect() { Ok(active) if is_expected_missing(&active, &target.expected_path) => {} Ok(active) => { - return observation_changed_retained(id, active.observation, quarantine, claimed); + return observation_changed_retained(id, active.observation, &quarantine, &claimed); } Err(error) => { return self.retained_after_claim( id, - quarantine, - claimed, + &quarantine, + &claimed, format!( "could not verify the path before restoring changed runtime state: {}", error.message() @@ -62,8 +68,8 @@ impl RuntimeUiStateStore { } Err(error) => self.retained_after_claim( id, - quarantine, - claimed, + &quarantine, + &claimed, format!("could not restore changed claimed source without replacement: {error}"), ), } @@ -72,11 +78,11 @@ impl RuntimeUiStateStore { pub(super) fn retained_after_claim( &self, id: SourceMutationId, - quarantine: store_fs::PinnedPath, - claimed: RuntimeUiStateInspection, + quarantine: &store_fs::PinnedPath, + claimed: &RuntimeUiStateInspection, reason: impl Into, ) -> SourceMutationResult { - let artifacts = artifact_for(quarantine.clone(), claimed); + let artifacts = artifact_for(quarantine, claimed); if artifacts.is_empty() { return failed( id, diff --git a/src/runtime_ui_state/store/mutation/replace.rs b/src/runtime_ui_state/store/mutation/replace.rs index ff3a6e22e..93726257f 100644 --- a/src/runtime_ui_state/store/mutation/replace.rs +++ b/src/runtime_ui_state/store/mutation/replace.rs @@ -1,4 +1,12 @@ -use super::*; +use super::super::RuntimeUiStateStore; +use super::result::{ + applied, artifact_for, failed, is_expected_missing, observation_changed_retained, untouched, +}; +use super::{ClaimedSource, MutationPoint, MutationTarget, PreparedReplacement, store_fs}; +use crate::runtime_ui_state::{ + AcceptedStateRevision, RuntimeStateFailurePathEffect, RuntimeStatePathIdentity, + RuntimeUiFileStatus, SourceMutationId, SourceMutationResult, +}; impl RuntimeUiStateStore { pub(super) fn install_at_missing( @@ -6,7 +14,7 @@ impl RuntimeUiStateStore { id: SourceMutationId, applied_through: AcceptedStateRevision, target: &MutationTarget, - replacement: PreparedReplacement<'_>, + replacement: &mut PreparedReplacement<'_>, hook: &mut dyn FnMut(MutationPoint), ) -> SourceMutationResult { hook(MutationPoint::BeforeInstall); @@ -84,7 +92,7 @@ impl RuntimeUiStateStore { applied_through: AcceptedStateRevision, target: &MutationTarget, claimed: ClaimedSource, - replacement: PreparedReplacement<'_>, + replacement: &mut PreparedReplacement<'_>, hook: &mut dyn FnMut(MutationPoint), ) -> SourceMutationResult { let ClaimedSource { @@ -95,13 +103,13 @@ impl RuntimeUiStateStore { match self.inspect() { Ok(active) if is_expected_missing(&active, &target.expected_path) => {} Ok(active) => { - return observation_changed_retained(id, active.observation, quarantine, claimed); + return observation_changed_retained(id, active.observation, &quarantine, &claimed); } Err(error) => { return self.retained_after_claim( id, - quarantine, - claimed, + &quarantine, + &claimed, format!( "could not verify the claimed runtime-state path before install: {}", error.message() @@ -114,8 +122,8 @@ impl RuntimeUiStateStore { { return self.retained_after_claim( id, - quarantine, - claimed, + &quarantine, + &claimed, format!("active path changed before runtime-state install: {error}"), ); } @@ -126,7 +134,7 @@ impl RuntimeUiStateStore { id, format!("runtime-state replacement has an uncertain durability outcome: {error}"), self.inspect().ok().map(|inspection| inspection.observation), - artifact_for(quarantine, claimed), + artifact_for(&quarantine, &claimed), RuntimeStateFailurePathEffect::UnknownAfterMutation, ); } @@ -141,7 +149,7 @@ impl RuntimeUiStateStore { error.message() ), None, - artifact_for(quarantine, claimed), + artifact_for(&quarantine, &claimed), RuntimeStateFailurePathEffect::UnknownAfterMutation, ); } @@ -151,12 +159,12 @@ impl RuntimeUiStateStore { || active.observation.revision.path_identity() != &target.expected_path || active.observation.revision.bytes() != Some(replacement.bytes) { - return observation_changed_retained(id, active.observation, quarantine, claimed); + return observation_changed_retained(id, active.observation, &quarantine, &claimed); } let cleanup_artifact = match store_fs::remove_file(&quarantine) { Ok(()) => Vec::new(), - Err(_) => artifact_for(quarantine.clone(), claimed), + Err(_) => artifact_for(&quarantine, &claimed), }; if let Err(error) = target.operation_path.sync_parent() { return failed( diff --git a/src/runtime_ui_state/store/mutation/reset.rs b/src/runtime_ui_state/store/mutation/reset.rs index 23ef91bf8..aff43914a 100644 --- a/src/runtime_ui_state/store/mutation/reset.rs +++ b/src/runtime_ui_state/store/mutation/reset.rs @@ -1,4 +1,11 @@ -use super::*; +use super::super::RuntimeUiStateStore; +use super::result::{ + applied, artifact_for, failed, is_expected_missing, observation_changed_retained, +}; +use super::{ClaimedSource, MutationPoint, MutationTarget, store_fs}; +use crate::runtime_ui_state::{ + AcceptedStateRevision, RuntimeStateFailurePathEffect, SourceMutationId, SourceMutationResult, +}; impl RuntimeUiStateStore { pub(super) fn finish_reset_after_claim( @@ -24,20 +31,20 @@ impl RuntimeUiStateStore { error.message() ), None, - artifact_for(quarantine, claimed), + artifact_for(&quarantine, &claimed), RuntimeStateFailurePathEffect::UnknownAfterMutation, ); } }; if !is_expected_missing(&active, &target.expected_path) { - return observation_changed_retained(id, active.observation, quarantine, claimed); + return observation_changed_retained(id, active.observation, &quarantine, &claimed); } if let Err(error) = target.operation_path.sync_parent() { return failed( id, format!("runtime-state reset has an uncertain durability outcome: {error}"), Some(active.observation), - artifact_for(quarantine, claimed), + artifact_for(&quarantine, &claimed), RuntimeStateFailurePathEffect::UnknownAfterMutation, ); } @@ -54,7 +61,7 @@ impl RuntimeUiStateStore { error.message() ), None, - artifact_for(quarantine, claimed), + artifact_for(&quarantine, &claimed), RuntimeStateFailurePathEffect::UnknownAfterMutation, ); } @@ -63,11 +70,11 @@ impl RuntimeUiStateStore { return observation_changed_retained( id, final_active.observation, - quarantine, - claimed, + &quarantine, + &claimed, ); } - let artifacts = artifact_for(quarantine, claimed); + let artifacts = artifact_for(&quarantine, &claimed); if artifacts.is_empty() { return failed( id, @@ -87,7 +94,7 @@ impl RuntimeUiStateStore { let cleanup_artifact = match store_fs::remove_file(&quarantine) { Ok(()) => Vec::new(), - Err(_) => artifact_for(quarantine, claimed), + Err(_) => artifact_for(&quarantine, &claimed), }; if let Err(error) = target.operation_path.sync_parent() { return failed( diff --git a/src/runtime_ui_state/store/mutation/result.rs b/src/runtime_ui_state/store/mutation/result.rs index 9cddf8345..036e7c3c6 100644 --- a/src/runtime_ui_state/store/mutation/result.rs +++ b/src/runtime_ui_state/store/mutation/result.rs @@ -1,4 +1,11 @@ -use super::*; +use super::super::{RuntimeUiStateInspection, inspection}; +use super::{MutationMode, store_fs}; +use crate::runtime_ui_state::{ + AcceptedStateRevision, RuntimeStateFailurePathEffect, RuntimeStateIoError, + RuntimeStateObservedPathEffect, RuntimeStatePathIdentity, RuntimeStatePostClaimPathEffect, + RuntimeStateRecoveryArtifact, RuntimeStateSourceObservation, RuntimeStateSourceRevision, + RuntimeUiFileStatus, SourceMutationId, SourceMutationResult, +}; pub(super) fn status_allowed(mode: MutationMode, status: &RuntimeUiFileStatus) -> bool { match mode { @@ -18,15 +25,15 @@ pub(super) fn status_allowed(mode: MutationMode, status: &RuntimeUiFileStatus) - } pub(super) fn artifact_for( - path: store_fs::PinnedPath, - source_inspection: RuntimeUiStateInspection, + path: &store_fs::PinnedPath, + source_inspection: &RuntimeUiStateInspection, ) -> Vec { let source_path = source_inspection .observation .revision .path_identity() .clone(); - inspection::inspect_pinned(&path).map_or_else( + inspection::inspect_pinned(path).map_or_else( |_| Vec::new(), |mut inspection| { if inspection.observation.revision.bytes().is_none() { @@ -50,10 +57,10 @@ pub(super) fn artifact_for( pub(super) fn observation_changed_retained( id: SourceMutationId, active: RuntimeStateSourceObservation, - recovery_path: store_fs::PinnedPath, - claimed: RuntimeUiStateInspection, + recovery_path: &store_fs::PinnedPath, + claimed: &RuntimeUiStateInspection, ) -> SourceMutationResult { - let recovery_artifacts = artifact_for(recovery_path.clone(), claimed); + let recovery_artifacts = artifact_for(recovery_path, claimed); if recovery_artifacts.is_empty() { return failed( id, diff --git a/src/runtime_ui_state/store/tests.rs b/src/runtime_ui_state/store/tests.rs index 4bd49df80..fc2c5ae29 100644 --- a/src/runtime_ui_state/store/tests.rs +++ b/src/runtime_ui_state/store/tests.rs @@ -2,6 +2,9 @@ use std::ffi::CString; use std::fs; use std::os::unix::ffi::OsStrExt; use std::os::unix::fs::{PermissionsExt, symlink}; +use std::time::{Duration, Instant}; + +use crate::test_temp; use super::mutation::MutationPoint; use super::*; @@ -40,7 +43,7 @@ fn request( fn commit_top_pinned(controller: &mut RuntimeUiStateController) -> SourceMutationRequest { let permit = controller - .begin_mutation(RuntimeUiMutationScope::one( + .begin_mutation(&RuntimeUiMutationScope::one( InteractionSeedTarget::TopPinned, )) .unwrap(); @@ -50,7 +53,7 @@ fn commit_top_pinned(controller: &mut RuntimeUiStateController) -> SourceMutatio ) .unwrap(); assert!(matches!( - controller.commit(permit, values), + controller.commit(permit, &values), CommitResult::Accepted { .. } )); controller.take_source_mutation().expect("source mutation") @@ -58,7 +61,7 @@ fn commit_top_pinned(controller: &mut RuntimeUiStateController) -> SourceMutatio #[test] fn supported_write_connects_controller_to_restart_inspection() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let store = RuntimeUiStateStore::new(&path); let mut controller = store @@ -89,7 +92,7 @@ fn supported_write_connects_controller_to_restart_inspection() { #[test] fn external_change_before_missing_write_wins_without_overwrite() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let store = RuntimeUiStateStore::new(&path); let mut controller = store @@ -110,7 +113,7 @@ fn external_change_before_missing_write_wins_without_overwrite() { #[test] fn replacing_same_bytes_with_a_new_inode_is_a_conflict() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let bytes = encode_runtime_ui_file(&RuntimeUiWireState::default()).unwrap(); fs::write(&path, &bytes).unwrap(); @@ -134,7 +137,7 @@ fn replacing_same_bytes_with_a_new_inode_is_a_conflict() { #[test] fn active_file_appearing_after_claim_is_not_overwritten() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let original = encode_runtime_ui_file(&RuntimeUiWireState::default()).unwrap(); fs::write(&path, &original).unwrap(); @@ -174,7 +177,7 @@ fn active_file_appearing_after_claim_is_not_overwritten() { #[test] fn unsupported_reset_preserves_exact_bytes_as_an_artifact() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let unsupported = b"version = 9\nfuture = [1, 2, 3]\n"; fs::write(&path, unsupported).unwrap(); @@ -212,7 +215,7 @@ fn unsupported_reset_preserves_exact_bytes_as_an_artifact() { #[test] fn invalid_reset_preserves_exact_bytes_as_an_artifact() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let invalid = b"this is not valid TOML\n"; fs::write(&path, invalid).unwrap(); @@ -237,7 +240,7 @@ fn invalid_reset_preserves_exact_bytes_as_an_artifact() { #[test] fn target_symlink_is_rejected_and_neither_path_is_modified() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let target = temp.path().join("external.toml"); let path = temp.path().join("runtime-ui.toml"); let external = b"version = 17\n"; @@ -252,19 +255,19 @@ fn target_symlink_is_rejected_and_neither_path_is_modified() { #[test] fn fifo_is_rejected_without_blocking_for_a_writer() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let c_path = CString::new(path.as_os_str().as_bytes()).unwrap(); // SAFETY: the CString is NUL-terminated and remains live for the call. assert_eq!(unsafe { libc::mkfifo(c_path.as_ptr(), 0o600) }, 0); - let started = std::time::Instant::now(); + let started = Instant::now(); assert!(RuntimeUiStateStore::new(path).inspect().is_err()); - assert!(started.elapsed() < std::time::Duration::from_secs(1)); + assert!(started.elapsed() < Duration::from_secs(1)); } #[test] fn uncertain_post_install_result_blocks_the_controller() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let store = RuntimeUiStateStore::new(&path); let mut controller = store @@ -295,7 +298,7 @@ fn uncertain_post_install_result_blocks_the_controller() { #[test] fn oversized_file_is_rejected_before_parsing() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); fs::write(&path, vec![b'x'; (MAX_RUNTIME_UI_FILE_BYTES + 1) as usize]).unwrap(); assert!(RuntimeUiStateStore::new(path).inspect().is_err()); @@ -303,7 +306,7 @@ fn oversized_file_is_rejected_before_parsing() { #[test] fn preparation_failure_reports_an_untouched_failure() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("missing-parent/runtime-ui.toml"); let store = RuntimeUiStateStore::new(&path); let expected = store.inspect().unwrap().observation.revision; @@ -326,7 +329,7 @@ fn preparation_failure_reports_an_untouched_failure() { #[test] fn missing_source_install_error_with_unchanged_source_is_an_untouched_failure() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let store = RuntimeUiStateStore::new(&path); let mut controller = store @@ -378,7 +381,7 @@ fn missing_source_install_error_with_unchanged_source_is_an_untouched_failure() #[test] fn malformed_startup_enters_the_recovery_barrier() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); fs::write(&path, b"not valid TOML").unwrap(); let mut bootstrap = RuntimeUiStateStore::new(path) @@ -403,7 +406,7 @@ fn malformed_startup_enters_the_recovery_barrier() { #[test] fn unrelated_controller_write_preserves_supported_unknown_fields() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); fs::write( &path, @@ -430,7 +433,7 @@ future_boards = true .into_controller_bootstrap(seeds()) .controller; let permit = controller - .begin_mutation(RuntimeUiMutationScope::one( + .begin_mutation(&RuntimeUiMutationScope::one( InteractionSeedTarget::TopMinimized, )) .unwrap(); @@ -440,7 +443,7 @@ future_boards = true ) .unwrap(); assert!(matches!( - controller.commit(permit, values), + controller.commit(permit, &values), CommitResult::Accepted { .. } )); let result = store.execute_source_mutation(controller.take_source_mutation().unwrap()); @@ -460,7 +463,7 @@ future_boards = true #[test] fn ordinary_commands_cannot_overwrite_unsupported_or_invalid_files() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); for (name, bytes) in [ ( "unsupported.toml", @@ -485,7 +488,7 @@ fn ordinary_commands_cannot_overwrite_unsupported_or_invalid_files() { #[test] fn symlink_retarget_after_inspection_is_rejected_without_touching_target() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let target = temp.path().join("external.toml"); let supported = encode_runtime_ui_file(&RuntimeUiWireState::default()).unwrap(); @@ -509,7 +512,7 @@ fn symlink_retarget_after_inspection_is_rejected_without_touching_target() { #[test] fn supported_reset_removes_only_the_exact_confirmed_source() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); fs::write( &path, @@ -538,7 +541,7 @@ fn supported_reset_removes_only_the_exact_confirmed_source() { #[test] fn reset_conflict_after_claim_preserves_both_files() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let original = encode_runtime_ui_file(&RuntimeUiWireState::default()).unwrap(); let external = b"version = 44\nexternal = true\n"; @@ -568,7 +571,7 @@ fn reset_conflict_after_claim_preserves_both_files() { #[test] fn stale_board_ids_are_pruned_by_startup_cleanup() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); fs::write( &path, @@ -596,7 +599,7 @@ value = true #[test] fn changed_claim_is_restored_without_overwriting_another_path() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let original = encode_runtime_ui_file(&RuntimeUiWireState::default()).unwrap(); let changed = b"version = 61\nchanged_after_claim = true\n"; @@ -643,7 +646,7 @@ fn changed_claim_is_restored_without_overwriting_another_path() { #[test] fn disappeared_claim_is_not_reported_as_a_recovery_artifact() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let original = encode_runtime_ui_file(&RuntimeUiWireState::default()).unwrap(); fs::write(&path, original).unwrap(); @@ -684,7 +687,7 @@ fn disappeared_claim_is_not_reported_as_a_recovery_artifact() { #[test] fn confirmed_unsupported_reset_runs_end_to_end_through_the_controller() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let unsupported = b"version = 73\nfuture = 'preserve me'\n"; fs::write(&path, unsupported).unwrap(); @@ -739,7 +742,7 @@ fn confirmed_unsupported_reset_runs_end_to_end_through_the_controller() { #[test] fn missing_source_rejects_parent_symlink_retarget_before_write() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let first_parent = temp.path().join("first"); let second_parent = temp.path().join("second"); let selected_parent = temp.path().join("selected"); @@ -768,7 +771,7 @@ fn missing_source_rejects_parent_symlink_retarget_before_write() { #[test] fn missing_source_rejects_replacement_of_the_resolved_parent_directory() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let parent = temp.path().join("state"); let displaced_parent = temp.path().join("displaced-state"); fs::create_dir(&parent).unwrap(); @@ -794,7 +797,7 @@ fn missing_source_rejects_replacement_of_the_resolved_parent_directory() { #[test] fn parent_symlink_retarget_after_claim_retains_the_original_artifact() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let first_parent = temp.path().join("first"); let second_parent = temp.path().join("second"); let selected_parent = temp.path().join("selected"); @@ -833,7 +836,7 @@ fn parent_symlink_retarget_after_claim_retains_the_original_artifact() { #[test] fn resolved_parent_replacement_after_claim_retains_the_original_artifact() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let parent = temp.path().join("state"); let displaced_parent = temp.path().join("displaced-state"); fs::create_dir(&parent).unwrap(); @@ -878,7 +881,7 @@ fn resolved_parent_replacement_after_claim_retains_the_original_artifact() { #[test] fn removed_override_does_not_resurrect_its_entry_passthrough() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); fs::write( &path, @@ -899,7 +902,7 @@ future_entry = "must not return" .controller; let permit = controller - .begin_mutation(RuntimeUiMutationScope::one( + .begin_mutation(&RuntimeUiMutationScope::one( InteractionSeedTarget::TopPinned, )) .unwrap(); @@ -909,7 +912,7 @@ future_entry = "must not return" ) .unwrap(); assert!(matches!( - controller.commit(permit, configured), + controller.commit(permit, &configured), CommitResult::Accepted { .. } )); let removed = store.execute_source_mutation(controller.take_source_mutation().unwrap()); @@ -931,7 +934,7 @@ future_entry = "must not return" #[test] fn seed_reconciliation_does_not_resurrect_pruned_entry_passthrough() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); fs::write( &path, @@ -965,7 +968,7 @@ future_entry = "must not return" .unwrap(); assert!(matches!( - controller.update_seeds(changed_seeds), + controller.update_seeds(&changed_seeds), UpdateSeedsResult::Applied { cleanup_through: Some(_), .. @@ -977,7 +980,7 @@ future_entry = "must not return" SubmitSourceMutationResult::Integrated { .. } )); assert!(matches!( - controller.update_seeds(seeds()), + controller.update_seeds(&seeds()), UpdateSeedsResult::Applied { .. } )); diff --git a/src/runtime_ui_state/tests.rs b/src/runtime_ui_state/tests.rs index 4ee8e9053..2021ab53d 100644 --- a/src/runtime_ui_state/tests.rs +++ b/src/runtime_ui_state/tests.rs @@ -101,10 +101,10 @@ fn commit_bool( value: bool, ) -> AcceptedStateRevision { let permit = controller - .begin_mutation(RuntimeUiMutationScope::one(target.clone())) + .begin_mutation(&RuntimeUiMutationScope::one(target.clone())) .unwrap(); let values = RuntimeUiMutationValues::one(target, InteractionSeedValue::Bool(value)).unwrap(); - match controller.commit(permit, values) { + match controller.commit(permit, &values) { CommitResult::Accepted { through } => through, result => panic!("expected accepted mutation, got {result:?}"), } diff --git a/src/runtime_ui_state/tests/invalid_recovery.rs b/src/runtime_ui_state/tests/invalid_recovery.rs index 5f41a69b6..2cc3392ba 100644 --- a/src/runtime_ui_state/tests/invalid_recovery.rs +++ b/src/runtime_ui_state/tests/invalid_recovery.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use super::*; #[test] @@ -99,7 +101,7 @@ fn confirmed_invalid_reset_is_observation_bound_and_retains_artifact() { RecoveryIoOperation::PreserveInvalidIfUnchanged { mutation_id, .. } => mutation_id, operation => panic!("unexpected operation: {operation:?}"), }; - let artifact_path = std::path::PathBuf::from("/tmp/wayscriber-invalid-preserved"); + let artifact_path = PathBuf::from("/tmp/wayscriber-invalid-preserved"); let missing = missing_revision(); assert!(matches!( controller.submit_persistence_recovery_io(RecoveryIoCompletion { @@ -481,7 +483,7 @@ fn preserve_invalid_that_reports_a_present_source_remains_unhealthy() { fn preserve_invalid_rejects_duplicate_recovery_artifact_paths() { let (mut controller, _, incident, client, preserve, mutation_id) = begin_confirmed_invalid_preserve(); - let duplicate_path = std::path::PathBuf::from("/tmp/duplicate-preserved-invalid"); + let duplicate_path = PathBuf::from("/tmp/duplicate-preserved-invalid"); assert!(matches!( controller.submit_persistence_recovery_io(RecoveryIoCompletion { controller_id: controller.id(), @@ -522,7 +524,7 @@ fn retry_does_not_overwrite_an_exact_invalid_source() { let (mut controller, incident) = RuntimeUiStateController::new_startup_unhealthy( test_seeds(false, false), invalid.clone(), - RuntimeStateIoError::new("malformed startup state"), + &RuntimeStateIoError::new("malformed startup state"), Vec::new(), RuntimeStateFailurePathEffect::Known(RuntimeStateObservedPathEffect::Untouched), ); diff --git a/src/runtime_ui_state/tests/permits.rs b/src/runtime_ui_state/tests/permits.rs index f49221815..066710ff8 100644 --- a/src/runtime_ui_state/tests/permits.rs +++ b/src/runtime_ui_state/tests/permits.rs @@ -4,18 +4,18 @@ use super::*; fn coalesced_a_to_b_to_a_reload_invalidates_pre_barrier_permit() { let mut controller = controller(); let stale = controller - .begin_mutation(RuntimeUiMutationScope::one( + .begin_mutation(&RuntimeUiMutationScope::one( InteractionSeedTarget::TopPinned, )) .unwrap(); commit_bool(&mut controller, InteractionSeedTarget::TopMinimized, true); let (_, incident) = fail_current_replace(&mut controller, "temporary"); assert!(matches!( - controller.update_seeds(test_seeds(true, false)), + controller.update_seeds(&test_seeds(true, false)), UpdateSeedsResult::StagedBehindBarrier { .. } )); assert!(matches!( - controller.update_seeds(test_seeds(false, false)), + controller.update_seeds(&test_seeds(false, false)), UpdateSeedsResult::StagedBehindBarrier { replaced_older_staged_reload: true, .. @@ -72,7 +72,7 @@ fn coalesced_a_to_b_to_a_reload_invalidates_pre_barrier_permit() { ) .unwrap(); assert!(matches!( - controller.commit(stale, values), + controller.commit(stale, &values), CommitResult::RejectedSeedChanged { targets } if targets == vec![InteractionSeedTarget::TopPinned] )); @@ -111,17 +111,17 @@ fn accepted_mutation_uses_controller_seed_and_exact_source_revision() { fn relevant_seed_change_rejects_without_revision_or_write() { let mut controller = controller(); let permit = controller - .begin_mutation(RuntimeUiMutationScope::one( + .begin_mutation(&RuntimeUiMutationScope::one( InteractionSeedTarget::TopPinned, )) .unwrap(); - let result = controller.update_seeds(test_seeds(true, false)); + let result = controller.update_seeds(&test_seeds(true, false)); assert!(matches!(result, UpdateSeedsResult::Applied { .. })); let accepted_before = controller.pipeline().latest_accepted(); let result = controller.commit( permit, - RuntimeUiMutationValues::one( + &RuntimeUiMutationValues::one( InteractionSeedTarget::TopPinned, InteractionSeedValue::Bool(true), ) @@ -141,7 +141,7 @@ fn relevant_seed_change_rejects_without_revision_or_write() { fn seed_reversal_still_rejects_an_old_permit() { let mut controller = controller(); let permit = controller - .begin_mutation(RuntimeUiMutationScope::one( + .begin_mutation(&RuntimeUiMutationScope::one( InteractionSeedTarget::TopPinned, )) .unwrap(); @@ -151,8 +151,8 @@ fn seed_reversal_still_rejects_an_old_permit() { .unwrap() .generation(); - controller.update_seeds(test_seeds(true, false)); - controller.update_seeds(test_seeds(false, false)); + controller.update_seeds(&test_seeds(true, false)); + controller.update_seeds(&test_seeds(false, false)); assert_eq!( controller @@ -165,7 +165,7 @@ fn seed_reversal_still_rejects_an_old_permit() { assert!(matches!( controller.commit( permit, - RuntimeUiMutationValues::one( + &RuntimeUiMutationValues::one( InteractionSeedTarget::TopPinned, InteractionSeedValue::Bool(true), ) @@ -179,17 +179,17 @@ fn seed_reversal_still_rejects_an_old_permit() { fn unrelated_seed_change_keeps_permit_valid() { let mut controller = controller(); let permit = controller - .begin_mutation(RuntimeUiMutationScope::one( + .begin_mutation(&RuntimeUiMutationScope::one( InteractionSeedTarget::TopPinned, )) .unwrap(); - controller.update_seeds(test_seeds(false, true)); + controller.update_seeds(&test_seeds(false, true)); assert!(matches!( controller.commit( permit, - RuntimeUiMutationValues::one( + &RuntimeUiMutationValues::one( InteractionSeedTarget::TopPinned, InteractionSeedValue::Bool(true), ) @@ -203,16 +203,16 @@ fn unrelated_seed_change_keeps_permit_valid() { fn atomic_batch_rejects_when_one_guard_changes() { let mut controller = controller(); let permit = controller - .begin_mutation(RuntimeUiMutationScope::batch([ + .begin_mutation(&RuntimeUiMutationScope::batch([ InteractionSeedTarget::TopPinned, InteractionSeedTarget::TopMinimized, ])) .unwrap(); - controller.update_seeds(test_seeds(false, true)); + controller.update_seeds(&test_seeds(false, true)); let result = controller.commit( permit, - RuntimeUiMutationValues::batch([ + &RuntimeUiMutationValues::batch([ ( InteractionSeedTarget::TopPinned, InteractionSeedValue::Bool(true), @@ -235,14 +235,14 @@ fn removed_and_reused_board_target_rejects_old_permit() { let mut controller = controller(); let target = InteractionSeedTarget::BoardPin("board-6".to_string()); let permit = controller - .begin_mutation(RuntimeUiMutationScope::one(target.clone())) + .begin_mutation(&RuntimeUiMutationScope::one(target.clone())) .unwrap(); let old_generation = controller.seeds().state(&target).unwrap().generation(); let mut removed = test_seeds(false, false); removed.remove(&target); - controller.update_seeds(removed); - controller.update_seeds(test_seeds(false, false)); + controller.update_seeds(&removed); + controller.update_seeds(&test_seeds(false, false)); assert_eq!( controller.seeds().state(&target).unwrap().generation(), @@ -251,7 +251,7 @@ fn removed_and_reused_board_target_rejects_old_permit() { assert!(matches!( controller.commit( permit, - RuntimeUiMutationValues::one(target, InteractionSeedValue::Bool(true)).unwrap(), + &RuntimeUiMutationValues::one(target, InteractionSeedValue::Bool(true)).unwrap(), ), CommitResult::RejectedSeedChanged { .. } )); @@ -261,15 +261,15 @@ fn removed_and_reused_board_target_rejects_old_permit() { fn position_permit_survives_an_unrelated_seed_reload() { let mut controller = controller(); let top = controller - .begin_mutation(RuntimeUiMutationScope::one( + .begin_mutation(&RuntimeUiMutationScope::one( InteractionSeedTarget::TopPosition, )) .unwrap(); - controller.update_seeds(test_seeds(false, true)); + controller.update_seeds(&test_seeds(false, true)); assert!(matches!( controller.commit( top, - RuntimeUiMutationValues::one( + &RuntimeUiMutationValues::one( InteractionSeedTarget::TopPosition, InteractionSeedValue::Position(ToolbarPositionSeed::new(11.0, 21.0).unwrap()), ) diff --git a/src/runtime_ui_state/tests/recovery_basics.rs b/src/runtime_ui_state/tests/recovery_basics.rs index 6180690b6..7a67799b1 100644 --- a/src/runtime_ui_state/tests/recovery_basics.rs +++ b/src/runtime_ui_state/tests/recovery_basics.rs @@ -113,7 +113,7 @@ fn recovery_inspection_is_split_phase_and_reload_is_staged_while_io_is_held() { assert!(matches!(command.operation, RecoveryIoOperation::Inspect)); assert!(matches!( - controller.update_seeds(test_seeds(false, true)), + controller.update_seeds(&test_seeds(false, true)), UpdateSeedsResult::StagedBehindBarrier { .. } )); let result = controller.submit_persistence_recovery_io(RecoveryIoCompletion { @@ -415,7 +415,7 @@ fn startup_invalid_authority_begins_with_a_recoverable_barrier() { let (mut controller, incident) = RuntimeUiStateController::new_startup_unhealthy( test_seeds(false, false), invalid, - RuntimeStateIoError::new("malformed runtime state"), + &RuntimeStateIoError::new("malformed runtime state"), Vec::new(), RuntimeStateFailurePathEffect::Known(RuntimeStateObservedPathEffect::Untouched), ); @@ -508,12 +508,12 @@ fn startup_reconciles_stale_overrides_and_queues_pruning() { InteractionSeedValue::ItemOrder(vec![item_ids::TOP_TOOL_MARKER, item_ids::TOP_TOOL_PEN]); let mut source = controller(); let permit = source - .begin_mutation(RuntimeUiMutationScope::one(target.clone())) + .begin_mutation(&RuntimeUiMutationScope::one(target.clone())) .unwrap(); assert!(matches!( source.commit( permit, - RuntimeUiMutationValues::one(target.clone(), stale_value).unwrap(), + &RuntimeUiMutationValues::one(target.clone(), stale_value).unwrap(), ), CommitResult::Accepted { .. } )); diff --git a/src/runtime_ui_state/tests/recovery_cleanup.rs b/src/runtime_ui_state/tests/recovery_cleanup.rs index a97bc44ea..7b6b45bd7 100644 --- a/src/runtime_ui_state/tests/recovery_cleanup.rs +++ b/src/runtime_ui_state/tests/recovery_cleanup.rs @@ -10,11 +10,11 @@ fn recovery_prunes_override_when_board_id_is_removed_and_readded_with_same_seed( let mut removed = test_seeds(false, false); removed.remove(&target); assert!(matches!( - controller.update_seeds(removed), + controller.update_seeds(&removed), UpdateSeedsResult::StagedBehindBarrier { .. } )); assert!(matches!( - controller.update_seeds(test_seeds(false, false)), + controller.update_seeds(&test_seeds(false, false)), UpdateSeedsResult::StagedBehindBarrier { .. } )); @@ -80,7 +80,7 @@ fn recovery_captures_replacements_fenced_by_a_flush_before_closing_barrier() { let flush = controller.request_flush(second).unwrap(); let (_, incident) = fail_current_replace(&mut controller, "first write failed"); assert!(matches!( - controller.update_seeds(test_seeds(true, false)), + controller.update_seeds(&test_seeds(true, false)), UpdateSeedsResult::StagedBehindBarrier { .. } )); @@ -237,7 +237,7 @@ fn reload_cleanup_is_written_even_when_failed_reset_has_no_retry_snapshot() { controller.receipt(reset_through), Some(DurabilityOutcome::Failed(_)) )); - controller.update_seeds(test_seeds(true, false)); + controller.update_seeds(&test_seeds(true, false)); let (_client, inspection) = begin_recovery( &mut controller, @@ -300,7 +300,7 @@ fn reload_during_cleanup_acknowledgement_recomputes_and_writes_again() { SubmitSourceMutationResult::PersistenceUnhealthy { incident, .. } => incident, result => panic!("unexpected reset failure: {result:?}"), }; - controller.update_seeds(test_seeds(true, false)); + controller.update_seeds(&test_seeds(true, false)); let (client, inspection) = begin_recovery( &mut controller, incident, @@ -322,7 +322,7 @@ fn reload_during_cleanup_acknowledgement_recomputes_and_writes_again() { operation => panic!("unexpected operation: {operation:?}"), }; assert!(matches!( - controller.update_seeds(test_seeds(true, true)), + controller.update_seeds(&test_seeds(true, true)), UpdateSeedsResult::StagedBehindBarrier { .. } )); assert!(matches!( @@ -406,7 +406,7 @@ fn failed_recovery_cleanup_receipt_stays_failed_and_retry_allocates_a_new_one() SubmitSourceMutationResult::PersistenceUnhealthy { incident, .. } => incident, result => panic!("unexpected reset failure: {result:?}"), }; - controller.update_seeds(test_seeds(true, false)); + controller.update_seeds(&test_seeds(true, false)); let (client, inspection) = begin_recovery( &mut controller, incident, @@ -521,7 +521,7 @@ fn external_conflict_pruning_keeps_recovery_barrier_until_cleanup_ack() { .take_recovery_io_command() .expect("external authority reinspection"); assert!(matches!( - controller.update_seeds(test_seeds(true, false)), + controller.update_seeds(&test_seeds(true, false)), UpdateSeedsResult::StagedBehindBarrier { .. } )); let authority = observation(present_revision("fresh-external")); @@ -600,7 +600,7 @@ fn external_reinspection_settles_an_abandoned_cleanup_receipt_after_reload() { SubmitSourceMutationResult::PersistenceUnhealthy { incident, .. } => incident, result => panic!("unexpected prerequisite failure: {result:?}"), }; - controller.update_seeds(test_seeds(true, false)); + controller.update_seeds(&test_seeds(true, false)); let (client, inspection) = begin_recovery( &mut controller, incident, @@ -637,7 +637,7 @@ fn external_reinspection_settles_an_abandoned_cleanup_receipt_after_reload() { result => panic!("unexpected invalid acknowledgement result: {result:?}"), }; assert!(matches!( - controller.update_seeds(test_seeds(true, true)), + controller.update_seeds(&test_seeds(true, true)), UpdateSeedsResult::StagedBehindBarrier { .. } )); let retry = match controller.begin_persistence_recovery(PersistenceRecoveryRequest { @@ -695,7 +695,7 @@ fn preserve_invalid_settles_an_abandoned_cleanup_receipt() { SubmitSourceMutationResult::PersistenceUnhealthy { incident, .. } => incident, result => panic!("unexpected prerequisite failure: {result:?}"), }; - controller.update_seeds(test_seeds(true, false)); + controller.update_seeds(&test_seeds(true, false)); let (client, inspection) = begin_recovery( &mut controller, incident, diff --git a/src/runtime_ui_state/tests/recovery_conflicts.rs b/src/runtime_ui_state/tests/recovery_conflicts.rs index cb7bf847d..0730d8844 100644 --- a/src/runtime_ui_state/tests/recovery_conflicts.rs +++ b/src/runtime_ui_state/tests/recovery_conflicts.rs @@ -446,7 +446,7 @@ fn reload_during_external_cleanup_is_applied_before_reconciliation_closes() { result => panic!("unexpected result: {result:?}"), }; assert!(matches!( - controller.update_seeds(test_seeds(true, false)), + controller.update_seeds(&test_seeds(true, false)), UpdateSeedsResult::StagedBehindBarrier { .. } )); let RuntimeUiWireState { model, passthrough } = wire_with_top_pinned(true); @@ -462,7 +462,7 @@ fn reload_during_external_cleanup_is_applied_before_reconciliation_closes() { assert!(installed.cleanup_through.is_some()); let cleanup = controller.take_source_mutation().unwrap(); assert!(matches!( - controller.update_seeds(test_seeds(false, false)), + controller.update_seeds(&test_seeds(false, false)), UpdateSeedsResult::StagedBehindBarrier { .. } )); @@ -511,7 +511,7 @@ fn shutdown_during_external_cleanup_closes_after_a_late_reload() { controller.finish_preview(preview, RuntimePreviewFinishIntent::Cancel), PreviewFinishResult::AbandonedDuringBarrier { barrier } ); - controller.update_seeds(test_seeds(true, false)); + controller.update_seeds(&test_seeds(true, false)); let RuntimeUiWireState { model, passthrough } = external_wire; controller .install_external_authority( @@ -523,7 +523,7 @@ fn shutdown_during_external_cleanup_closes_after_a_late_reload() { ) .unwrap(); let cleanup = controller.take_source_mutation().unwrap(); - controller.update_seeds(test_seeds(true, true)); + controller.update_seeds(&test_seeds(true, true)); controller.request_shutdown().unwrap(); apply_request(&mut controller, &cleanup, present_revision("cleaned")); diff --git a/src/runtime_ui_state/tests/recovery_lifecycle.rs b/src/runtime_ui_state/tests/recovery_lifecycle.rs index 0b78895c0..ee931c081 100644 --- a/src/runtime_ui_state/tests/recovery_lifecycle.rs +++ b/src/runtime_ui_state/tests/recovery_lifecycle.rs @@ -404,7 +404,7 @@ fn cancelled_recovery_applies_reload_and_retains_cleanup_work() { CancelPersistenceRecoveryResult::PendingIrrevocableIo { .. } )); assert!(matches!( - controller.update_seeds(test_seeds(true, false)), + controller.update_seeds(&test_seeds(true, false)), UpdateSeedsResult::StagedBehindBarrier { .. } )); let applied = present_revision("applied-before-cancel-with-reload"); diff --git a/src/runtime_ui_state/tests/reset_preview.rs b/src/runtime_ui_state/tests/reset_preview.rs index 063c81de9..1435976bc 100644 --- a/src/runtime_ui_state/tests/reset_preview.rs +++ b/src/runtime_ui_state/tests/reset_preview.rs @@ -41,7 +41,7 @@ fn supported_reset_rejects_a_nonmissing_acknowledgement_without_partial_publish( fn supported_reset_waits_for_in_flight_write_and_publishes_epoch_on_ack() { let mut controller = controller(); let old_permit = controller - .begin_mutation(RuntimeUiMutationScope::one( + .begin_mutation(&RuntimeUiMutationScope::one( InteractionSeedTarget::TopPinned, )) .unwrap(); @@ -59,7 +59,7 @@ fn supported_reset_waits_for_in_flight_write_and_publishes_epoch_on_ack() { }; assert_eq!(controller.authority_epoch(), old_epoch); assert!(matches!( - controller.begin_mutation(RuntimeUiMutationScope::one( + controller.begin_mutation(&RuntimeUiMutationScope::one( InteractionSeedTarget::TopPinned )), Err(BeginMutationError::ControllerBusy(id)) if id == barrier @@ -103,7 +103,7 @@ fn supported_reset_waits_for_in_flight_write_and_publishes_epoch_on_ack() { assert!(matches!( controller.commit( old_permit, - RuntimeUiMutationValues::one( + &RuntimeUiMutationValues::one( InteractionSeedTarget::TopPinned, InteractionSeedValue::Bool(true) ) @@ -228,7 +228,7 @@ fn cancelled_unsupported_reset_confirmation_is_single_use_and_has_no_side_effect let replay = confirmation.clone(); assert_eq!( - controller.cancel_unsupported_reset_confirmation(confirmation), + controller.cancel_unsupported_reset_confirmation(&confirmation), CancelUnsupportedResetConfirmationResult::Cancelled ); assert_eq!( @@ -277,7 +277,7 @@ fn confirmed_unsupported_reset_uses_the_exact_source_and_publishes_only_after_ac let reset = controller .take_source_mutation() .expect("unsupported reset"); - assert_eq!(reset.expected_source, source.clone()); + assert_eq!(reset.expected_source, source); assert!(matches!( reset.kind, SourceMutationKind::ResetUnsupportedIfUnchanged { @@ -454,7 +454,7 @@ fn shutdown_rejects_seed_updates_and_preexisting_preview_commits() { PreviewFinishResult::RejectedStaleAuthority { .. } )); assert!(matches!( - controller.update_seeds(test_seeds(true, true)), + controller.update_seeds(&test_seeds(true, true)), UpdateSeedsResult::RejectedShuttingDown )); assert_eq!( @@ -488,7 +488,7 @@ fn stale_runtime_and_position_preview_cancellations_do_not_restore_rollbacks() { ) .unwrap(); assert!(matches!( - controller.update_seeds(changed), + controller.update_seeds(&changed), UpdateSeedsResult::Applied { .. } )); @@ -627,7 +627,7 @@ fn retained_authority_failure_resolves_previews_before_a_later_staged_reload() { resolution.reason == AbandonedPreviewResolutionReason::CancelledUnderRetainedAuthority })); assert!(matches!( - controller.update_seeds(test_seeds(true, false)), + controller.update_seeds(&test_seeds(true, false)), UpdateSeedsResult::StagedBehindBarrier { .. } )); let (client, inspection) = begin_recovery( diff --git a/src/runtime_ui_state/types.rs b/src/runtime_ui_state/types.rs index 3cf692e37..d39bbf6e6 100644 --- a/src/runtime_ui_state/types.rs +++ b/src/runtime_ui_state/types.rs @@ -1,5 +1,6 @@ use crate::config::{ - ToolbarItemId, ToolbarItemOrderGroup, ToolbarItemVisibilitySetting as ItemVisibilitySetting, + StatusBarItem, ToolbarItemId, ToolbarItemOrderGroup, + ToolbarItemVisibilitySetting as ItemVisibilitySetting, ToolbarLayoutMode, ToolbarSectionFlag, TopDisplayMode, }; @@ -116,7 +117,7 @@ pub(crate) enum InteractionSeedTarget { /// Whether status-bar segments respond to clicks. StatusBarInteractive, /// Whether one status-bar segment is shown. - StatusBarItem(crate::config::StatusBarItem), + StatusBarItem(StatusBarItem), /// The status bar as a whole. StatusBar, /// The board and page badges that float over the canvas. @@ -139,7 +140,7 @@ pub(crate) enum InteractionSeedTarget { /// `ItemVisibility`: a section's visibility has a layout-mode baseline and /// a legacy mirror behind it, so it is seeded and restored as its own /// value rather than as an individual item override. - SectionVisibility(crate::config::ToolbarSectionFlag), + SectionVisibility(ToolbarSectionFlag), /// The toolbar layout preset the strip's layout button cycles through. ToolbarLayoutMode, /// The click highlight, and the ring the highlight tool keeps on screen. @@ -157,7 +158,7 @@ pub(crate) enum InteractionSeedValue { ItemOrder(Vec), Position(ToolbarPositionSeed), TopDisplayMode(PersistedTopDisplayMode), - LayoutMode(crate::config::ToolbarLayoutMode), + LayoutMode(ToolbarLayoutMode), } impl InteractionSeedValue { diff --git a/src/runtime_ui_state/wire.rs b/src/runtime_ui_state/wire.rs index 0f6a63c36..66eddee53 100644 --- a/src/runtime_ui_state/wire.rs +++ b/src/runtime_ui_state/wire.rs @@ -1,4 +1,5 @@ use toml::Value; +use toml::map::Map; use super::{RuntimeStateObservedEnvelope, RuntimeUiFileStatus, RuntimeUiWireState}; @@ -36,7 +37,7 @@ pub(crate) fn decode_runtime_ui_file(bytes: &[u8]) -> DecodedRuntimeUiFile { envelope: RuntimeStateObservedEnvelope::PresentWithoutReadableVersion, supported_wire: None, }; - let Ok(text) = std::str::from_utf8(bytes) else { + let Ok(text) = str::from_utf8(bytes) else { return invalid(); }; let Ok(Value::Table(mut root)) = toml::from_str::(text) else { @@ -80,7 +81,7 @@ pub(crate) fn encode_runtime_ui_file( } fn preserve_value(value: &Value) -> Result { - let mut wrapper = toml::map::Map::new(); + let mut wrapper = Map::new(); wrapper.insert("preserved".to_string(), value.clone()); toml::to_string(&Value::Table(wrapper)) .map_err(|error| RuntimeUiWireError::new(format!("could not preserve TOML value: {error}"))) diff --git a/src/runtime_ui_state/wire/v1.rs b/src/runtime_ui_state/wire/v1.rs index ff2b771ce..f4102fc20 100644 --- a/src/runtime_ui_state/wire/v1.rs +++ b/src/runtime_ui_state/wire/v1.rs @@ -2,6 +2,8 @@ use std::collections::{BTreeMap, BTreeSet}; use toml::{Table, Value}; +use crate::config::{StatusBarItem, ToolbarLayoutMode, ToolbarSectionFlag}; + use super::{RuntimeUiWireError, preserve_value, restore_value}; use crate::config::{ ToolbarItemId, ToolbarItemOrderGroup, toolbar_item_definitions, toolbar_item_order_group, @@ -106,7 +108,7 @@ fn decode_toolbar( decode_id_map( toolbar.remove("sections"), |id| { - crate::config::ToolbarSectionFlag::ALL + ToolbarSectionFlag::ALL .into_iter() .find(|flag| flag.item_id().as_str() == id) .map(InteractionSeedTarget::SectionVisibility) @@ -116,10 +118,7 @@ fn decode_toolbar( )?; decode_id_map( toolbar.remove("status_bar_items"), - |id| { - crate::config::StatusBarItem::from_config_id(id) - .map(InteractionSeedTarget::StatusBarItem) - }, + |id| StatusBarItem::from_config_id(id).map(InteractionSeedTarget::StatusBarItem), &mut wire.model, &mut wire.passthrough, )?; @@ -608,7 +607,7 @@ fn item_belongs_to_group(id: ToolbarItemId, group: ToolbarItemOrderGroup) -> boo /// The wire spelling of a layout preset. Independent of the config's serde /// naming so a rename there cannot silently reinterpret a stored value. -fn layout_mode_wire_id(mode: crate::config::ToolbarLayoutMode) -> &'static str { +fn layout_mode_wire_id(mode: ToolbarLayoutMode) -> &'static str { use crate::config::ToolbarLayoutMode as Mode; match mode { Mode::Simple => "simple", @@ -617,7 +616,7 @@ fn layout_mode_wire_id(mode: crate::config::ToolbarLayoutMode) -> &'static str { } } -fn layout_mode_from_wire_id(value: &str) -> Option { +fn layout_mode_from_wire_id(value: &str) -> Option { use crate::config::ToolbarLayoutMode as Mode; match value { "simple" => Some(Mode::Simple), diff --git a/src/runtime_ui_state/writer.rs b/src/runtime_ui_state/writer.rs index bb44ad6db..58d01e82b 100644 --- a/src/runtime_ui_state/writer.rs +++ b/src/runtime_ui_state/writer.rs @@ -1,5 +1,6 @@ +use std::io; use std::panic::{AssertUnwindSafe, catch_unwind}; -use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, TrySendError, sync_channel}; +use std::sync::mpsc::{Receiver, RecvError, SyncSender, TryRecvError, TrySendError, sync_channel}; use std::thread::{self, JoinHandle}; use super::{ @@ -40,19 +41,12 @@ impl RuntimeUiStateWriter { pub(crate) fn spawn_with_completion_notifier( store: RuntimeUiStateStore, notify_completion: impl Fn() + Send + 'static, - ) -> std::io::Result { + ) -> io::Result { let (command_tx, command_rx) = sync_channel(WRITER_COMMAND_CAPACITY); let (completion_tx, completion_rx) = sync_channel(WRITER_COMMAND_CAPACITY); let worker = thread::Builder::new() .name("wayscriber-runtime-state-writer".to_string()) - .spawn(move || { - run_writer( - store, - command_rx, - completion_tx, - Box::new(notify_completion), - ) - })?; + .spawn(move || run_writer(&store, &command_rx, &completion_tx, ¬ify_completion))?; Ok(Self { commands: Some(command_tx), completions: completion_rx, @@ -82,7 +76,7 @@ impl RuntimeUiStateWriter { } } - pub(crate) fn recv(&self) -> Result { + pub(crate) fn recv(&self) -> Result { self.completions.recv() } @@ -109,23 +103,31 @@ impl Drop for RuntimeUiStateWriter { } } +#[expect( + clippy::disallowed_methods, + reason = "a panicking completion notifier must not stop the writer from serving later commands" +)] fn run_writer( - store: RuntimeUiStateStore, - commands: Receiver, - completions: SyncSender, - notify_completion: Box, + store: &RuntimeUiStateStore, + commands: &Receiver, + completions: &SyncSender, + notify_completion: &dyn Fn(), ) { while let Ok(command) = commands.recv() { - let completion = execute_catching_panics(&store, command); + let completion = execute_catching_panics(store, command); if completions.send(completion).is_err() { break; } - if catch_unwind(AssertUnwindSafe(¬ify_completion)).is_err() { + if catch_unwind(AssertUnwindSafe(notify_completion)).is_err() { log::error!("Runtime UI completion notifier panicked; writer will continue"); } } } +#[expect( + clippy::disallowed_methods, + reason = "a panicking mutation or recovery must still produce its completion, so the controller never waits forever" +)] fn execute_catching_panics( store: &RuntimeUiStateStore, command: RuntimeStateWriterCommand, diff --git a/src/runtime_ui_state/writer/tests.rs b/src/runtime_ui_state/writer/tests.rs index 008a53a88..c552d2fba 100644 --- a/src/runtime_ui_state/writer/tests.rs +++ b/src/runtime_ui_state/writer/tests.rs @@ -1,8 +1,9 @@ +use std::collections::BTreeSet; use std::fs; -use std::sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, -}; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use crate::test_temp; use super::*; use crate::runtime_ui_state::*; @@ -20,7 +21,7 @@ fn startup_seeds() -> ValidatedInteractionSeeds { #[test] fn every_accepted_command_produces_one_typed_completion() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let store = RuntimeUiStateStore::new(&path); let expected = store.inspect().unwrap().observation.revision; @@ -50,7 +51,7 @@ fn every_accepted_command_produces_one_typed_completion() { #[test] fn a_panicking_completion_notifier_does_not_stop_the_writer() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let store = RuntimeUiStateStore::new(&path); let expected = store.inspect().unwrap().observation.revision; @@ -105,12 +106,11 @@ fn a_panicking_completion_notifier_does_not_stop_the_writer() { #[test] fn source_commands_are_serialized_against_the_previous_completion_source() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let store = RuntimeUiStateStore::new(&path); let missing = store.inspect().unwrap().observation.revision; - let writer = - RuntimeUiStateWriter::spawn_with_completion_notifier(store.clone(), || {}).unwrap(); + let writer = RuntimeUiStateWriter::spawn_with_completion_notifier(store, || {}).unwrap(); let first = SourceMutationRequest { id: SourceMutationId(1), accepted_through: AcceptedStateRevision(1), @@ -151,7 +151,7 @@ fn source_commands_are_serialized_against_the_previous_completion_source() { #[test] fn writer_reports_filesystem_failures_without_dropping_completion() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("gone/runtime-ui.toml"); let store = RuntimeUiStateStore::new(&path); let expected = store.inspect().unwrap().observation.revision; @@ -180,7 +180,7 @@ fn writer_reports_filesystem_failures_without_dropping_completion() { #[test] fn inspection_command_returns_exact_unsupported_bytes() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let bytes = b"version = 22\nfuture = true\n"; fs::write(&path, bytes).unwrap(); @@ -219,7 +219,7 @@ fn inspection_command_returns_exact_unsupported_bytes() { #[test] fn a_batch_of_accepted_inspections_has_no_missing_or_duplicate_completion() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let writer = RuntimeUiStateWriter::spawn_with_completion_notifier(RuntimeUiStateStore::new(path), || {}) @@ -236,7 +236,7 @@ fn a_batch_of_accepted_inspections_has_no_missing_or_duplicate_completion() { })) .unwrap(); } - let mut completed = std::collections::BTreeSet::new(); + let mut completed = BTreeSet::new(); for _ in 0..12 { let RuntimeStateWriterCompletion::Recovery(completion) = writer.recv().unwrap() else { panic!("expected recovery completion"); @@ -250,7 +250,7 @@ fn a_batch_of_accepted_inspections_has_no_missing_or_duplicate_completion() { #[test] fn malformed_startup_is_preserved_and_reset_through_real_recovery_io() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("runtime-ui.toml"); let invalid = b"not valid TOML at all\n"; fs::write(&path, invalid).unwrap(); diff --git a/src/screen_pixels.rs b/src/screen_pixels.rs index a28dea5d0..2f34e8354 100644 --- a/src/screen_pixels.rs +++ b/src/screen_pixels.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::fmt::{Debug, Formatter}; + /// A point in source-image pixel-edge coordinates. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ImagePoint { @@ -325,8 +328,8 @@ impl ScreenImage { } } -impl std::fmt::Debug for PackedArgb32 { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Debug for PackedArgb32 { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter .debug_struct("PackedArgb32") .field("width", &self.width) @@ -671,12 +674,12 @@ mod tests { #[test] fn packed_argb32_accepts_only_exact_tightly_packed_storage() { - let pixels = PackedArgb32::new(2, 3, 8, vec![0xA5; 24]).expect("valid packed pixels"); + let pixels = PackedArgb32::new(2, 3, 8, vec![0xa5; 24]).expect("valid packed pixels"); assert_eq!( (pixels.width(), pixels.height(), pixels.stride()), (2, 3, 8) ); - assert_eq!(pixels.data(), &[0xA5; 24]); + assert_eq!(pixels.data(), &[0xa5; 24]); assert_eq!(PackedArgb32::new(2, 3, 12, vec![0; 36]), None); assert_eq!(PackedArgb32::new(2, 3, 8, vec![0; 23]), None); assert_eq!(PackedArgb32::new(u32::MAX, 1, -4, Vec::new()), None); @@ -684,7 +687,7 @@ mod tests { #[test] fn packed_argb32_debug_redacts_captured_bytes() { - let pixels = PackedArgb32::new(3, 4, 12, vec![0xFF; 48]).unwrap(); + let pixels = PackedArgb32::new(3, 4, 12, vec![0xff; 48]).unwrap(); let rendered = format!("{pixels:?}"); assert!(rendered.contains("width: 3")); assert!(rendered.contains("bytes: 48")); diff --git a/src/session/artifacts.rs b/src/session/artifacts.rs index 3eb3e39bb..76d21895d 100644 --- a/src/session/artifacts.rs +++ b/src/session/artifacts.rs @@ -10,6 +10,9 @@ use std::io::{self, ErrorKind}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::path::{Path, PathBuf}; +use crate::session; +use crate::session::catalog; + use super::options::append_path_suffix; use super::primary::open_session_artifact_for_read; @@ -91,6 +94,7 @@ pub struct NamedSessionDuplicateOutcome { impl NamedSessionClearOutcome { #[allow(dead_code)] + #[must_use] pub fn removed_any(self) -> bool { self.removed_primary || self.removed_backup @@ -100,6 +104,7 @@ impl NamedSessionClearOutcome { } #[allow(dead_code)] +#[must_use] pub fn named_session_artifact_paths(path: &Path) -> SessionArtifactPaths { SessionArtifactPaths { primary: path.to_path_buf(), @@ -208,7 +213,7 @@ fn preserved_newer_version_suffix<'a>( #[allow(dead_code)] pub fn clear_named_session_non_lock_artifacts(path: &Path) -> Result { - crate::session::validate_named_session_file_for_clear(path)?; + session::validate_named_session_file_for_clear(path)?; let artifacts = named_session_artifact_paths(path); let recovery_artifacts = removable_recovery_artifact_paths(&artifacts.recovery)?; @@ -240,9 +245,9 @@ pub fn duplicate_named_session_primary( source: &Path, target: &Path, ) -> Result { - crate::session::validate_named_session_file_for_info(source)?; - crate::session::validate_named_session_file_for_foreground(target)?; - if crate::session::catalog::session_paths_match(source, target) { + session::validate_named_session_file_for_info(source)?; + session::validate_named_session_file_for_foreground(target)?; + if catalog::session_paths_match(source, target) { return Err(anyhow!( "Duplicate Session target must be different from the source: {}", target.display() diff --git a/src/session/artifacts/corrupt_copies.rs b/src/session/artifacts/corrupt_copies.rs index bd10e92d0..bfcddd6e6 100644 --- a/src/session/artifacts/corrupt_copies.rs +++ b/src/session/artifacts/corrupt_copies.rs @@ -1,7 +1,12 @@ //! Side files containing unreadable artifacts, independent of rotation backups. -use super::*; +use super::super::options::append_path_suffix; +use super::remove_artifact_if_exists; +use anyhow::{Context, Result}; use std::ffi::OsStr; +use std::fs; +use std::io::ErrorKind; use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf}; pub(crate) const MAX_CORRUPT_COPIES_PER_ARTIFACT: usize = 3; const CORRUPT_COPY_MARKER: &str = ".corrupt-"; @@ -29,7 +34,7 @@ pub(crate) fn parse_corrupt_copy_name(name: &OsStr) -> Option<(&OsStr, u64)> { } Some(( OsStr::from_bytes(artifact), - std::str::from_utf8(seq).ok()?.parse().ok()?, + str::from_utf8(seq).ok()?.parse().ok()?, )) } @@ -94,10 +99,10 @@ pub(super) fn collect_corrupt_copies(primary: &Path, paths: &mut Vec) - pub(crate) fn remove_corrupt_copies(artifact: &Path) -> Result { let mut removed = false; - for copy in corrupt_copies_of(artifact)? + let regular_copies = corrupt_copies_of(artifact)? .into_iter() - .filter(|copy| copy.regular_file) - { + .filter(|copy| copy.regular_file); + for copy in regular_copies { removed = remove_artifact_if_exists(©.path)? || removed; } Ok(removed) diff --git a/src/session/artifacts/kind.rs b/src/session/artifacts/kind.rs index 8d91d9e49..2cc2b7586 100644 --- a/src/session/artifacts/kind.rs +++ b/src/session/artifacts/kind.rs @@ -16,6 +16,7 @@ pub enum SessionArtifactKind { UnreadableCopy, } +#[must_use] pub fn named_session_artifact_kind(primary: &Path, path: &Path) -> Option { let artifacts = named_session_artifact_paths(primary); let name = path.file_name()?; diff --git a/src/session/artifacts/move_file.rs b/src/session/artifacts/move_file.rs index effe74404..35d93de2d 100644 --- a/src/session/artifacts/move_file.rs +++ b/src/session/artifacts/move_file.rs @@ -1,3 +1,5 @@ +use std::ffi::CString; + use anyhow::{Context, Result, anyhow}; use std::ffi::{OsStr, OsString}; use std::fs::{self, File}; @@ -6,6 +8,8 @@ use std::io::ErrorKind; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; +use crate::session; +use crate::session::catalog; use crate::session::lock::{lock_exclusive, open_runtime_lock_file, unlock}; use crate::session::primary::open_session_artifact_for_read; @@ -76,9 +80,9 @@ pub fn move_named_session_non_lock_artifacts( source: &Path, target: &Path, ) -> Result { - crate::session::validate_named_session_file_for_info(source)?; - crate::session::validate_named_session_file_for_foreground(target)?; - if crate::session::catalog::session_paths_match(source, target) { + session::validate_named_session_file_for_info(source)?; + session::validate_named_session_file_for_foreground(target)?; + if catalog::session_paths_match(source, target) { return Err(anyhow!( "Move Session target must be different from the source: {}", target.display() @@ -249,7 +253,7 @@ fn target_path_for_source_artifact( if let Some(name) = source_name.to_str() && let Some(suffix) = preserved_newer_version_suffix(&source_artifacts.primary, name) { - return Some(crate::session::append_path_suffix( + return Some(session::append_path_suffix( &target_artifacts.primary, suffix, )); @@ -402,7 +406,7 @@ pub(crate) fn rename_artifact_no_replace(source: &Path, target: &Path) -> io::Re } #[cfg(target_os = "linux")] -fn path_to_cstring(path: &Path) -> io::Result { +fn path_to_cstring(path: &Path) -> io::Result { use std::ffi::CString; use std::os::unix::ffi::OsStrExt; diff --git a/src/session/artifacts/move_file/tests.rs b/src/session/artifacts/move_file/tests.rs index 67e05d69e..ae4e9fe41 100644 --- a/src/session/artifacts/move_file/tests.rs +++ b/src/session/artifacts/move_file/tests.rs @@ -1,3 +1,5 @@ +use crate::test_temp; + use super::*; use crate::session::{SessionArtifactPaths, named_session_artifact_paths}; @@ -7,13 +9,13 @@ fn assert_moved_target_artifacts( preserved: &Path, preserved_moved: &Path, ) { - assert_eq!(std::fs::read(&artifacts.primary).unwrap(), b"primary"); - assert_eq!(std::fs::read(&artifacts.backup).unwrap(), b"backup"); - assert_eq!(std::fs::read(&artifacts.recovery).unwrap(), b"recovery"); - assert_eq!(std::fs::read(&artifacts.clear_marker).unwrap(), b"cleared"); - assert_eq!(std::fs::read(rotated_recovery).unwrap(), b"rotated"); - assert_eq!(std::fs::read(preserved).unwrap(), b"newer"); - assert_eq!(std::fs::read(preserved_moved).unwrap(), b"newer moved"); + assert_eq!(fs::read(&artifacts.primary).unwrap(), b"primary"); + assert_eq!(fs::read(&artifacts.backup).unwrap(), b"backup"); + assert_eq!(fs::read(&artifacts.recovery).unwrap(), b"recovery"); + assert_eq!(fs::read(&artifacts.clear_marker).unwrap(), b"cleared"); + assert_eq!(fs::read(rotated_recovery).unwrap(), b"rotated"); + assert_eq!(fs::read(preserved).unwrap(), b"newer"); + assert_eq!(fs::read(preserved_moved).unwrap(), b"newer moved"); } fn assert_moved_sources_removed( @@ -36,21 +38,18 @@ fn assert_source_lock_was_not_moved( target_artifacts: &SessionArtifactPaths, ) { assert_eq!( - std::fs::read(&source_artifacts.lock).unwrap(), + fs::read(&source_artifacts.lock).unwrap(), b"source lock", "source lock must not be moved as session data" ); if target_artifacts.lock.exists() { - assert_ne!( - std::fs::read(&target_artifacts.lock).unwrap(), - b"source lock" - ); + assert_ne!(fs::read(&target_artifacts.lock).unwrap(), b"source lock"); } } #[test] fn move_named_session_non_lock_artifacts_moves_primary_and_sidecars_without_lock() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session"); let source_artifacts = named_session_artifact_paths(&source); @@ -60,20 +59,19 @@ fn move_named_session_non_lock_artifacts_moves_primary_and_sidecars_without_lock PathBuf::from(format!("{}.old", target_artifacts.recovery.display())); let preserved_suffix = ".v2-preserved-0123456789abcdef"; let preserved_moved_suffix = ".v3-preserved-fedcba9876543210-moved-1"; - let preserved = crate::session::append_path_suffix(&source, preserved_suffix); - let preserved_moved = crate::session::append_path_suffix(&source, preserved_moved_suffix); - let target_preserved = crate::session::append_path_suffix(&target, preserved_suffix); - let target_preserved_moved = - crate::session::append_path_suffix(&target, preserved_moved_suffix); - - std::fs::write(&source_artifacts.primary, b"primary").unwrap(); - std::fs::write(&source_artifacts.backup, b"backup").unwrap(); - std::fs::write(&source_artifacts.recovery, b"recovery").unwrap(); - std::fs::write(&source_artifacts.clear_marker, b"cleared").unwrap(); - std::fs::write(&rotated_recovery, b"rotated").unwrap(); - std::fs::write(&source_artifacts.lock, b"source lock").unwrap(); - std::fs::write(&preserved, b"newer").unwrap(); - std::fs::write(&preserved_moved, b"newer moved").unwrap(); + let preserved = session::append_path_suffix(&source, preserved_suffix); + let preserved_moved = session::append_path_suffix(&source, preserved_moved_suffix); + let target_preserved = session::append_path_suffix(&target, preserved_suffix); + let target_preserved_moved = session::append_path_suffix(&target, preserved_moved_suffix); + + fs::write(&source_artifacts.primary, b"primary").unwrap(); + fs::write(&source_artifacts.backup, b"backup").unwrap(); + fs::write(&source_artifacts.recovery, b"recovery").unwrap(); + fs::write(&source_artifacts.clear_marker, b"cleared").unwrap(); + fs::write(&rotated_recovery, b"rotated").unwrap(); + fs::write(&source_artifacts.lock, b"source lock").unwrap(); + fs::write(&preserved, b"newer").unwrap(); + fs::write(&preserved_moved, b"newer moved").unwrap(); let outcome = move_named_session_non_lock_artifacts(&source, &target).unwrap(); @@ -98,13 +96,13 @@ fn move_named_session_non_lock_artifacts_moves_primary_and_sidecars_without_lock #[test] fn rollback_named_session_move_uses_exact_moved_artifacts_and_ignores_skipped_source_dirs() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session"); let source_artifacts = named_session_artifact_paths(&source); let target_artifacts = named_session_artifact_paths(&target); - std::fs::write(&source_artifacts.primary, b"primary").unwrap(); - std::fs::create_dir(&source_artifacts.backup).unwrap(); + fs::write(&source_artifacts.primary, b"primary").unwrap(); + fs::create_dir(&source_artifacts.backup).unwrap(); let outcome = move_named_session_non_lock_artifacts(&source, &target).unwrap(); @@ -112,62 +110,53 @@ fn rollback_named_session_move_uses_exact_moved_artifacts_and_ignores_skipped_so assert_eq!(outcome.moved_artifact_paths.len(), 1); assert!(!source_artifacts.primary.exists()); assert!(source_artifacts.backup.is_dir()); - assert_eq!( - std::fs::read(&target_artifacts.primary).unwrap(), - b"primary" - ); + assert_eq!(fs::read(&target_artifacts.primary).unwrap(), b"primary"); rollback_named_session_non_lock_artifacts_move(&outcome).unwrap(); - assert_eq!( - std::fs::read(&source_artifacts.primary).unwrap(), - b"primary" - ); + assert_eq!(fs::read(&source_artifacts.primary).unwrap(), b"primary"); assert!(source_artifacts.backup.is_dir()); assert!(!target_artifacts.primary.exists()); } #[test] fn move_named_session_non_lock_artifacts_rejects_target_artifact_collision() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session"); let source_artifacts = named_session_artifact_paths(&source); let target_artifacts = named_session_artifact_paths(&target); - std::fs::write(&source_artifacts.primary, b"primary").unwrap(); - std::fs::write(&source_artifacts.backup, b"backup").unwrap(); - std::fs::write(&target_artifacts.clear_marker, b"cleared").unwrap(); + fs::write(&source_artifacts.primary, b"primary").unwrap(); + fs::write(&source_artifacts.backup, b"backup").unwrap(); + fs::write(&target_artifacts.clear_marker, b"cleared").unwrap(); let err = move_named_session_non_lock_artifacts(&source, &target) .expect_err("target sidecar should block move"); assert!(format!("{err:#}").contains("already has session artifacts")); - assert_eq!( - std::fs::read(&source_artifacts.primary).unwrap(), - b"primary" - ); - assert_eq!(std::fs::read(&source_artifacts.backup).unwrap(), b"backup"); + assert_eq!(fs::read(&source_artifacts.primary).unwrap(), b"primary"); + assert_eq!(fs::read(&source_artifacts.backup).unwrap(), b"backup"); assert!(!target_artifacts.primary.exists()); assert_eq!( - std::fs::read(&target_artifacts.clear_marker).unwrap(), + fs::read(&target_artifacts.clear_marker).unwrap(), b"cleared" ); } #[test] fn rename_artifact_no_replace_does_not_overwrite_existing_target() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let source = temp.path().join("source"); let target = temp.path().join("target"); - std::fs::write(&source, b"source").unwrap(); - std::fs::write(&target, b"target").unwrap(); + fs::write(&source, b"source").unwrap(); + fs::write(&target, b"target").unwrap(); let err = rename_artifact_no_replace(&source, &target).expect_err("target must not be overwritten"); assert_eq!(err.kind(), ErrorKind::AlreadyExists); - assert_eq!(std::fs::read(&source).unwrap(), b"source"); - assert_eq!(std::fs::read(&target).unwrap(), b"target"); + assert_eq!(fs::read(&source).unwrap(), b"source"); + assert_eq!(fs::read(&target).unwrap(), b"target"); } #[cfg(unix)] @@ -175,23 +164,20 @@ fn rename_artifact_no_replace_does_not_overwrite_existing_target() { fn move_named_session_non_lock_artifacts_rejects_symlink_source_sidecar_before_move() { use std::os::unix::fs::symlink; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session"); let source_artifacts = named_session_artifact_paths(&source); let backup_target = temp.path().join("backup-target"); - std::fs::write(&source_artifacts.primary, b"primary").unwrap(); - std::fs::write(&backup_target, b"backup").unwrap(); + fs::write(&source_artifacts.primary, b"primary").unwrap(); + fs::write(&backup_target, b"backup").unwrap(); symlink(&backup_target, &source_artifacts.backup).unwrap(); let err = move_named_session_non_lock_artifacts(&source, &target) .expect_err("source sidecar symlink should reject move"); assert!(format!("{err:#}").contains("symlink")); - assert_eq!( - std::fs::read(&source_artifacts.primary).unwrap(), - b"primary" - ); + assert_eq!(fs::read(&source_artifacts.primary).unwrap(), b"primary"); assert!(source_artifacts.backup.exists()); assert!(!target.exists()); } diff --git a/src/session/artifacts/save_as_quarantine.rs b/src/session/artifacts/save_as_quarantine.rs index 9713a035b..12cdb2ab9 100644 --- a/src/session/artifacts/save_as_quarantine.rs +++ b/src/session/artifacts/save_as_quarantine.rs @@ -1,3 +1,5 @@ +use std::{io, process}; + use anyhow::{Context, Result, anyhow}; use log::info; use std::ffi::OsString; @@ -7,6 +9,8 @@ use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; +use crate::durable_io; + use super::{ArtifactMovePaths, rollback_artifact_moves}; pub(crate) const SAVE_AS_STAGING_PREFIX: &str = ".wayscriber-save-as-sidecars"; @@ -61,7 +65,7 @@ impl SaveAsSidecarQuarantine { directory.display() ) })?; - crate::durable_io::sync_parent_dir(directory).with_context(|| { + durable_io::sync_parent_dir(directory).with_context(|| { format!( "failed to sync restored Save Session As sidecars under {}", directory.display() @@ -75,7 +79,7 @@ impl SaveAsSidecarQuarantine { self.remove_after_commit_with( |path| fs::remove_file(path), |path| fs::remove_dir(path), - |path| crate::durable_io::sync_parent_dir(path).map_err(Into::into), + |path| durable_io::sync_parent_dir(path).map_err(Into::into), ) } @@ -86,8 +90,8 @@ impl SaveAsSidecarQuarantine { sync_parent: SyncParent, ) -> Result<()> where - RemoveFile: FnMut(&Path) -> std::io::Result<()>, - RemoveDir: FnOnce(&Path) -> std::io::Result<()>, + RemoveFile: FnMut(&Path) -> io::Result<()>, + RemoveDir: FnOnce(&Path) -> io::Result<()>, SyncParent: FnOnce(&Path) -> Result<()>, { let Some(directory) = self.directory.as_deref() else { @@ -188,7 +192,7 @@ impl RetainedSidecar { self.staged.display() ) })?; - std::io::copy(&mut self.file, &mut restored).with_context(|| { + io::copy(&mut self.file, &mut restored).with_context(|| { format!( "failed to restore retained Save Session As sidecar {}", self.staged.display() @@ -235,14 +239,14 @@ fn retain_after_cleanup_failure( sidecar.restore_if_missing()?; } if let Some(first) = retained.first() { - crate::durable_io::sync_parent_dir(&first.staged).with_context(|| { + durable_io::sync_parent_dir(&first.staged).with_context(|| { format!( "failed to sync retained Save Session As sidecars in {}", directory.display() ) })?; } - crate::durable_io::sync_parent_dir(directory).with_context(|| { + durable_io::sync_parent_dir(directory).with_context(|| { format!( "failed to sync retained Save Session As quarantine {}", directory.display() @@ -274,8 +278,8 @@ pub(crate) fn quarantine_save_as_sidecars( quarantine_save_as_sidecars_with( sidecars, session_path, - |directory| crate::durable_io::sync_directory(directory).map_err(Into::into), - |directory| crate::durable_io::sync_parent_dir(directory).map_err(Into::into), + |directory| durable_io::sync_directory(directory).map_err(Into::into), + |directory| durable_io::sync_parent_dir(directory).map_err(Into::into), ) } @@ -407,10 +411,7 @@ where fn create_save_as_staging_dir(parent: &Path) -> Result { for _ in 0..1024 { let id = NEXT_SAVE_AS_STAGING_ID.fetch_add(1, Ordering::Relaxed); - let candidate = parent.join(format!( - "{SAVE_AS_STAGING_PREFIX}-{}-{id}", - std::process::id() - )); + let candidate = parent.join(format!("{SAVE_AS_STAGING_PREFIX}-{}-{id}", process::id())); let mut builder = fs::DirBuilder::new(); builder.mode(0o700); match builder.create(&candidate) { diff --git a/src/session/artifacts/save_as_quarantine/tests.rs b/src/session/artifacts/save_as_quarantine/tests.rs index 97d0db86e..3c522d939 100644 --- a/src/session/artifacts/save_as_quarantine/tests.rs +++ b/src/session/artifacts/save_as_quarantine/tests.rs @@ -1,3 +1,7 @@ +use std::slice; + +use crate::test_temp; + use super::*; fn staged_file(directory: &Path, original_name: &str) -> PathBuf { @@ -26,7 +30,7 @@ fn staging_directories(parent: &Path) -> Vec { #[test] fn quarantine_validation_failure_preserves_every_sidecar() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let session = temp.path().join("target.wayscriber-session"); let backup = temp.path().join("target.wayscriber-session.bak"); let recovery = temp.path().join("target.wayscriber-session.recovery"); @@ -44,13 +48,13 @@ fn quarantine_validation_failure_preserves_every_sidecar() { #[test] fn quarantine_directory_sync_failure_rolls_back_before_commit() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let session = temp.path().join("target.wayscriber-session"); let recovery = temp.path().join("target.wayscriber-session.recovery"); fs::write(&recovery, b"recovery").expect("write recovery"); let err = quarantine_save_as_sidecars_with( - std::slice::from_ref(&recovery), + slice::from_ref(&recovery), &session, |_| Err(anyhow!("injected quarantine directory sync failure")), |_| -> Result<()> { panic!("parent sync must not run after quarantine sync failure") }, @@ -64,7 +68,7 @@ fn quarantine_directory_sync_failure_rolls_back_before_commit() { #[test] fn quarantine_parent_sync_failure_rolls_back_before_commit() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let session = temp.path().join("target.wayscriber-session"); let backup = temp.path().join("target.wayscriber-session.bak"); let recovery = temp.path().join("target.wayscriber-session.recovery"); @@ -87,15 +91,15 @@ fn quarantine_parent_sync_failure_rolls_back_before_commit() { #[test] fn cleanup_failure_after_recovery_deletion_restores_every_sidecar() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let session = temp.path().join("target.wayscriber-session"); let recovery = temp.path().join("target.wayscriber-session.recovery"); let backup = temp.path().join("target.wayscriber-session.bak"); fs::write(&recovery, b"recovery").expect("write recovery"); fs::write(&backup, b"backup").expect("write backup"); - let quarantine = quarantine_save_as_sidecars(&[recovery.clone(), backup.clone()], &session) - .expect("quarantine sidecars"); + let quarantine = + quarantine_save_as_sidecars(&[recovery, backup], &session).expect("quarantine sidecars"); let directory = quarantine .directory() .expect("quarantine directory") @@ -109,11 +113,11 @@ fn cleanup_failure_after_recovery_deletion_restores_every_sidecar() { if removals == 1 { fs::remove_file(path) } else { - Err(std::io::Error::other("injected second deletion failure")) + Err(io::Error::other("injected second deletion failure")) } }, |path| fs::remove_dir(path), - |path| crate::durable_io::sync_parent_dir(path).map_err(Into::into), + |path| durable_io::sync_parent_dir(path).map_err(Into::into), ) .expect_err("partial cleanup must report failure"); @@ -137,12 +141,12 @@ fn cleanup_failure_after_recovery_deletion_restores_every_sidecar() { fn cleanup_parent_sync_failure_recreates_private_quarantine_with_bytes() { use std::os::unix::fs::PermissionsExt; - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let session = temp.path().join("target.wayscriber-session"); let recovery = temp.path().join("target.wayscriber-session.recovery"); fs::write(&recovery, b"recovery").expect("write recovery"); - let quarantine = quarantine_save_as_sidecars(std::slice::from_ref(&recovery), &session) + let quarantine = quarantine_save_as_sidecars(slice::from_ref(&recovery), &session) .expect("quarantine sidecar"); let directory = quarantine .directory() @@ -181,14 +185,14 @@ fn cleanup_parent_sync_failure_recreates_private_quarantine_with_bytes() { fn quarantine_rejects_symlink_sidecar_without_following_it() { use std::os::unix::fs::symlink; - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let session = temp.path().join("target.wayscriber-session"); let sidecar = temp.path().join("target.wayscriber-session.recovery"); let target = temp.path().join("outside"); fs::write(&target, b"outside").expect("write target"); symlink(&target, &sidecar).expect("create sidecar symlink"); - let err = quarantine_save_as_sidecars(std::slice::from_ref(&sidecar), &session) + let err = quarantine_save_as_sidecars(slice::from_ref(&sidecar), &session) .expect_err("symlink sidecar must be rejected"); assert!(format!("{err:#}").contains("sidecar symlink")); diff --git a/src/session/artifacts/tests.rs b/src/session/artifacts/tests.rs index 5fd70c75f..4b3c7dfa5 100644 --- a/src/session/artifacts/tests.rs +++ b/src/session/artifacts/tests.rs @@ -1,16 +1,19 @@ use std::path::{Path, PathBuf}; +use crate::session::{LoadSnapshotOutcome, SessionOptions}; +use crate::test_temp; + use super::*; #[test] fn corrupt_copies_move_with_session_and_clear_without_touching_siblings() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let source = temp.path().join("board.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session"); - std::fs::write(&source, b"primary").unwrap(); + fs::write(&source, b"primary").unwrap(); let suffixes = [".corrupt-1", ".bak.corrupt-2", ".recovery.corrupt-3"]; for suffix in suffixes { - std::fs::write(append_path_suffix(&source, suffix), suffix.as_bytes()).unwrap(); + fs::write(append_path_suffix(&source, suffix), suffix.as_bytes()).unwrap(); } let invalid_suffixes = [ ".corrupt-0", @@ -23,21 +26,21 @@ fn corrupt_copies_move_with_session_and_clear_without_touching_siblings() { ".bak.corrupt-2-more", ]; for suffix in invalid_suffixes { - std::fs::write(append_path_suffix(&source, suffix), b"not our name").unwrap(); + fs::write(append_path_suffix(&source, suffix), b"not our name").unwrap(); } move_named_session_non_lock_artifacts(&source, &target).unwrap(); for suffix in suffixes { assert!(!append_path_suffix(&source, suffix).exists()); assert_eq!( - std::fs::read(append_path_suffix(&target, suffix)).unwrap(), + fs::read(append_path_suffix(&target, suffix)).unwrap(), suffix.as_bytes() ); } assert!(duplicate_named_session_primary(&target, &source).is_ok()); - std::fs::remove_file(&source).unwrap(); - std::fs::write(append_path_suffix(&source, ".corrupt-4"), b"old diagnostic").unwrap(); + fs::remove_file(&source).unwrap(); + fs::write(append_path_suffix(&source, ".corrupt-4"), b"old diagnostic").unwrap(); assert!(duplicate_named_session_primary(&target, &source).is_err()); clear_named_session_non_lock_artifacts(&target).unwrap(); @@ -46,7 +49,7 @@ fn corrupt_copies_move_with_session_and_clear_without_touching_siblings() { } for suffix in invalid_suffixes { assert_eq!( - std::fs::read(append_path_suffix(&source, suffix)).unwrap(), + fs::read(append_path_suffix(&source, suffix)).unwrap(), b"not our name", "{suffix}" ); @@ -76,7 +79,7 @@ fn named_session_artifact_paths_use_exact_primary_suffixes() { #[test] fn clear_named_session_non_lock_artifacts_preserves_lock_and_sibling() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("lecture.wayscriber-session"); let sibling = temp.path().join("other.wayscriber-session"); let artifacts = named_session_artifact_paths(&path); @@ -94,18 +97,18 @@ fn clear_named_session_non_lock_artifacts_preserves_lock_and_sibling() { &sibling_artifacts.backup, &sibling_artifacts.lock, ] { - std::fs::write(path, b"artifact").unwrap(); + fs::write(path, b"artifact").unwrap(); } let rotated_recovery = PathBuf::from(format!("{}.old", artifacts.recovery.display())); - std::fs::write(&rotated_recovery, b"rotated").unwrap(); + fs::write(&rotated_recovery, b"rotated").unwrap(); let preserved = append_path_suffix(&artifacts.primary, ".v2-preserved-0123456789abcdef"); let preserved_moved = append_path_suffix(&artifacts.primary, ".v3-preserved-fedcba9876543210-moved-1"); let similar_sibling = append_path_suffix(&artifacts.primary, ".version-preserved-not-a-session"); - std::fs::write(&preserved, b"newer").unwrap(); - std::fs::write(&preserved_moved, b"newer moved").unwrap(); - std::fs::write(&similar_sibling, b"sibling").unwrap(); + fs::write(&preserved, b"newer").unwrap(); + fs::write(&preserved_moved, b"newer moved").unwrap(); + fs::write(&similar_sibling, b"sibling").unwrap(); let outcome = clear_named_session_non_lock_artifacts(&path).unwrap(); @@ -138,14 +141,14 @@ fn clear_named_session_non_lock_artifacts_preserves_lock_and_sibling() { #[test] fn clear_named_session_non_lock_artifacts_skips_directory_sidecars() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("lecture.wayscriber-session"); let artifacts = named_session_artifact_paths(&path); let recovery_dir = PathBuf::from(format!("{}.old", artifacts.recovery.display())); - std::fs::write(&artifacts.primary, b"primary").unwrap(); - std::fs::create_dir(&artifacts.backup).unwrap(); - std::fs::create_dir(&recovery_dir).unwrap(); + fs::write(&artifacts.primary, b"primary").unwrap(); + fs::create_dir(&artifacts.backup).unwrap(); + fs::create_dir(&recovery_dir).unwrap(); let outcome = clear_named_session_non_lock_artifacts(&path).unwrap(); @@ -165,26 +168,23 @@ fn clear_named_session_non_lock_artifacts_skips_directory_sidecars() { #[test] fn duplicate_named_session_primary_copies_only_primary() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("lecture-copy.wayscriber-session"); let source_artifacts = named_session_artifact_paths(&source); let target_artifacts = named_session_artifact_paths(&target); - std::fs::write(&source_artifacts.primary, b"primary").unwrap(); - std::fs::write(&source_artifacts.backup, b"backup").unwrap(); - std::fs::write(&source_artifacts.recovery, b"recovery").unwrap(); - std::fs::write(&source_artifacts.clear_marker, b"cleared").unwrap(); - std::fs::write(&source_artifacts.lock, b"lock").unwrap(); + fs::write(&source_artifacts.primary, b"primary").unwrap(); + fs::write(&source_artifacts.backup, b"backup").unwrap(); + fs::write(&source_artifacts.recovery, b"recovery").unwrap(); + fs::write(&source_artifacts.clear_marker, b"cleared").unwrap(); + fs::write(&source_artifacts.lock, b"lock").unwrap(); let outcome = duplicate_named_session_primary(&source, &target).unwrap(); assert_eq!(outcome.target, target); assert_eq!(outcome.bytes_copied, "primary".len() as u64); - assert_eq!( - std::fs::read(&target_artifacts.primary).unwrap(), - b"primary" - ); + assert_eq!(fs::read(&target_artifacts.primary).unwrap(), b"primary"); for path in [ &target_artifacts.backup, &target_artifacts.recovery, @@ -205,12 +205,12 @@ fn duplicate_named_session_primary_copies_only_primary() { #[test] fn duplicate_named_session_primary_rejects_existing_target_sidecar() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("lecture-copy.wayscriber-session"); let target_artifacts = named_session_artifact_paths(&target); - std::fs::write(&source, b"primary").unwrap(); - std::fs::write(&target_artifacts.backup, b"backup").unwrap(); + fs::write(&source, b"primary").unwrap(); + fs::write(&target_artifacts.backup, b"backup").unwrap(); let err = duplicate_named_session_primary(&source, &target) .expect_err("target sidecar should block duplicate"); @@ -228,11 +228,11 @@ fn duplicate_named_session_primary_rejects_existing_target_sidecar() { fn duplicate_named_session_primary_rejects_symlink_source() { use std::os::unix::fs::symlink; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let target_source = temp.path().join("real.wayscriber-session"); let source = temp.path().join("link.wayscriber-session"); let target = temp.path().join("copy.wayscriber-session"); - std::fs::write(&target_source, b"primary").unwrap(); + fs::write(&target_source, b"primary").unwrap(); symlink(&target_source, &source).unwrap(); let err = duplicate_named_session_primary(&source, &target) @@ -246,31 +246,31 @@ fn duplicate_named_session_primary_rejects_symlink_source() { fn non_utf8_diagnostics_are_reused_pruned_moved_and_removed_by_core_clear() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let source = temp .path() .join(OsStr::from_bytes(b"board-\xff.wayscriber-session")); let target = temp .path() .join(OsStr::from_bytes(b"archive-\xfe.wayscriber-session")); - let mut options = crate::session::SessionOptions::new(temp.path().to_path_buf(), "nonutf"); + let mut options = SessionOptions::new(temp.path().to_path_buf(), "nonutf"); options.set_named_file_target(source.clone()); for index in 0..5 { - std::fs::write(&source, format!("broken {index}")).unwrap(); - let outcome = crate::session::load_snapshot_with_outcome(&options).unwrap(); + fs::write(&source, format!("broken {index}")).unwrap(); + let outcome = session::load_snapshot_with_outcome(&options).unwrap(); assert!(matches!( outcome, - crate::session::LoadSnapshotOutcome::EmptyAfterCorruption { .. } + LoadSnapshotOutcome::EmptyAfterCorruption { .. } )); // The same unreadable primary reuses its diagnostic, rather than allocating again. - crate::session::load_snapshot_with_outcome(&options).unwrap(); + session::load_snapshot_with_outcome(&options).unwrap(); assert_eq!( corrupt_copies_of(&source).unwrap().len(), (index + 1).min(3) ); } for suffix in [".bak.corrupt-1", ".recovery.corrupt-1"] { - std::fs::write(append_path_suffix(&source, suffix), b"side diagnostic").unwrap(); + fs::write(append_path_suffix(&source, suffix), b"side diagnostic").unwrap(); } let before = named_session_non_lock_artifact_paths(&source).unwrap(); move_named_session_non_lock_artifacts(&source, &target).unwrap(); @@ -278,6 +278,6 @@ fn non_utf8_diagnostics_are_reused_pruned_moved_and_removed_by_core_clear() { options.set_named_file_target(target.clone()); let moved = named_session_non_lock_artifact_paths(&target).unwrap(); assert_eq!(moved.len(), before.len()); - crate::session::clear_session(&options).unwrap(); + session::clear_session(&options).unwrap(); assert!(moved.iter().all(|path| !path.exists())); } diff --git a/src/session/catalog.rs b/src/session/catalog.rs index 959d3eb9f..527349036 100644 --- a/src/session/catalog.rs +++ b/src/session/catalog.rs @@ -1,12 +1,18 @@ +use std::ffi::OsString; +use std::os::unix::fs::OpenOptionsExt; +use std::{env, io, process}; + use anyhow::{Context, Result, anyhow}; use log::warn; use serde::{Deserialize, Serialize}; use std::fs::{self, File, OpenOptions}; use std::io::{ErrorKind, Write}; -use std::path::{Component, Path, PathBuf}; +use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; +use crate::paths; + use super::lock::{lock_exclusive, open_runtime_lock_file, unlock}; mod hooks; mod identity; @@ -67,6 +73,7 @@ impl Default for CatalogFile { } /// Path to the session catalog under the configured XDG data root. +#[must_use] pub fn catalog_path() -> PathBuf { catalog_dir().join("sessions.json") } @@ -328,7 +335,7 @@ fn save_catalog_atomic_with_temp_path( let mut open_options = OpenOptions::new(); open_options.write(true).create_new(true); #[cfg(unix)] - std::os::unix::fs::OpenOptionsExt::mode(&mut open_options, 0o600); + OpenOptionsExt::mode(&mut open_options, 0o600); let mut file = open_options.open(tmp_path).with_context(|| { format!( "failed to open temporary session catalog {}", @@ -362,14 +369,14 @@ fn save_catalog_atomic_with_temp_path( } fn catalog_dir() -> PathBuf { - crate::paths::data_dir() - .unwrap_or_else(|| crate::paths::home_dir().unwrap_or_else(std::env::temp_dir)) + paths::data_dir() + .unwrap_or_else(|| paths::home_dir().unwrap_or_else(env::temp_dir)) .join("wayscriber") } /// Creates missing directories with mode 0700, as the XDG base directory spec asks. #[cfg(unix)] -fn create_private_dir_all(dir: &Path) -> std::io::Result<()> { +fn create_private_dir_all(dir: &Path) -> io::Result<()> { use std::os::unix::fs::DirBuilderExt; fs::DirBuilder::new() @@ -384,7 +391,7 @@ fn create_private_dir_all(dir: &Path) -> std::io::Result<()> { } fn catalog_lock_path(path: &Path) -> PathBuf { - let mut raw = std::ffi::OsString::from(path.as_os_str()); + let mut raw = OsString::from(path.as_os_str()); raw.push(".lock"); PathBuf::from(raw) } @@ -403,7 +410,7 @@ fn catalog_temp_path(path: &Path) -> Result { ) })?; let id = NEXT_CATALOG_TEMP_ID.fetch_add(1, Ordering::Relaxed); - Ok(parent.join(format!(".{file_name}.{}-{id}.tmp", std::process::id()))) + Ok(parent.join(format!(".{file_name}.{}-{id}.tmp", process::id()))) } #[cfg(unix)] @@ -431,7 +438,7 @@ fn apply_event(entry: &mut CatalogEntry, event: CatalogEvent, now: u64) { fn generated_catalog_id(now: u64) -> String { let counter = NEXT_CATALOG_ID.fetch_add(1, Ordering::Relaxed); - format!("s-{:x}-{:x}-{counter:x}", std::process::id(), now) + format!("s-{:x}-{:x}-{counter:x}", process::id(), now) } fn now_epoch_millis() -> u64 { diff --git a/src/session/catalog/hooks.rs b/src/session/catalog/hooks.rs index b884971d2..bed7a8161 100644 --- a/src/session/catalog/hooks.rs +++ b/src/session/catalog/hooks.rs @@ -6,9 +6,15 @@ use super::{CatalogEvent, upsert_session_event}; use crate::session::options::SessionOptions; #[cfg(test)] -use { - super::identity::normalize_exact_path, crate::env_vars::CATALOG_HOOKS_TEST_ENV, std::path::Path, -}; +use super::identity::normalize_exact_path; +#[cfg(test)] +use crate::env_vars::CATALOG_HOOKS_TEST_ENV; +#[cfg(test)] +use std::env; +#[cfg(test)] +use std::ffi::OsStr; +#[cfg(test)] +use std::path::Path; /// Record a committed named Open, preserving failures for runtime feedback. pub(crate) fn try_record_named_session_opened(options: &SessionOptions) -> Result<()> { @@ -66,10 +72,10 @@ pub(crate) fn record_named_session_saved(options: &SessionOptions) { #[cfg(test)] fn test_catalog_hooks_enabled_for_path(path: &Path) -> bool { - let Some(raw) = std::env::var_os(CATALOG_HOOKS_TEST_ENV) else { + let Some(raw) = env::var_os(CATALOG_HOOKS_TEST_ENV) else { return false; }; - if raw.is_empty() || raw == std::ffi::OsStr::new("1") { + if raw.is_empty() || raw == OsStr::new("1") { return true; } diff --git a/src/session/catalog/identity.rs b/src/session/catalog/identity.rs index 71c2a584d..7b60c32cb 100644 --- a/src/session/catalog/identity.rs +++ b/src/session/catalog/identity.rs @@ -1,4 +1,8 @@ -use super::*; +use std::env; + +use super::CatalogEntry; +use anyhow::{Result, anyhow}; +use std::path::{Component, Path, PathBuf}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CatalogPathIdentity { @@ -7,6 +11,7 @@ pub struct CatalogPathIdentity { } /// Compute the exact/canonical identity used for dedupe without creating files. +#[must_use] pub fn session_path_identity(path: &Path) -> CatalogPathIdentity { let exact_path = normalize_exact_path(path); let canonical_path = existing_or_parent_canonical_path(&exact_path); @@ -18,6 +23,7 @@ pub fn session_path_identity(path: &Path) -> CatalogPathIdentity { /// Return true when two paths identify the same session target by exact or /// canonical identity without requiring the primary file to exist. +#[must_use] pub fn session_paths_match(a: &Path, b: &Path) -> bool { let a = session_path_identity(a); let b = session_path_identity(b); @@ -67,7 +73,7 @@ pub(super) fn normalize_exact_path(path: &Path) -> PathBuf { let absolute = if path.is_absolute() { path.to_path_buf() } else { - std::env::current_dir() + env::current_dir() .unwrap_or_else(|_| PathBuf::from(".")) .join(path) }; diff --git a/src/session/catalog/tests.rs b/src/session/catalog/tests.rs index 2597244a2..e9cba1e33 100644 --- a/src/session/catalog/tests.rs +++ b/src/session/catalog/tests.rs @@ -1,25 +1,29 @@ +use crate::test_env::LeaseGuard; +use crate::{session, test_env, test_temp}; + use super::*; use crate::draw::{Color, Frame, Shape}; use crate::env_vars::{CATALOG_HOOKS_TEST_ENV, XDG_DATA_HOME_ENV}; use crate::session::{BoardPagesSnapshot, BoardSnapshot, SessionOptions, SessionSnapshot}; use std::fs; use std::path::{Path, PathBuf}; -use std::sync::MutexGuard; struct EnvGuard { - _guard: MutexGuard<'static, ()>, - catalog_hooks: Option, - xdg_data_home: Option, + _guard: LeaseGuard<'static>, + catalog_hooks: Option, + xdg_data_home: Option, } impl EnvGuard { fn set_xdg_data_home(path: &Path) -> Self { - let guard = crate::test_env::lock(); - let catalog_hooks = std::env::var_os(CATALOG_HOOKS_TEST_ENV); - let xdg_data_home = std::env::var_os(XDG_DATA_HOME_ENV); + let guard = test_env::lock(); + let catalog_hooks = env::var_os(CATALOG_HOOKS_TEST_ENV); + let xdg_data_home = env::var_os(XDG_DATA_HOME_ENV); + // SAFETY: serialized by the test environment mutex in `guard`, which + // the returned value keeps until it restores both variables. unsafe { - std::env::set_var(CATALOG_HOOKS_TEST_ENV, path); - std::env::set_var(XDG_DATA_HOME_ENV, path); + env::set_var(CATALOG_HOOKS_TEST_ENV, path); + env::set_var(XDG_DATA_HOME_ENV, path); } Self { _guard: guard, @@ -31,13 +35,17 @@ impl EnvGuard { impl Drop for EnvGuard { fn drop(&mut self) { - match self.catalog_hooks.take() { - Some(value) => unsafe { std::env::set_var(CATALOG_HOOKS_TEST_ENV, value) }, - None => unsafe { std::env::remove_var(CATALOG_HOOKS_TEST_ENV) }, - } - match self.xdg_data_home.take() { - Some(value) => unsafe { std::env::set_var(XDG_DATA_HOME_ENV, value) }, - None => unsafe { std::env::remove_var(XDG_DATA_HOME_ENV) }, + // SAFETY: fields drop only after this body returns, so `_guard` still + // holds the test environment mutex here. + unsafe { + match self.catalog_hooks.take() { + Some(value) => env::set_var(CATALOG_HOOKS_TEST_ENV, value), + None => env::remove_var(CATALOG_HOOKS_TEST_ENV), + } + match self.xdg_data_home.take() { + Some(value) => env::set_var(XDG_DATA_HOME_ENV, value), + None => env::remove_var(XDG_DATA_HOME_ENV), + } } } } @@ -81,7 +89,7 @@ fn named_options(temp: &Path, name: &str) -> SessionOptions { #[test] fn catalog_path_honors_xdg_data_home() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); assert_eq!( @@ -95,7 +103,7 @@ fn catalog_path_honors_xdg_data_home() { fn catalog_file_and_its_new_directories_are_private() { use std::os::unix::fs::PermissionsExt; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let data_home = temp.path().join("data"); let _env = EnvGuard::set_xdg_data_home(&data_home); let session = temp.path().join("lecture.wayscriber-session"); @@ -112,7 +120,7 @@ fn catalog_file_and_its_new_directories_are_private() { #[test] fn malformed_catalog_is_not_clobbered_by_upsert_failure() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let path = catalog_path(); fs::create_dir_all(path.parent().unwrap()).unwrap(); @@ -129,7 +137,7 @@ fn malformed_catalog_is_not_clobbered_by_upsert_failure() { #[test] fn equivalent_existing_paths_dedupe_after_canonicalization() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let sessions = temp.path().join("sessions"); fs::create_dir(&sessions).unwrap(); @@ -151,7 +159,7 @@ fn equivalent_existing_paths_dedupe_after_canonicalization() { #[test] fn missing_target_identity_uses_canonical_parent_plus_filename() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let parent = temp.path().join("sessions"); fs::create_dir(&parent).unwrap(); let missing = parent.join("new-session.wayscriber-session"); @@ -176,7 +184,7 @@ fn missing_target_identity_uses_canonical_parent_plus_filename() { #[test] fn duplicate_display_names_are_allowed_for_distinct_paths() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let left = temp.path().join("left"); let right = temp.path().join("right"); @@ -201,7 +209,7 @@ fn duplicate_display_names_are_allowed_for_distinct_paths() { #[test] fn upsert_with_display_name_creates_distinct_named_entry() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let source = temp.path().join("source.wayscriber-session"); let duplicate = temp.path().join("duplicate.wayscriber-session"); @@ -221,11 +229,11 @@ fn upsert_with_display_name_creates_distinct_named_entry() { #[test] fn forget_by_path_removes_metadata_only() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let session = temp.path().join("session.wayscriber-session"); let backup = { - let mut raw = std::ffi::OsString::from(session.as_os_str()); + let mut raw = OsString::from(session.as_os_str()); raw.push(".bak"); PathBuf::from(raw) }; @@ -242,7 +250,7 @@ fn forget_by_path_removes_metadata_only() { #[test] fn rename_display_name_changes_metadata_only_and_allows_duplicates() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let left = temp.path().join("left.wayscriber-session"); let right = temp.path().join("right.wayscriber-session"); @@ -270,7 +278,7 @@ fn rename_display_name_changes_metadata_only_and_allows_duplicates() { #[test] fn move_session_path_by_id_preserves_id_and_display_name() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session"); @@ -300,7 +308,7 @@ fn move_session_path_by_id_preserves_id_and_display_name() { #[test] fn move_session_path_by_id_rejects_catalog_target_collision() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let source = temp.path().join("lecture.wayscriber-session"); let target = temp.path().join("archive.wayscriber-session"); @@ -323,7 +331,7 @@ fn move_session_path_by_id_rejects_catalog_target_collision() { #[test] fn renamed_display_name_survives_later_upsert() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let session = temp.path().join("lecture.wayscriber-session"); fs::write(&session, b"{}").unwrap(); @@ -341,7 +349,7 @@ fn renamed_display_name_survives_later_upsert() { #[test] fn rename_display_name_rejects_empty_names() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let session = temp.path().join("lecture.wayscriber-session"); fs::write(&session, b"{}").unwrap(); @@ -355,7 +363,7 @@ fn rename_display_name_rejects_empty_names() { #[test] fn rename_display_name_by_path_changes_metadata_only() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let session = temp.path().join("lecture.wayscriber-session"); fs::write(&session, b"{}").unwrap(); @@ -373,7 +381,7 @@ fn rename_display_name_by_path_changes_metadata_only() { #[test] fn rename_display_name_by_path_returns_none_for_unknown_path() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let missing = temp.path().join("missing.wayscriber-session"); @@ -384,7 +392,7 @@ fn rename_display_name_by_path_returns_none_for_unknown_path() { #[test] fn failed_temp_write_leaves_existing_catalog_intact() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("sessions.json"); fs::write(&path, br#"{"version":1,"sessions":[]}"#).unwrap(); let tmp_path = temp.path().join("missing").join("sessions.json.tmp"); @@ -406,13 +414,13 @@ fn failed_temp_write_leaves_existing_catalog_intact() { #[test] fn named_backup_fallback_load_records_catalog_open() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let options = named_options(temp.path(), "backup-open"); - crate::session::save_snapshot(&sample_snapshot(), &options).unwrap(); + session::save_snapshot(&sample_snapshot(), &options).unwrap(); fs::rename(options.session_file_path(), options.backup_file_path()).unwrap(); - let loaded = crate::session::load_snapshot(&options) + let loaded = session::load_snapshot(&options) .unwrap() .expect("backup fallback should load"); @@ -427,13 +435,13 @@ fn named_backup_fallback_load_records_catalog_open() { #[test] fn named_recovery_fallback_load_records_catalog_open() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let _env = EnvGuard::set_xdg_data_home(temp.path()); let options = named_options(temp.path(), "recovery-open"); - crate::session::save_snapshot(&sample_snapshot(), &options).unwrap(); + session::save_snapshot(&sample_snapshot(), &options).unwrap(); fs::rename(options.session_file_path(), options.recovery_file_path()).unwrap(); - let loaded = crate::session::load_snapshot(&options) + let loaded = session::load_snapshot(&options) .unwrap() .expect("recovery fallback should load"); diff --git a/src/session/options/config.rs b/src/session/options/config.rs index e170bc6cd..498dfa55a 100644 --- a/src/session/options/config.rs +++ b/src/session/options/config.rs @@ -19,6 +19,7 @@ pub fn options_from_config( Ok(options_from_settings(session_cfg, base_dir, display_id)) } +#[must_use] pub fn options_from_config_for_named_file( session_cfg: &SessionConfig, path: PathBuf, diff --git a/src/session/options/tests.rs b/src/session/options/tests.rs index 883aa9c35..9ad7d8f97 100644 --- a/src/session/options/tests.rs +++ b/src/session/options/tests.rs @@ -1,14 +1,13 @@ -use std::fs; -use std::path::{Path, PathBuf}; -use std::sync::Mutex; #[cfg(unix)] -use std::{ - ffi::CString, - os::unix::{ - ffi::OsStrExt, - fs::{PermissionsExt, symlink}, - }, -}; +use std::ffi::CString; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +#[cfg(unix)] +use std::os::unix::fs::{PermissionsExt, symlink}; +use std::path::{Path, PathBuf}; +use std::{fs, io, process}; + +use crate::{test_env, test_temp}; use super::config::options_from_config; use super::identifiers::{resolve_display_id, sanitize_identifier}; @@ -17,8 +16,6 @@ use super::validation; use crate::config::{SessionConfig, SessionStorageMode}; use crate::env_vars::WAYLAND_DISPLAY_ENV; -static ENV_MUTEX: Mutex<()> = Mutex::new(()); - #[test] fn sanitize_identifier_replaces_non_alphanumeric() { assert_eq!(sanitize_identifier("DP-1"), "DP_1"); @@ -35,12 +32,10 @@ fn sanitize_identifier_empty_defaults_to_default() { fn resolve_display_id_prefers_argument_and_uses_env_fallback() { use std::env; - let _guard = ENV_MUTEX - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + let _guard = test_env::lock(); let prev = env::var_os(WAYLAND_DISPLAY_ENV); - // SAFETY: serialized via ENV_MUTEX + // SAFETY: the environment lease held above serializes environment access. unsafe { env::set_var(WAYLAND_DISPLAY_ENV, "wayland-0"); } @@ -51,9 +46,12 @@ fn resolve_display_id_prefers_argument_and_uses_env_fallback() { let from_env = resolve_display_id(None); assert_eq!(from_env, "wayland_0"); - match prev { - Some(v) => unsafe { env::set_var(WAYLAND_DISPLAY_ENV, v) }, - None => unsafe { env::remove_var(WAYLAND_DISPLAY_ENV) }, + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { + match prev { + Some(v) => env::set_var(WAYLAND_DISPLAY_ENV, v), + None => env::remove_var(WAYLAND_DISPLAY_ENV), + } } } @@ -165,7 +163,7 @@ fn force_resume_persistence_enables_all_session_payloads() { #[test] fn named_file_foreground_validation_accepts_writable_parent_and_cleans_probe() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("lecture-04.wayscriber-session"); validation::validate_named_session_file_for_foreground(&path) @@ -189,7 +187,7 @@ fn named_file_foreground_validation_accepts_writable_parent_and_cleans_probe() { #[cfg(unix)] #[test] fn named_file_open_validation_rejects_existing_session_in_readonly_parent() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let parent = temp.path().join("readonly-parent"); fs::create_dir(&parent).expect("create parent"); let path = parent.join("lecture-04.wayscriber-session"); @@ -207,7 +205,7 @@ fn named_file_open_validation_rejects_existing_session_in_readonly_parent() { #[test] fn named_file_offline_validation_allows_missing_parent() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp .path() .join("missing") @@ -221,7 +219,7 @@ fn named_file_offline_validation_allows_missing_parent() { #[test] fn named_file_validation_rejects_missing_directory_shaped_path() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let raw_path = format!("{}/", temp.path().join("lecture").display()); let path = PathBuf::from(raw_path); @@ -250,7 +248,7 @@ fn named_file_validation_rejects_missing_directory_shaped_path() { #[cfg(unix)] #[test] fn named_file_validation_rejects_fifo_target() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let path = temp.path().join("session-fifo.wayscriber-session"); make_fifo(&path); @@ -279,7 +277,7 @@ fn named_file_validation_rejects_fifo_target() { #[cfg(unix)] #[test] fn named_file_validation_rejects_symlink_target() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let target = temp.path().join("real-session.wayscriber-session"); let link = temp.path().join("linked-session.wayscriber-session"); fs::write(&target, b"{}").expect("write symlink target"); @@ -320,7 +318,7 @@ fn named_file_validation_rejects_parent_with_write_bits_but_no_effective_access( let manual_probe = parent.join(format!( ".wayscriber-session-manual-access-test-{}", - std::process::id() + process::id() )); if let Ok(file) = fs::OpenOptions::new() .write(true) @@ -334,7 +332,7 @@ fn named_file_validation_rejects_parent_with_write_bits_but_no_effective_access( let path = parent.join(format!( "wayscriber-session-validation-{}.wayscriber-session", - std::process::id() + process::id() )); let foreground_err = validation::validate_named_session_file_for_foreground(&path) .expect_err("foreground validation should reject process-unwritable parents"); @@ -381,7 +379,7 @@ fn readonly_dir_guard(path: &Path) -> ReadonlyDirGuard { fn can_create_probe(parent: &Path) -> bool { let probe = parent.join(format!( ".wayscriber-session-open-manual-access-test-{}", - std::process::id() + process::id() )); match fs::OpenOptions::new() .write(true) @@ -407,6 +405,6 @@ fn make_fifo(path: &Path) { 0, "mkfifo {} failed: {}", path.display(), - std::io::Error::last_os_error() + io::Error::last_os_error() ); } diff --git a/src/session/options/types.rs b/src/session/options/types.rs index 0fe8c7e71..c3246d227 100644 --- a/src/session/options/types.rs +++ b/src/session/options/types.rs @@ -84,10 +84,12 @@ impl SessionOptions { } } + #[must_use] pub fn any_enabled(&self) -> bool { self.persist_transparent || self.persist_whiteboard || self.persist_blackboard } + #[must_use] pub fn effective_history_limit(&self, runtime_limit: usize) -> usize { if !self.persist_history { return 0; @@ -98,6 +100,7 @@ impl SessionOptions { } } + #[must_use] pub fn is_named_file(&self) -> bool { matches!(self.target, SessionTarget::NamedFile(_)) } @@ -117,6 +120,7 @@ impl SessionOptions { self.restore_tool_state = true; } + #[must_use] pub fn session_file_path(&self) -> PathBuf { match &self.target { SessionTarget::Configured => self @@ -126,6 +130,7 @@ impl SessionOptions { } } + #[must_use] pub fn backup_file_path(&self) -> PathBuf { match &self.target { SessionTarget::Configured => self @@ -135,6 +140,7 @@ impl SessionOptions { } } + #[must_use] pub fn backup_recovery_marker_file_path(&self) -> PathBuf { match &self.target { SessionTarget::Configured => self @@ -144,6 +150,7 @@ impl SessionOptions { } } + #[must_use] pub fn recovery_file_path(&self) -> PathBuf { match &self.target { SessionTarget::Configured => self @@ -153,6 +160,7 @@ impl SessionOptions { } } + #[must_use] pub fn recovery_recoverable_marker_file_path(&self) -> PathBuf { match &self.target { SessionTarget::Configured => self.base_dir.join(format!( @@ -163,6 +171,7 @@ impl SessionOptions { } } + #[must_use] pub fn clear_marker_file_path(&self) -> PathBuf { match &self.target { SessionTarget::Configured => self @@ -172,6 +181,7 @@ impl SessionOptions { } } + #[must_use] pub fn lock_file_path(&self) -> PathBuf { match &self.target { SessionTarget::Configured => self @@ -181,6 +191,7 @@ impl SessionOptions { } } + #[must_use] pub fn file_prefix(&self) -> String { format!("session-{}", self.display_id) } @@ -210,6 +221,7 @@ impl SessionOptions { } } + #[must_use] pub fn output_identity(&self) -> Option<&str> { self.output_identity.as_deref() } diff --git a/src/session/options/validation.rs b/src/session/options/validation.rs index 9ac6ddbbf..86d209a04 100644 --- a/src/session/options/validation.rs +++ b/src/session/options/validation.rs @@ -1,9 +1,9 @@ use std::error::Error; -use std::fmt; use std::fs::{self, OpenOptions}; use std::io::ErrorKind; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; +use std::{fmt, path, process}; use anyhow::{Context, Result, anyhow}; @@ -29,6 +29,7 @@ impl MissingNamedSessionFile { } } + #[must_use] pub fn path(&self) -> &Path { &self.path } @@ -60,6 +61,7 @@ impl MissingNamedSessionParent { } } + #[must_use] pub fn path(&self) -> &Path { &self.path } @@ -77,7 +79,8 @@ impl fmt::Display for MissingNamedSessionParent { impl Error for MissingNamedSessionParent {} -pub fn normalize_named_session_file_arg(raw: &str) -> std::path::PathBuf { +#[must_use] +pub fn normalize_named_session_file_arg(raw: &str) -> PathBuf { expand_tilde(raw) } @@ -205,7 +208,7 @@ fn write_probe_path(parent: &Path) -> PathBuf { let id = NEXT_WRITE_PROBE_ID.fetch_add(1, Ordering::Relaxed); parent.join(format!( ".wayscriber-session-write-test-{}-{id}", - std::process::id() + process::id() )) } @@ -255,7 +258,7 @@ fn has_trailing_separator(path: &Path) -> bool { path.as_os_str() .as_encoded_bytes() .last() - .is_some_and(|last| *last == std::path::MAIN_SEPARATOR as u8) + .is_some_and(|last| *last == path::MAIN_SEPARATOR as u8) } fn validate_existing_parent_if_present(path: &Path, operation: &str) -> Result> { diff --git a/src/session/primary.rs b/src/session/primary.rs index cdacadd99..ad54960b6 100644 --- a/src/session/primary.rs +++ b/src/session/primary.rs @@ -1,11 +1,11 @@ use anyhow::{Context, Result}; use std::error::Error; -use std::fmt; use std::fs::{self, File, Metadata, OpenOptions}; use std::io::ErrorKind; #[cfg(unix)] use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::path::{Path, PathBuf}; +use std::{fmt, io}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum PrimaryTargetKind { @@ -192,7 +192,7 @@ pub(crate) fn make_session_artifact_private(path: &Path) -> Result<()> { }) } -fn raw_metadata(path: &Path, no_follow: bool) -> std::io::Result { +fn raw_metadata(path: &Path, no_follow: bool) -> io::Result { if no_follow { fs::symlink_metadata(path) } else { @@ -222,7 +222,7 @@ fn ensure_regular_session_artifact(path: &Path, metadata: Metadata) -> Result bool { +fn open_error_is_symlink(err: &io::Error) -> bool { err.raw_os_error() == Some(libc::ELOOP) } diff --git a/src/session/snapshot/appearance.rs b/src/session/snapshot/appearance.rs index e5911d25e..fadef1424 100644 --- a/src/session/snapshot/appearance.rs +++ b/src/session/snapshot/appearance.rs @@ -1,4 +1,5 @@ use crate::config::BoardGridConfig; +use crate::domain; use crate::domain::{BoardBackground, Color}; use crate::input::boards::{BoardAppearance, BoardPenOrigin, BoardState}; use serde::{Deserialize, Deserializer, Serialize}; @@ -44,7 +45,7 @@ impl BoardAppearanceSnapshot { return; } // The reserved overlay identity must stay transparent. - let background = if board.spec.id == crate::domain::BOARD_ID_TRANSPARENT { + let background = if board.spec.id == domain::BOARD_ID_TRANSPARENT { BoardBackground::Transparent } else { self.background diff --git a/src/session/snapshot/appearance/tests.rs b/src/session/snapshot/appearance/tests.rs index 6a53ff7c5..51c9e82c0 100644 --- a/src/session/snapshot/appearance/tests.rs +++ b/src/session/snapshot/appearance/tests.rs @@ -1,3 +1,13 @@ +use std::fs; + +use crate::config::{BoardGridKindConfig, BoardsConfig, Config}; +use crate::draw::Shape; +use crate::draw::frame::UndoAction; +use crate::input::BoardManager; +use crate::session::ToolStateSnapshot; +use crate::session::snapshot::types::BoardFile; +use crate::test_temp; + use super::*; use crate::domain::{BoardGrid, BoardGridKind}; use crate::draw::{Frame, RED}; @@ -7,8 +17,7 @@ use crate::session::{ }; fn snapshot(explicit: bool) -> SessionSnapshot { - let mut boards = - crate::input::BoardManager::from_config(crate::config::BoardsConfig::default()); + let mut boards = BoardManager::from_config(BoardsConfig::default()); boards.switch_to_id("whiteboard"); let board = boards.active_board_mut(); board.spec.grid = BoardGrid::new(BoardGridKind::IsometricDots, 20); @@ -30,7 +39,7 @@ fn snapshot(explicit: bool) -> SessionSnapshot { #[test] fn appearance_only_roundtrip_and_backup_recovery_preserve_exact_pen_none() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().into(), "paper"); options.persist_whiteboard = true; let original = snapshot(true); @@ -40,11 +49,8 @@ fn appearance_only_roundtrip_and_backup_recovery_preserve_exact_pen_none() { let appearance = loaded.boards[0].appearance.as_ref().unwrap(); assert!(appearance.explicit); assert_eq!(appearance.default_pen_color, None); - assert_eq!( - appearance.grid.kind, - crate::config::BoardGridKindConfig::IsometricDots - ); - std::fs::rename(options.session_file_path(), options.backup_file_path()).unwrap(); + assert_eq!(appearance.grid.kind, BoardGridKindConfig::IsometricDots); + fs::rename(options.session_file_path(), options.backup_file_path()).unwrap(); assert!(load_snapshot(&options).unwrap().unwrap().has_board_data()); } @@ -54,7 +60,7 @@ fn appearance_history_trimming_retains_only_explicit_empty_paper() { for explicit in [false, true] { let mut original = snapshot(explicit); let frame = &mut original.boards[0].pages.pages[0]; - let id = frame.add_shape(crate::draw::Shape::Line { + let id = frame.add_shape(Shape::Line { x1: 0, y1: 0, x2: 10, @@ -63,7 +69,7 @@ fn appearance_history_trimming_retains_only_explicit_empty_paper() { color: RED, }); frame.push_undo_action( - crate::draw::frame::UndoAction::Create { + UndoAction::Create { shapes: vec![(0, frame.shape(id).unwrap().clone())], }, 100, @@ -82,13 +88,13 @@ fn appearance_invalid_metadata_cannot_make_an_empty_board_recoverable() { original.boards[0].appearance.as_mut().unwrap().grid.spacing = -1; assert!(!original.has_board_data()); let value = serde_json::json!({"id":"whiteboard","pages":[],"active_page":0,"appearance":{"explicit":true,"grid":{"kind":"unknown"}}}); - let board: crate::session::snapshot::types::BoardFile = serde_json::from_value(value).unwrap(); + let board: BoardFile = serde_json::from_value(value).unwrap(); assert!(board.appearance.is_none()); } #[test] fn appearance_only_named_backup_survives_a_tool_only_primary() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().into(), "paper"); options.set_named_file_target(temp.path().join("paper.wayscriber")); options.persist_whiteboard = true; @@ -97,9 +103,7 @@ fn appearance_only_named_backup_survives_a_tool_only_primary() { let tools_only = SessionSnapshot { active_board_id: "whiteboard".into(), boards: Vec::new(), - tool_state: Some(crate::session::ToolStateSnapshot::from_config( - &crate::config::Config::default(), - )), + tool_state: Some(ToolStateSnapshot::from_config(&Config::default())), }; super::super::save::save_snapshot_with_report_and_clear_boundary(&tools_only, &options, false) .unwrap(); diff --git a/src/session/snapshot/apply.rs b/src/session/snapshot/apply.rs index 42d1955c3..2a2dc4133 100644 --- a/src/session/snapshot/apply.rs +++ b/src/session/snapshot/apply.rs @@ -1,3 +1,6 @@ +use crate::draw::TextMeasurer; +use crate::input::Tool; + use super::types::{BoardPagesSnapshot, SessionSnapshot, ToolStateSnapshot}; use crate::draw::BoardPages; use crate::input::{BOARD_ID_TRANSPARENT, InputState}; @@ -7,20 +10,20 @@ use std::collections::HashSet; /// Apply a session snapshot to the live [`InputState`]. pub fn apply_snapshot(input: &mut InputState, snapshot: SessionSnapshot, options: &SessionOptions) { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); apply_snapshot_inner(input, &measurer, snapshot, options, None); } fn apply_snapshot_inner( input: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, snapshot: SessionSnapshot, options: &SessionOptions, replacement_board_ids: Option<&HashSet>, ) { let previous_spec = &input.boards.active_board().spec; let previous_auto = previous_spec.auto_adjust_pen && !previous_spec.background.is_transparent(); - let previous_pen = input.color_for_tool(crate::input::Tool::Pen); + let previous_pen = input.color_for_tool(Tool::Pen); let previous_color = input.board_previous_color(); if replacement_board_ids.is_some() { input.boards.reset_appearances(); @@ -69,7 +72,7 @@ fn apply_snapshot_inner( if options.restore_tool_state { if let Some(tool_state) = snapshot.tool_state { - apply_tool_state_snapshot(input, measurer, tool_state); + apply_tool_state_snapshot(input, measurer, &tool_state); } else { log::info!("No tool state found in session; skipping tool restore"); } @@ -88,8 +91,8 @@ fn apply_snapshot_inner( /// last one run. pub(crate) fn apply_tool_state_snapshot( input: &mut InputState, - measurer: &crate::draw::TextMeasurer, - tool_state: ToolStateSnapshot, + measurer: &TextMeasurer, + tool_state: &ToolStateSnapshot, ) { let marker_opacity = tool_state .marker_opacity @@ -113,7 +116,7 @@ pub(crate) fn apply_tool_state_snapshot( tool_state.arrow_label_enabled ); let active_tool = input.active_tool(); - input.style.restore_snapshot(&tool_state, active_tool); + input.style.restore_snapshot(tool_state, active_tool); // The snapshot replaces every value a highlighted preset slot described, // and the per-field setters this used to go through invalidated the // canvas as they went; keep both effects now that the owner restores in @@ -136,7 +139,7 @@ pub(crate) fn apply_tool_state_snapshot( #[allow(dead_code)] pub(crate) fn apply_snapshot_replacing_boards( input: &mut InputState, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, snapshot: SessionSnapshot, options: &SessionOptions, ) -> Result<()> { @@ -171,7 +174,7 @@ pub(crate) fn apply_snapshot_replacing_boards( Ok(()) } -fn clear_board_pages(input: &mut InputState, measurer: &crate::draw::TextMeasurer) { +fn clear_board_pages(input: &mut InputState, measurer: &TextMeasurer) { input.cancel_active_interaction_with(measurer); // Every page is about to be replaced. A wheel adjustment still in flight // belongs to a frame that will not exist afterwards, so record it now diff --git a/src/session/snapshot/capture.rs b/src/session/snapshot/capture.rs index c31c80968..c743c9fbf 100644 --- a/src/session/snapshot/capture.rs +++ b/src/session/snapshot/capture.rs @@ -1,3 +1,5 @@ +use crate::draw::BoardPages; + use super::types::{BoardPagesSnapshot, BoardSnapshot, SessionSnapshot, ToolStateSnapshot}; use crate::input::InputState; use crate::session::options::SessionOptions; @@ -19,7 +21,7 @@ pub fn snapshot_from_input( let history_limit = options.effective_history_limit(input.history_limits.undo_stack_limit()); - let capture_pages = |pages: &crate::draw::BoardPages| -> BoardPagesSnapshot { + let capture_pages = |pages: &BoardPages| -> BoardPagesSnapshot { let cloned_pages = pages .pages() .iter() diff --git a/src/session/snapshot/compat_tests.rs b/src/session/snapshot/compat_tests.rs index 4530213ff..359f0416d 100644 --- a/src/session/snapshot/compat_tests.rs +++ b/src/session/snapshot/compat_tests.rs @@ -102,15 +102,14 @@ //! fs::remove_dir_all(&root).unwrap(); //! } //! ``` -use super::ToolStateSnapshot; +use std::path::PathBuf; + +use super::compression::DEFAULT_MAX_EXPANDED_SESSION_BYTES; +use super::generation::{self, Generation}; +use super::load::{self, LoadSnapshotOutcome}; +use super::tests::{sample_snapshot, set_modified}; use super::types::BoardFile; -use super::{ - compression::DEFAULT_MAX_EXPANDED_SESSION_BYTES, - generation::{self, Generation}, - load::{self, LoadSnapshotOutcome}, - save, - tests::{sample_snapshot, set_modified}, -}; +use super::{ToolStateSnapshot, save}; use crate::draw::Frame; use crate::session::{CompressionMode, SessionOptions}; use crate::test_temp::tempdir; @@ -273,7 +272,7 @@ enum Slot { } impl Slot { - fn path(self, o: &SessionOptions) -> std::path::PathBuf { + fn path(self, o: &SessionOptions) -> PathBuf { match self { Self::P => o.session_file_path(), Self::B => o.backup_file_path(), diff --git a/src/session/snapshot/compression.rs b/src/session/snapshot/compression.rs index 76b1d48fd..353adf07c 100644 --- a/src/session/snapshot/compression.rs +++ b/src/session/snapshot/compression.rs @@ -1,5 +1,9 @@ +use std::error; + use anyhow::{Context, Result}; -use flate2::{Compression, bufread::GzDecoder, write::GzEncoder}; +use flate2::Compression; +use flate2::bufread::GzDecoder; +use flate2::write::GzEncoder; use std::fmt; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; @@ -22,7 +26,7 @@ impl fmt::Display for ExpandedSessionTooLarge { } } -impl std::error::Error for ExpandedSessionTooLarge {} +impl error::Error for ExpandedSessionTooLarge {} pub(super) fn compress_bytes(data: &[u8]) -> Result> { let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); diff --git a/src/session/snapshot/generation/probe.rs b/src/session/snapshot/generation/probe.rs index 1e13ca976..cae083273 100644 --- a/src/session/snapshot/generation/probe.rs +++ b/src/session/snapshot/generation/probe.rs @@ -100,12 +100,12 @@ fn scan_header(bytes: &[u8]) -> Option { found } -struct HeaderCursor<'a> { - bytes: &'a [u8], +struct HeaderCursor<'bytes> { + bytes: &'bytes [u8], offset: usize, } -impl<'a> HeaderCursor<'a> { +impl<'bytes> HeaderCursor<'bytes> { fn peek(&self) -> Option { self.bytes.get(self.offset).copied() } @@ -128,7 +128,7 @@ impl<'a> HeaderCursor<'a> { Some(()) } - fn string(&mut self, limit: usize, key: bool) -> Option<&'a [u8]> { + fn string(&mut self, limit: usize, key: bool) -> Option<&'bytes [u8]> { self.expect(b'"')?; let start = self.offset; while let Some(byte) = self.peek() { @@ -165,6 +165,6 @@ impl<'a> HeaderCursor<'a> { { return None; } - std::str::from_utf8(digits).ok()?.parse().ok() + str::from_utf8(digits).ok()?.parse().ok() } } diff --git a/src/session/snapshot/generation/tests.rs b/src/session/snapshot/generation/tests.rs index fb26c517c..483dfd5d5 100644 --- a/src/session/snapshot/generation/tests.rs +++ b/src/session/snapshot/generation/tests.rs @@ -1,3 +1,8 @@ +use std::path::PathBuf; +use std::thread; + +use crate::session; + use super::marker::read_marker_generation; use super::probe::{ PROBE_READ_BYTES, PROBE_SCAN_BYTES, inflate_probe_prefix, probe_payload_header, @@ -5,7 +10,8 @@ use super::probe::{ }; use super::*; -use crate::session::snapshot::{load, save, tests::sample_snapshot}; +use crate::session::snapshot::tests::sample_snapshot; +use crate::session::snapshot::{load, save}; use crate::session::{CompressionMode, SessionOptions}; use crate::test_temp::tempdir; use std::fs::{self, File}; @@ -177,13 +183,13 @@ fn marker_format_round_trips_and_rejects_wrong_kind_or_version() { } } -fn options(path: &std::path::Path) -> SessionOptions { +fn options(path: &Path) -> SessionOptions { let mut options = SessionOptions::new(path.to_path_buf(), "generation"); options.persist_transparent = true; options.compression = CompressionMode::Off; options } -fn generation(path: std::path::PathBuf) -> Generation { +fn generation(path: PathBuf) -> Generation { payload_prefix_generation(&fs::read(path).unwrap()) } @@ -272,7 +278,7 @@ fn concurrent_writers_allocate_in_exclusive_lock_order() { let mut threads = vec![]; for _ in 0..2 { let options = options.clone(); - threads.push(std::thread::spawn(move || { + threads.push(thread::spawn(move || { (0..20) .map(|_| { save::save_snapshot_with_report(&sample_snapshot(), &options) @@ -479,7 +485,7 @@ fn writer_header_restart_output_isolation_and_clear_reset_are_stable() { .generation, Some(1) ); - crate::session::clear_session(&first).unwrap(); + session::clear_session(&first).unwrap(); assert_eq!( save::save_snapshot_with_report(&snapshot, &first) .unwrap() @@ -518,7 +524,8 @@ fn each_marker_slot_advances_the_next_real_save() { #[test] fn socket_and_symlink_sidecars_are_classified_without_blocking_autosave() { - use std::os::unix::{fs::symlink, net::UnixListener}; + use std::os::unix::fs::symlink; + use std::os::unix::net::UnixListener; for slot in ["clear", "recovery", "backup"] { for kind in ["socket", "socket link", "loop", "regular link"] { let short_root = [Path::new("/tmp"), Path::new(".")] diff --git a/src/session/snapshot/generation/view.rs b/src/session/snapshot/generation/view.rs index 5be5bcf7c..3e7a30c1c 100644 --- a/src/session/snapshot/generation/view.rs +++ b/src/session/snapshot/generation/view.rs @@ -1,10 +1,8 @@ use super::marker::{MarkerKind, read_marker_generation}; use super::probe::probe_payload_header; use super::{Generation, MAX_GENERATION}; -use crate::session::{ - SessionOptions, - primary::{is_non_regular_session_artifact, open_session_artifact_for_probe}, -}; +use crate::session::SessionOptions; +use crate::session::primary::{is_non_regular_session_artifact, open_session_artifact_for_probe}; use anyhow::{Result, anyhow}; use log::warn; use std::path::{Path, PathBuf}; diff --git a/src/session/snapshot/load.rs b/src/session/snapshot/load.rs index df4d56fa1..b61c88a6f 100644 --- a/src/session/snapshot/load.rs +++ b/src/session/snapshot/load.rs @@ -1,15 +1,10 @@ -use super::compression::{ - DEFAULT_MAX_EXPANDED_SESSION_BYTES, ExpandedSessionTooLarge, is_gzip, - maybe_decompress_with_limit, -}; -use super::history::{ - apply_history_policies, enforce_shape_limits, max_history_depth, strip_history_fields, -}; -use super::types::{ - BoardFile, BoardPagesSnapshot, BoardSnapshot, CURRENT_VERSION, SessionFile, SessionSnapshot, -}; -use crate::draw::Frame; -use crate::draw::frame::MAX_COMPOUND_DEPTH; +use std::error; + +use crate::session; +use crate::session::catalog; + +use super::compression::DEFAULT_MAX_EXPANDED_SESSION_BYTES; +use super::types::SessionSnapshot; use crate::session::lock::{ lock_shared, open_existing_runtime_lock_file_for_read, open_runtime_lock_file, unlock, }; @@ -19,12 +14,9 @@ use crate::session::primary::{ session_artifact_metadata_if_exists, }; use anyhow::{Context, Result, anyhow}; -use log::{debug, info, warn}; -use serde_json::Value; -use std::fmt; -use std::fs; -use std::io::Read; +use log::{info, warn}; use std::path::{Path, PathBuf}; +use std::{fmt, fs}; mod artifact_outcome; mod corrupt; @@ -35,12 +27,11 @@ mod payload; pub(crate) use artifact_outcome::CorruptArtifactPreservationFailed; use artifact_outcome::{CorruptLoadAction, artifact_load_outcome}; -use corrupt::{preserve_newer_version_session, quarantine_corrupt_artifact}; use fallback::load_normal_session_or_empty; use markers::{ - backup_is_newer_than_primary, clear_marker_metadata, clear_marker_suppresses_artifact, - preserve_unloadable_recovery, recoverable_backup_marker_metadata, - recoverable_recovery_marker_metadata, should_prefer_recovery, + clear_marker_metadata, clear_marker_suppresses_artifact, preserve_unloadable_recovery, + recoverable_backup_marker_metadata, recoverable_recovery_marker_metadata, + should_prefer_recovery, }; use named_candidate::{load_named_candidate_with_fallbacks, log_named_candidate_outcome}; use payload::load_snapshot_opened_with_expanded_limit; @@ -109,7 +100,7 @@ impl fmt::Display for NewerVersionPreservationFailed { } } -impl std::error::Error for NewerVersionPreservationFailed {} +impl error::Error for NewerVersionPreservationFailed {} impl LoadSnapshotOutcome { #[allow(dead_code)] @@ -184,7 +175,7 @@ pub(super) fn load_named_session_candidate_with_expanded_limit( } let session_path = options.session_file_path(); - crate::session::validate_named_session_file_for_open(&session_path)?; + session::validate_named_session_file_for_open(&session_path)?; let result = load_named_candidate_with_fallbacks(&session_path, options, max_expanded_size); @@ -274,7 +265,7 @@ fn load_snapshot_with_expanded_limit_inner( return load_normal_session_or_empty( options, &session_path, - session_metadata, + session_metadata.as_ref(), max_expanded_size, clear_marker_metadata.as_ref(), backup_recovery_marker_metadata.as_ref(), @@ -335,7 +326,7 @@ fn load_snapshot_with_expanded_limit_inner( load_normal_session_or_empty( options, &session_path, - session_metadata, + session_metadata.as_ref(), max_expanded_size, clear_marker_metadata.as_ref(), backup_recovery_marker_metadata.as_ref(), @@ -482,7 +473,7 @@ fn record_named_session_opened_for_outcome( | LoadSnapshotOutcome::RestoredAfterCorruption { .. } ) { - crate::session::catalog::record_named_session_opened(options); + catalog::record_named_session_opened(options); } } diff --git a/src/session/snapshot/load/artifact_outcome.rs b/src/session/snapshot/load/artifact_outcome.rs index 9c14a3d7d..2c4297f42 100644 --- a/src/session/snapshot/load/artifact_outcome.rs +++ b/src/session/snapshot/load/artifact_outcome.rs @@ -1,6 +1,16 @@ //! What one artifact's load result means: a snapshot, an empty session, a //! preserved unreadable copy, or a refusal to continue. -use super::*; +use std::error; + +use super::super::compression::ExpandedSessionTooLarge; +use super::corrupt::quarantine_corrupt_artifact; +use super::{LoadSnapshotOutcome, LoadedSnapshot, NewerVersionPreservationFailed}; +use crate::session::options::SessionOptions; +use crate::session::primary::is_non_regular_session_artifact; +use anyhow::Result; +use log::{debug, info, warn}; +use std::fmt; +use std::path::{Path, PathBuf}; #[derive(Debug)] pub(crate) struct CorruptArtifactPreservationFailed { @@ -20,7 +30,7 @@ impl fmt::Display for CorruptArtifactPreservationFailed { } } -impl std::error::Error for CorruptArtifactPreservationFailed {} +impl error::Error for CorruptArtifactPreservationFailed {} #[derive(Clone, Copy)] pub(super) enum CorruptLoadAction { diff --git a/src/session/snapshot/load/corrupt.rs b/src/session/snapshot/load/corrupt.rs index c47371ea2..090a11587 100644 --- a/src/session/snapshot/load/corrupt.rs +++ b/src/session/snapshot/load/corrupt.rs @@ -1,10 +1,23 @@ -use super::*; +use crate::session; +use crate::session::{artifacts, lock, primary}; + +use super::super::types::SessionSnapshot; +use super::{NewerVersionAction, RestoredArtifact, is_named_primary_path, payload}; use crate::durable_io::{ AtomicWriteOptions, DestinationExpectation, DurableIoError, FileIdentity, OverwriteMode, PermissionPolicy, SymlinkPolicy, sync_parent_dir, write_atomic, write_atomic_reporting_identity, }; -use std::io::ErrorKind; +use crate::session::lock::{open_runtime_lock_file, unlock}; +use crate::session::options::SessionOptions; +use crate::session::primary::{ + open_session_artifact_for_read, session_artifact_metadata, session_artifact_metadata_if_exists, +}; +use anyhow::{Context, Result, anyhow}; +use log::{debug, info, warn}; +use std::fs; +use std::io::{ErrorKind, Read}; +use std::path::{Path, PathBuf}; /// Keeps unreadable bytes away from rotation backups. Called under the session lock. pub(super) fn quarantine_corrupt_artifact( @@ -12,18 +25,17 @@ pub(super) fn quarantine_corrupt_artifact( options: &SessionOptions, max_expanded_size: u64, ) -> Result { - let copies = crate::session::artifacts::corrupt_copies_of(artifact)?; + let copies = artifacts::corrupt_copies_of(artifact)?; let named_bytes = if is_named_primary_path(artifact, options) { read_artifact_bounded(artifact, max_expanded_size.max(options.max_file_size_bytes)).ok() } else { None }; if let Some(bytes) = &named_bytes { - for copy in copies.iter().filter(|copy| copy.regular_file) { + let regular_copies = copies.iter().filter(|copy| copy.regular_file); + for copy in regular_copies { if preserved_already_holds(©.path, bytes) { - if let Err(error) = - crate::session::primary::make_session_artifact_private(©.path) - { + if let Err(error) = primary::make_session_artifact_private(©.path) { warn!( "Diagnostic bytes are preserved at {}, but permissions could not be restricted: {error}", copy.path.display() @@ -40,7 +52,7 @@ pub(super) fn quarantine_corrupt_artifact( let seq = last_seq .checked_add(attempt) .ok_or_else(|| anyhow!("corrupt copy sequence exhausted"))?; - let target = crate::session::artifacts::corrupt_copy_path(artifact, seq); + let target = artifacts::corrupt_copy_path(artifact, seq); if let Some(bytes) = &named_bytes { match write_atomic( &target, @@ -63,9 +75,9 @@ pub(super) fn quarantine_corrupt_artifact( Err(err) => warn!("Could not copy unreadable session; moving it aside: {err}"), } } - match crate::session::artifacts::rename_artifact_no_replace(artifact, &target) { + match artifacts::rename_artifact_no_replace(artifact, &target) { Ok(()) => { - if let Err(err) = crate::session::primary::make_session_artifact_private(&target) { + if let Err(err) = primary::make_session_artifact_private(&target) { warn!( "Could not restrict corrupt copy {}: {err}", target.display() @@ -86,15 +98,15 @@ pub(super) fn quarantine_corrupt_artifact( } fn prune_corrupt_copies(artifact: &Path, keep: &Path) { - let Ok(copies) = crate::session::artifacts::corrupt_copies_of(artifact) else { + let Ok(copies) = artifacts::corrupt_copies_of(artifact) else { return; }; let mut count = copies.iter().filter(|copy| copy.regular_file).count(); - for copy in copies + let removable_copies = copies .into_iter() - .filter(|copy| copy.regular_file && copy.path != keep) - { - if count <= crate::session::artifacts::MAX_CORRUPT_COPIES_PER_ARTIFACT { + .filter(|copy| copy.regular_file && copy.path != keep); + for copy in removable_copies { + if count <= artifacts::MAX_CORRUPT_COPIES_PER_ARTIFACT { break; } match fs::remove_file(©.path) { @@ -119,7 +131,7 @@ pub(super) fn restore_named_primary_after_corruption( ) -> Result>> { use std::io::{Seek, SeekFrom}; let lock = open_runtime_lock_file(&options.lock_file_path(), true)?; - crate::session::lock::lock_exclusive(&lock)?; + lock::lock_exclusive(&lock)?; let result = (|| { let primary = options.session_file_path(); let copy_len = session_artifact_metadata(copy, true)?.len(); @@ -238,7 +250,7 @@ pub(super) fn preserve_newer_version_session( let digest = content_digest(bytes); let base = format!( ".v{version}{}{digest:016x}", - crate::session::artifacts::PRESERVED_SESSION_MARKER + artifacts::PRESERVED_SESSION_MARKER ); let mut last_error = None; @@ -248,10 +260,10 @@ pub(super) fn preserve_newer_version_session( } else { format!("{base}-{attempt}") }; - let preserved_path = crate::session::append_path_suffix(session_path, &suffix); + let preserved_path = session::append_path_suffix(session_path, &suffix); if preserved_already_holds(&preserved_path, bytes) { - crate::session::primary::make_session_artifact_private(&preserved_path)?; + primary::make_session_artifact_private(&preserved_path)?; debug!( "Newer-version session {} is already preserved at {}", session_path.display(), @@ -295,28 +307,25 @@ pub(super) fn preserve_newer_version_session( for attempt in 0..MAX_PRESERVE_ATTEMPTS { let suffix = if attempt == 0 { - format!( - "{base}{}", - crate::session::artifacts::PRESERVED_SESSION_MOVED_SUFFIX - ) + format!("{base}{}", artifacts::PRESERVED_SESSION_MOVED_SUFFIX) } else { format!( "{base}{}-{attempt}", - crate::session::artifacts::PRESERVED_SESSION_MOVED_SUFFIX + artifacts::PRESERVED_SESSION_MOVED_SUFFIX ) }; - let fallback_path = crate::session::append_path_suffix(session_path, &suffix); + let fallback_path = session::append_path_suffix(session_path, &suffix); // A previous fallback may already have established the required copy. // Trust it only after the same exact-byte verification as copy paths. if preserved_already_holds(&fallback_path, bytes) { - crate::session::primary::make_session_artifact_private(&fallback_path)?; + primary::make_session_artifact_private(&fallback_path)?; return Ok(fallback_path); } - match crate::session::artifacts::rename_artifact_no_replace(session_path, &fallback_path) { + match artifacts::rename_artifact_no_replace(session_path, &fallback_path) { Ok(()) => { - crate::session::primary::make_session_artifact_private(&fallback_path)?; + primary::make_session_artifact_private(&fallback_path)?; sync_parent_dir(&fallback_path).with_context(|| { format!( "moved newer-version session to {}, but failed to sync its directory", @@ -386,11 +395,14 @@ fn read_artifact_bounded(path: &Path, limit: u64) -> Result> { #[cfg(test)] mod tests { + use crate::test_temp; + use super::*; + use crate::session::snapshot::load::with_shared_session_lock; #[test] fn restored_backup_cleanup_does_not_remove_a_changed_named_primary() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "named"); options.persist_transparent = true; options.set_named_file_target(temp.path().join("board.wayscriber-session")); @@ -428,8 +440,7 @@ mod tests { let mut scratch = SessionOptions::new(dir.to_path_buf(), "scratch"); scratch.persist_transparent = true; scratch.set_named_file_target(dir.join("scratch.wayscriber-session")); - crate::session::save_snapshot(&super::super::super::tests::sample_snapshot(), &scratch) - .unwrap(); + session::save_snapshot(&super::super::super::tests::sample_snapshot(), &scratch).unwrap(); fs::read(scratch.session_file_path()).unwrap() } @@ -448,7 +459,7 @@ mod tests { #[test] fn restored_ink_too_large_for_a_session_file_is_not_written_into_the_primary() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let (mut options, corrupt_copy, corrupt_bytes) = quarantined_named_session(temp.path()); let backup = saved_session_bytes(temp.path()); fs::write(options.backup_file_path(), &backup).unwrap(); @@ -473,7 +484,7 @@ mod tests { #[test] fn restore_creates_a_primary_that_was_moved_aside() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let (options, corrupt_copy, _) = quarantined_named_session(temp.path()); fs::remove_file(options.session_file_path()).unwrap(); let backup = saved_session_bytes(temp.path()); diff --git a/src/session/snapshot/load/fallback.rs b/src/session/snapshot/load/fallback.rs index 0983ea1c5..1dd5c3bd1 100644 --- a/src/session/snapshot/load/fallback.rs +++ b/src/session/snapshot/load/fallback.rs @@ -1,18 +1,28 @@ -use super::*; +use super::super::types::SessionSnapshot; +use super::artifact_outcome::CorruptLoadAction; +use super::markers::{ + backup_is_newer_than_primary, clear_marker_suppresses_artifact, preserve_unloadable_recovery, +}; +use super::{LoadSnapshotOutcome, RestoredArtifact, corrupt, load_snapshot_path_with_outcome}; +use crate::session::options::SessionOptions; +use anyhow::{Result, anyhow}; +use log::{info, warn}; +use std::fs; +use std::path::{Path, PathBuf}; // These arguments carry the loader artifact view and its preservation/size policy. #[allow(clippy::too_many_arguments)] pub(super) fn load_normal_session_or_empty( options: &SessionOptions, session_path: &Path, - session_metadata: Option, + session_metadata: Option<&fs::Metadata>, max_expanded_size: u64, clear_marker_metadata: Option<&fs::Metadata>, backup_recovery_marker_metadata: Option<&fs::Metadata>, recovery_recoverable_marker_metadata: Option<&fs::Metadata>, restore_named_primary: bool, ) -> Result { - let Some(primary_metadata) = session_metadata.as_ref() else { + let Some(primary_metadata) = session_metadata else { if let Some(backup) = load_contentful_backup( options, max_expanded_size, diff --git a/src/session/snapshot/load/markers.rs b/src/session/snapshot/load/markers.rs index c1d986c62..6b43afef7 100644 --- a/src/session/snapshot/load/markers.rs +++ b/src/session/snapshot/load/markers.rs @@ -1,5 +1,12 @@ -use super::*; +use crate::session; +use crate::session::options::SessionOptions; +use crate::session::primary::{ + is_non_regular_session_artifact, session_artifact_metadata_if_exists, +}; use crate::session::snapshot::generation::{ArtifactStamp, LegacyOrder, cleared_by, written_after}; +use log::{info, warn}; +use std::fs; +use std::path::{Path, PathBuf}; pub(super) fn backup_is_newer_than_primary(backup: &fs::Metadata, primary: &fs::Metadata) -> bool { written_after( @@ -127,13 +134,13 @@ fn preserved_recovery_path(path: &Path, reason: &str) -> PathBuf { } else { format!(".{reason}.{index}") }; - let candidate = crate::session::append_path_suffix(path, &suffix); + let candidate = session::append_path_suffix(path, &suffix); if !candidate.exists() { return candidate; } } - crate::session::append_path_suffix(path, &format!(".{reason}.100")) + session::append_path_suffix(path, &format!(".{reason}.100")) } pub(super) fn should_prefer_recovery( diff --git a/src/session/snapshot/load/named_candidate.rs b/src/session/snapshot/load/named_candidate.rs index fc773098a..81ccac936 100644 --- a/src/session/snapshot/load/named_candidate.rs +++ b/src/session/snapshot/load/named_candidate.rs @@ -1,4 +1,21 @@ -use super::*; +use super::super::compression::ExpandedSessionTooLarge; +use super::super::types::SessionSnapshot; +use super::markers::{ + backup_is_newer_than_primary, clear_marker_metadata, clear_marker_suppresses_artifact, + recoverable_backup_marker_metadata, recoverable_recovery_marker_metadata, + should_prefer_recovery, +}; +use super::payload::load_snapshot_opened_with_expanded_limit; +use super::{LoadSnapshotOutcome, LoadedSnapshot, NewerVersionAction}; +use crate::session::options::SessionOptions; +use crate::session::primary::{ + is_non_regular_session_artifact, open_session_artifact_for_read, session_artifact_metadata, + session_artifact_metadata_if_exists, +}; +use anyhow::{Context, Result, anyhow}; +use log::{info, warn}; +use std::fs; +use std::path::Path; pub(super) fn log_named_candidate_outcome(session_path: &Path, outcome: &LoadSnapshotOutcome) { let (source, snapshot) = match outcome { diff --git a/src/session/snapshot/load/payload.rs b/src/session/snapshot/load/payload.rs index 9fe68b0e6..ea945ff02 100644 --- a/src/session/snapshot/load/payload.rs +++ b/src/session/snapshot/load/payload.rs @@ -1,4 +1,21 @@ -use super::*; +use super::super::compression::{ExpandedSessionTooLarge, is_gzip, maybe_decompress_with_limit}; +use super::super::history::{ + apply_history_policies, enforce_shape_limits, max_history_depth, strip_history_fields, +}; +use super::super::types::{ + BoardFile, BoardPagesSnapshot, BoardSnapshot, CURRENT_VERSION, SessionFile, SessionSnapshot, +}; +use super::corrupt::preserve_newer_version_session; +use super::{LoadedSnapshot, NewerVersionAction, NewerVersionPreservationFailed}; +use crate::draw::Frame; +use crate::draw::frame::MAX_COMPOUND_DEPTH; +use crate::session::options::SessionOptions; +use anyhow::{Context, Result, anyhow}; +use log::{debug, warn}; +use serde_json::Value; +use std::fs; +use std::io::Read; +use std::path::Path; pub(super) fn load_snapshot_opened_with_expanded_limit( session_path: &Path, diff --git a/src/session/snapshot/save.rs b/src/session/snapshot/save.rs index f0f699686..4074f6048 100644 --- a/src/session/snapshot/save.rs +++ b/src/session/snapshot/save.rs @@ -1,19 +1,23 @@ -use super::compression::{DEFAULT_MAX_EXPANDED_SESSION_BYTES, compress_bytes, temp_path}; +use std::io; +use std::io::ErrorKind; + +use crate::session::{catalog, primary}; +use crate::{durable_io, session}; + +use super::compression::{DEFAULT_MAX_EXPANDED_SESSION_BYTES, temp_path}; use super::generation::{ArtifactSetView, UnreadablePolicy}; -use super::types::{ - BoardFile, BoardPagesSnapshot, BoardSnapshot, CURRENT_VERSION, SessionFile, SessionSnapshot, -}; +use super::types::SessionSnapshot; use crate::session::lock::{lock_exclusive, open_runtime_lock_file, unlock}; -use crate::session::options::{CompressionMode, SessionOptions}; +use crate::session::options::SessionOptions; use crate::time_utils::now_rfc3339; -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result}; use log::{debug, info, warn}; use payload::PayloadStamp; use std::fs::{self, OpenOptions}; use std::io::Write; use std::os::unix::fs::OpenOptionsExt; use std::path::{Path, PathBuf}; -use std::time::{Duration, Instant}; +use std::time::Instant; const AUTOSAVE_HISTORY_FALLBACK_DEPTH: usize = 1; @@ -27,8 +31,9 @@ mod tests; pub(crate) use estimate::{ estimate_snapshot_payload, estimate_snapshot_save, estimate_snapshot_without_history_payload, }; +#[cfg(test)] pub(super) use payload::snapshot_without_history; -use payload::{PayloadCandidate, estimate_from_candidate, payload_candidate, payload_within_limit}; +use payload::{PayloadCandidate, payload_within_limit}; use recovery::{ preserve_oversized_recovery, remove_backup_file, remove_backup_recovery_marker_file, remove_clear_marker_file, remove_recoverable_artifacts_suppressed_by_clear_marker, @@ -180,7 +185,7 @@ fn save_snapshot_with_expanded_limit_and_strategy( } if matches!(&result, Ok(Some(_))) { - crate::session::catalog::record_named_session_saved(options); + catalog::record_named_session_saved(options); } result @@ -194,7 +199,7 @@ fn save_snapshot_with_expanded_limit_and_strategy( /// reported for a replacement that power loss can still undo. Autosave retries /// make the failure recoverable. pub(super) fn sync_session_parent_dir(path: &Path, label: &str) -> Result<()> { - crate::durable_io::sync_parent_dir(path).with_context(|| { + durable_io::sync_parent_dir(path).with_context(|| { format!( "failed to sync session directory after replacing {} {}", label, @@ -205,7 +210,7 @@ pub(super) fn sync_session_parent_dir(path: &Path, label: &str) -> Result<()> { fn prepare_session_parent_for_save(options: &SessionOptions) -> Result<()> { if options.is_named_file() { - crate::session::validate_named_session_file_for_foreground(&options.session_file_path())?; + session::validate_named_session_file_for_foreground(&options.session_file_path())?; return Ok(()); } @@ -223,7 +228,7 @@ fn prepare_session_parent_for_save(options: &SessionOptions) -> Result<()> { /// Keeping replacement as one rename is especially important when backups are /// disabled: unlinking the primary first would create a crash window with no /// recoverable session file at either path. -fn replace_session_file(replacement: &Path, primary: &Path) -> std::io::Result<()> { +fn replace_session_file(replacement: &Path, primary: &Path) -> io::Result<()> { fs::rename(replacement, primary) } @@ -254,7 +259,7 @@ impl Drop for TempFileGuard { fn drop(&mut self) { if let Some(path) = self.path.take() && let Err(err) = fs::remove_file(&path) - && err.kind() != std::io::ErrorKind::NotFound + && err.kind() != ErrorKind::NotFound { warn!( "Failed to remove temporary session file {}: {}", @@ -409,7 +414,7 @@ fn save_snapshot_inner( // it before rotation so the backup does not preserve that legacy // exposure. Opening through the session-artifact helper also // refuses a symlink instead of chmodding its target. - crate::session::primary::make_session_artifact_private(&session_path)?; + primary::make_session_artifact_private(&session_path)?; // rename replaces an existing backup atomically; removing it first // would only open a window in which no backup exists at all. fs::rename(&session_path, &backup_path).with_context(|| { @@ -473,21 +478,36 @@ fn save_snapshot_inner( log_near_limit(&report); cleanup_snapshot_artifacts_after_save( options, - snapshot_has_board_data, - contentless_clear_boundary, - had_session_file, - preserves_recoverable_recovery, + SavedSnapshotState { + snapshot_has_board_data, + contentless_clear_boundary, + had_session_file, + preserves_recoverable_recovery, + }, ); Ok(Some(report)) } -fn cleanup_snapshot_artifacts_after_save( - options: &SessionOptions, +/// What a completed save wrote, which decides the artifacts it leaves behind. +#[derive(Debug, Clone, Copy)] +struct SavedSnapshotState { snapshot_has_board_data: bool, + /// A contentless save that records a clear boundary. contentless_clear_boundary: bool, + /// A primary session file existed before this save. had_session_file: bool, + /// The recovery copy still holds the only recoverable drawings. preserves_recoverable_recovery: bool, -) { +} + +fn cleanup_snapshot_artifacts_after_save(options: &SessionOptions, saved: SavedSnapshotState) { + let SavedSnapshotState { + snapshot_has_board_data, + contentless_clear_boundary, + had_session_file, + preserves_recoverable_recovery, + } = saved; + if snapshot_has_board_data { if remove_recoverable_artifacts_suppressed_by_clear_marker(options) { remove_clear_marker_file(options); diff --git a/src/session/snapshot/save/estimate.rs b/src/session/snapshot/save/estimate.rs index f9bb016ba..b80bf387a 100644 --- a/src/session/snapshot/save/estimate.rs +++ b/src/session/snapshot/save/estimate.rs @@ -1,5 +1,13 @@ //! Size estimates for a save that has not happened yet. -use super::*; +use super::super::compression::DEFAULT_MAX_EXPANDED_SESSION_BYTES; +use super::super::types::SessionSnapshot; +use super::model::{SnapshotPayloadEstimate, SnapshotSaveEstimate}; +use super::payload::{ + PayloadStamp, estimate_from_candidate, payload_candidate, snapshot_without_history, +}; +use crate::session::options::SessionOptions; +use crate::time_utils::now_rfc3339; +use anyhow::Result; pub(crate) fn estimate_snapshot_save( snapshot: &SessionSnapshot, diff --git a/src/session/snapshot/save/model.rs b/src/session/snapshot/save/model.rs index efe2aa3f7..f7938c98d 100644 --- a/src/session/snapshot/save/model.rs +++ b/src/session/snapshot/save/model.rs @@ -1,3 +1,5 @@ +use std::error; + use super::payload::is_near_limit; use log::debug; use std::fmt; @@ -129,7 +131,7 @@ impl fmt::Display for SavePayloadTooLarge { } } -impl std::error::Error for SavePayloadTooLarge {} +impl error::Error for SavePayloadTooLarge {} pub(super) fn log_near_limit(report: &SaveSnapshotReport) { if report.is_near_limit() { diff --git a/src/session/snapshot/save/payload.rs b/src/session/snapshot/save/payload.rs index b4a5b0c05..54543e9ee 100644 --- a/src/session/snapshot/save/payload.rs +++ b/src/session/snapshot/save/payload.rs @@ -1,16 +1,27 @@ -use super::*; +use super::super::compression::compress_bytes; +use super::super::types::{ + BoardFile, BoardPagesSnapshot, BoardSnapshot, CURRENT_VERSION, SessionFile, SessionSnapshot, +}; +use super::model::{ + HistoryFallbackStrategy, SaveLimitExceeded, SavePayloadTooLarge, SaveSnapshotOutcome, + SnapshotPayloadEstimate, +}; +use crate::session::options::{CompressionMode, SessionOptions}; use crate::session::snapshot::generation::{MAX_GENERATION, payload_prefix_generation}; +use anyhow::{Context, Result}; +use log::{debug, info, warn}; +use std::time::{Duration, Instant}; #[derive(Clone, Copy)] -pub(super) struct PayloadStamp<'a> { - pub(super) last_modified: &'a str, +pub(super) struct PayloadStamp<'modified> { + pub(super) last_modified: &'modified str, pub(super) generation: Option, } -impl<'a> PayloadStamp<'a> { +impl<'modified> PayloadStamp<'modified> { /// The widest stamp a save can write, for estimates made before a /// generation is allocated. - pub(super) fn widest(last_modified: &'a str) -> Self { + pub(super) fn widest(last_modified: &'modified str) -> Self { Self { last_modified, generation: Some(MAX_GENERATION), diff --git a/src/session/snapshot/save/recovery.rs b/src/session/snapshot/save/recovery.rs index 934dc8b48..6e3bd0113 100644 --- a/src/session/snapshot/save/recovery.rs +++ b/src/session/snapshot/save/recovery.rs @@ -1,6 +1,22 @@ -use super::payload::{log_payload_candidate, snapshot_without_history}; -use super::*; +use std::io::ErrorKind; + +use crate::durable_io; +use crate::durable_io::{AtomicWriteOptions, OverwriteMode, PermissionPolicy, SymlinkPolicy}; + +use super::super::types::SessionSnapshot; +use super::model::{SavePayloadTooLarge, SaveSnapshotOutcome, SaveSnapshotReport}; +use super::payload; +use super::payload::{ + PayloadCandidate, log_payload_candidate, payload_candidate, snapshot_without_history, +}; +use crate::session::options::SessionOptions; use crate::session::snapshot::generation::{ArtifactStamp, MarkerKind, cleared_by}; +use crate::time_utils::now_rfc3339; +use anyhow::{Context, Result, anyhow}; +use log::{debug, info, warn}; +use std::fs; +use std::path::Path; +use std::time::Instant; pub(super) fn save_recovery_snapshot( snapshot: &SessionSnapshot, @@ -18,13 +34,13 @@ pub(super) fn save_recovery_snapshot( // Through durable_io: it owns the temp-write/fsync/rename/parent-sync // sequence, keeps the artifact at 0600 like the rest of the session's // private data, and removes its temporary file on every failure path. - crate::durable_io::write_atomic( + durable_io::write_atomic( &recovery_path, &payload.bytes, - crate::durable_io::AtomicWriteOptions { - overwrite: crate::durable_io::OverwriteMode::Replace, - permissions: crate::durable_io::PermissionPolicy::FixedMode(0o600), - symlink: crate::durable_io::SymlinkPolicy::Reject, + AtomicWriteOptions { + overwrite: OverwriteMode::Replace, + permissions: PermissionPolicy::FixedMode(0o600), + symlink: SymlinkPolicy::Reject, sync_file: true, sync_parent: true, }, @@ -106,7 +122,7 @@ pub(super) fn remove_session_file_after_clear_marker(session_path: &Path) { "Removed session file {} after writing clear marker", session_path.display() ), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) if err.kind() == ErrorKind::NotFound => {} Err(err) => warn!( "Clear marker was written, but failed to remove stale session file {}: {}", session_path.display(), @@ -195,13 +211,13 @@ fn write_session_marker( Some(g) => super::super::generation::marker_record_bytes(kind, g, written)?, None => written.into_bytes(), }; - crate::durable_io::write_atomic( + durable_io::write_atomic( marker_path, &content, - crate::durable_io::AtomicWriteOptions { - overwrite: crate::durable_io::OverwriteMode::Replace, - permissions: crate::durable_io::PermissionPolicy::FixedMode(0o600), - symlink: crate::durable_io::SymlinkPolicy::Reject, + AtomicWriteOptions { + overwrite: OverwriteMode::Replace, + permissions: PermissionPolicy::FixedMode(0o600), + symlink: SymlinkPolicy::Reject, sync_file: true, sync_parent: true, }, diff --git a/src/session/snapshot/save/save_as.rs b/src/session/snapshot/save/save_as.rs index 9166b5b63..fac6b53b6 100644 --- a/src/session/snapshot/save/save_as.rs +++ b/src/session/snapshot/save/save_as.rs @@ -1,5 +1,23 @@ -use super::*; +use std::io::ErrorKind; + +use crate::session; + +use super::super::compression::{DEFAULT_MAX_EXPANDED_SESSION_BYTES, temp_path}; +use super::super::generation::{ArtifactSetView, UnreadablePolicy}; +use super::super::types::SessionSnapshot; +use super::model::{HistoryFallbackStrategy, SaveAsOverwrite, SaveSnapshotReport, log_near_limit}; +use super::payload::{PayloadCandidate, PayloadStamp, payload_within_limit}; +use crate::session::lock::{lock_exclusive, open_runtime_lock_file, unlock}; +use crate::session::options::SessionOptions; +use crate::time_utils::now_rfc3339; +use anyhow::{Context, Result, anyhow}; +use log::{info, warn}; +use std::fs; +use std::fs::OpenOptions; +use std::io::Write; use std::os::unix::fs::OpenOptionsExt; +use std::path::{Path, PathBuf}; +use std::time::Instant; mod transaction; @@ -23,7 +41,7 @@ pub(crate) fn save_snapshot_as_with_report( } let session_path = options.session_file_path(); - crate::session::validate_named_session_file_for_foreground(&session_path)?; + session::validate_named_session_file_for_foreground(&session_path)?; let initial_artifacts = collect_save_as_artifacts(options)?; ensure_save_as_overwrite_allowed(&initial_artifacts, overwrite, &session_path)?; @@ -125,7 +143,7 @@ pub(crate) fn save_snapshot_as_with_report( let commit_started = Instant::now(); let commit_result = (|| { - crate::session::validate_named_session_file_for_foreground(&session_path)?; + session::validate_named_session_file_for_foreground(&session_path)?; let lock_time_artifacts = collect_save_as_artifacts(options)?; ensure_save_as_overwrite_allowed(&lock_time_artifacts, overwrite, &session_path)?; let sidecars = matches!(overwrite, SaveAsOverwrite::ConfirmReplace) @@ -177,7 +195,7 @@ pub(crate) fn save_snapshot_as_requires_overwrite(options: &SessionOptions) -> R } let session_path = options.session_file_path(); - crate::session::validate_named_session_file_for_foreground(&session_path)?; + session::validate_named_session_file_for_foreground(&session_path)?; Ok(collect_save_as_artifacts(options)?.has_any()) } @@ -224,7 +242,7 @@ fn ensure_save_as_overwrite_allowed( fn artifact_path_exists(path: &Path) -> Result { match fs::symlink_metadata(path) { Ok(_) => Ok(true), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false), + Err(err) if err.kind() == ErrorKind::NotFound => Ok(false), Err(err) => Err(err) .with_context(|| format!("failed to inspect session artifact {}", path.display())), } @@ -232,7 +250,7 @@ fn artifact_path_exists(path: &Path) -> Result { fn save_as_non_lock_sidecar_paths(options: &SessionOptions) -> Result> { let session_path = options.session_file_path(); - let mut paths = crate::session::named_session_non_lock_artifact_paths(&session_path)?; + let mut paths = session::named_session_non_lock_artifact_paths(&session_path)?; paths.retain(|path| path != &session_path); Ok(paths) } diff --git a/src/session/snapshot/save/save_as/transaction.rs b/src/session/snapshot/save/save_as/transaction.rs index 297346456..3b5265b91 100644 --- a/src/session/snapshot/save/save_as/transaction.rs +++ b/src/session/snapshot/save/save_as/transaction.rs @@ -1,3 +1,5 @@ +use std::io; + use super::super::sync_session_parent_dir; use anyhow::{Context, Result}; use log::warn; @@ -32,7 +34,7 @@ fn commit_save_as_target_with( cleanup_sidecars: Cleanup, ) -> Result<()> where - Replace: FnOnce(&Path, &Path) -> std::io::Result<()>, + Replace: FnOnce(&Path, &Path) -> io::Result<()>, Sync: FnOnce() -> Result<()>, Cleanup: FnOnce(SaveAsSidecarQuarantine) -> Result<()>, { diff --git a/src/session/snapshot/save/save_as/transaction/tests.rs b/src/session/snapshot/save/save_as/transaction/tests.rs index 2b65ffd59..76acb9ef0 100644 --- a/src/session/snapshot/save/save_as/transaction/tests.rs +++ b/src/session/snapshot/save/save_as/transaction/tests.rs @@ -1,3 +1,7 @@ +use std::slice; + +use crate::test_temp; + use super::*; use anyhow::anyhow; use std::cell::{Cell, RefCell}; @@ -17,7 +21,7 @@ fn staging_directories(parent: &Path) -> Vec { #[test] fn primary_rename_failure_restores_quarantined_sidecars() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let session = temp.path().join("target.wayscriber-session"); let tmp = temp.path().join("target.tmp"); let backup = temp.path().join("target.wayscriber-session.bak"); @@ -31,7 +35,7 @@ fn primary_rename_failure_restores_quarantined_sidecars() { &tmp, &session, &[backup.clone(), recovery.clone()], - |_, _| Err(std::io::Error::other("injected primary rename failure")), + |_, _| Err(io::Error::other("injected primary rename failure")), || -> Result<()> { panic!("sync must not run before primary replacement") }, |_| -> Result<()> { panic!("cleanup must not run before primary replacement") }, ) @@ -53,7 +57,7 @@ fn primary_rename_failure_restores_quarantined_sidecars() { #[test] fn successful_commit_removes_quarantined_sidecars() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let session = temp.path().join("target.wayscriber-session"); let tmp = temp.path().join("target.tmp"); let backup = temp.path().join("target.wayscriber-session.bak"); @@ -78,7 +82,7 @@ fn successful_commit_removes_quarantined_sidecars() { #[test] fn primary_sync_failure_retains_quarantine_and_skips_cleanup() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let session = temp.path().join("target.wayscriber-session"); let tmp = temp.path().join("target.tmp"); let recovery = temp.path().join("target.wayscriber-session.recovery"); @@ -90,7 +94,7 @@ fn primary_sync_failure_retains_quarantine_and_skips_cleanup() { let err = commit_save_as_target_with( &tmp, &session, - std::slice::from_ref(&recovery), + slice::from_ref(&recovery), |source, target| fs::rename(source, target), || Err(anyhow!("injected primary sync failure")), |_| { @@ -123,7 +127,7 @@ fn primary_sync_failure_retains_quarantine_and_skips_cleanup() { #[test] fn postcommit_cleanup_failure_keeps_recovery_bytes_quarantined() { - let temp = crate::test_temp::tempdir().expect("tempdir"); + let temp = test_temp::tempdir().expect("tempdir"); let session = temp.path().join("target.wayscriber-session"); let tmp = temp.path().join("target.tmp"); let recovery = temp.path().join("target.wayscriber-session.recovery"); @@ -135,7 +139,7 @@ fn postcommit_cleanup_failure_keeps_recovery_bytes_quarantined() { commit_save_as_target_with( &tmp, &session, - std::slice::from_ref(&recovery), + slice::from_ref(&recovery), |source, target| fs::rename(source, target), || Ok(()), |quarantine| { diff --git a/src/session/snapshot/save/tests.rs b/src/session/snapshot/save/tests.rs index 6f7236184..8e09800d2 100644 --- a/src/session/snapshot/save/tests.rs +++ b/src/session/snapshot/save/tests.rs @@ -1,9 +1,11 @@ +use crate::test_temp; + use super::replace_session_file; use std::fs; #[test] fn failed_session_replace_leaves_the_previous_primary_intact() { - let temp = crate::test_temp::tempdir().expect("temporary directory"); + let temp = test_temp::tempdir().expect("temporary directory"); let primary = temp.path().join("session.json"); let missing_replacement = temp.path().join("missing.tmp"); fs::write(&primary, b"previous session").expect("previous primary"); diff --git a/src/session/snapshot/tests.rs b/src/session/snapshot/tests.rs index f1ad1b310..8db4898f7 100644 --- a/src/session/snapshot/tests.rs +++ b/src/session/snapshot/tests.rs @@ -1,3 +1,9 @@ +use crate::config::SessionConfig; +use crate::draw::{EraserKind, ShapeId, color}; +use crate::image_decode::EncodedImageFormat; +use crate::session::{lock, options}; +use crate::{draw, image_decode, session}; + use super::compression::{ExpandedSessionTooLarge, compress_bytes, is_gzip}; use super::load::{ LoadSnapshotOutcome, load_named_session_candidate, @@ -18,13 +24,17 @@ use crate::input::EraserMode; use crate::session::options::{CompressionMode, SessionOptions}; use crate::test_temp::tempdir; use crate::time_utils::now_rfc3339; -use std::path::Path; -use std::time::{Duration, SystemTime}; #[cfg(unix)] -use std::{ - ffi::CString, - os::unix::{ffi::OsStrExt, fs::symlink}, -}; +use std::ffi::CString; +use std::fs::{File, Permissions}; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +#[cfg(unix)] +use std::os::unix::fs::symlink; +use std::path::{Path, PathBuf}; +use std::sync::mpsc; +use std::time::{Duration, SystemTime}; +use std::{fs, io, thread}; fn sample_frame() -> Frame { let mut frame = Frame::new(); @@ -71,32 +81,23 @@ fn corrupt_primary_restores_backup_without_destroying_its_bytes() { } save_snapshot(&sample_snapshot(), &options).unwrap(); - let good_bytes = std::fs::read(options.session_file_path()).unwrap(); - std::fs::copy(options.session_file_path(), options.backup_file_path()).unwrap(); - std::fs::write(options.session_file_path(), b"{broken primary").unwrap(); + let good_bytes = fs::read(options.session_file_path()).unwrap(); + fs::copy(options.session_file_path(), options.backup_file_path()).unwrap(); + fs::write(options.session_file_path(), b"{broken primary").unwrap(); let restored = load_snapshot(&options) .unwrap() .expect("restore the backup"); assert!(restored.has_board_data()); - assert_eq!( - std::fs::read(options.backup_file_path()).unwrap(), - good_bytes - ); + assert_eq!(fs::read(options.backup_file_path()).unwrap(), good_bytes); if named { - assert_eq!( - std::fs::read(options.session_file_path()).unwrap(), - good_bytes - ); + assert_eq!(fs::read(options.session_file_path()).unwrap(), good_bytes); } else { assert!(!options.session_file_path().exists()); } save_snapshot(&restored, &options).unwrap(); - assert_eq!( - std::fs::read(options.backup_file_path()).unwrap(), - good_bytes - ); + assert_eq!(fs::read(options.backup_file_path()).unwrap(), good_bytes); } } @@ -107,20 +108,20 @@ fn corrupt_artifacts_have_separate_copies_and_only_marked_recovery_restores() { let mut options = SessionOptions::new(temp.path().to_path_buf(), "corrupt-recovery"); options.persist_transparent = true; save_snapshot(&sample_snapshot(), &options).unwrap(); - std::fs::rename(options.session_file_path(), options.recovery_file_path()).unwrap(); + fs::rename(options.session_file_path(), options.recovery_file_path()).unwrap(); set_modified( &options.recovery_file_path(), SystemTime::UNIX_EPOCH + Duration::from_secs(10), ); if recoverable { - std::fs::write( + fs::write( options.recovery_recoverable_marker_file_path(), b"recoverable", ) .unwrap(); } - std::fs::write(options.backup_file_path(), b"broken backup").unwrap(); - std::fs::write(options.session_file_path(), b"broken primary").unwrap(); + fs::write(options.backup_file_path(), b"broken backup").unwrap(); + fs::write(options.session_file_path(), b"broken primary").unwrap(); set_modified( &options.session_file_path(), SystemTime::UNIX_EPOCH + Duration::from_secs(20), @@ -146,7 +147,7 @@ fn corrupt_artifacts_have_separate_copies_and_only_marked_recovery_restores() { (options.backup_file_path(), b"broken backup".as_slice()), ] { assert_eq!( - std::fs::read(crate::session::append_path_suffix(&slot, ".corrupt-1")).unwrap(), + fs::read(session::append_path_suffix(&slot, ".corrupt-1")).unwrap(), expected ); } @@ -161,24 +162,21 @@ fn corrupt_copy_retention_skips_foreign_entries_and_named_loads_reuse_bytes() { let primary = temp.path().join("board.wayscriber-session"); options.set_named_file_target(primary.clone()); options.persist_transparent = true; - let copy = |n| crate::session::append_path_suffix(&primary, &format!(".corrupt-{n}")); - std::fs::create_dir(copy(1)).unwrap(); + let copy = |n| session::append_path_suffix(&primary, &format!(".corrupt-{n}")); + fs::create_dir(copy(1)).unwrap(); let foreign = temp.path().join("foreign"); - std::fs::write(&foreign, b"unrelated").unwrap(); + fs::write(&foreign, b"unrelated").unwrap(); symlink(&foreign, copy(2)).unwrap(); for n in 3..=7 { - std::fs::write(&primary, format!("broken {n}")).unwrap(); + fs::write(&primary, format!("broken {n}")).unwrap(); let outcome = load_snapshot_with_expanded_limit(&options, 64 * 1024).unwrap(); assert!(matches!( outcome, LoadSnapshotOutcome::EmptyAfterCorruption { .. } )); load_snapshot_with_expanded_limit(&options, 64 * 1024).unwrap(); - assert_eq!( - std::fs::read(copy(n)).unwrap(), - format!("broken {n}").as_bytes() - ); + assert_eq!(fs::read(copy(n)).unwrap(), format!("broken {n}").as_bytes()); assert!(!copy(n + 1).exists()); } assert!(!copy(3).exists()); @@ -186,12 +184,12 @@ fn corrupt_copy_retention_skips_foreign_entries_and_named_loads_reuse_bytes() { assert!(copy(5).exists() && copy(6).exists() && copy(7).exists()); assert!(copy(1).is_dir()); assert!( - std::fs::symlink_metadata(copy(2)) + fs::symlink_metadata(copy(2)) .unwrap() .file_type() .is_symlink() ); - assert_eq!(std::fs::read(foreign).unwrap(), b"unrelated"); + assert_eq!(fs::read(foreign).unwrap(), b"unrelated"); } pub(super) fn sample_tool_state() -> ToolStateSnapshot { @@ -204,7 +202,7 @@ pub(super) fn sample_tool_state() -> ToolStateSnapshot { }, current_thickness: 3.0, eraser_size: 12.0, - eraser_kind: crate::draw::EraserKind::Circle, + eraser_kind: EraserKind::Circle, eraser_mode: EraserMode::Brush, blur_style: Default::default(), recent_colors: Vec::new(), @@ -222,7 +220,7 @@ pub(super) fn sample_tool_state() -> ToolStateSnapshot { arrow_head_at_end: Some(false), arrow_style: None, arrow_label_enabled: Some(false), - polygon_sides: crate::draw::REGULAR_POLYGON_DEFAULT_SIDES, + polygon_sides: draw::REGULAR_POLYGON_DEFAULT_SIDES, board_previous_color: None, tool_settings: None, } @@ -276,7 +274,7 @@ fn sample_session_file() -> SessionFile { } fn write_contentless_session(path: &Path) { - std::fs::write( + fs::write( path, serde_json::to_vec_pretty(&contentless_session_file()).expect("contentless session json"), ) @@ -293,12 +291,12 @@ pub(super) fn make_fifo(path: &Path) { 0, "mkfifo {} failed: {}", path.display(), - std::io::Error::last_os_error() + io::Error::last_os_error() ); } pub(super) fn set_modified(path: &Path, modified: SystemTime) { - std::fs::File::options() + File::options() .write(true) .open(path) .expect("open file for timestamp update") @@ -355,7 +353,7 @@ fn a_legacy_session_payload_ignores_its_status_bar_visibility() { ), version = CURRENT_VERSION ); - std::fs::write(options.session_file_path(), payload).expect("write legacy session"); + fs::write(options.session_file_path(), payload).expect("write legacy session"); let outcome = load_snapshot_with_expanded_limit(&options, 64 * 1024).expect("legacy session should load"); @@ -388,7 +386,7 @@ fn a_saved_session_no_longer_records_status_bar_visibility() { save_snapshot(&snapshot, &options).expect("save_snapshot should succeed"); let written = - std::fs::read_to_string(options.session_file_path()).expect("saved session should be text"); + fs::read_to_string(options.session_file_path()).expect("saved session should be text"); assert!( !written.contains("show_status_bar"), "chrome visibility must not reach the session file: {written}" @@ -477,7 +475,7 @@ fn save_snapshot_respects_auto_compression_threshold() { plain.compression = CompressionMode::Auto; plain.auto_compress_threshold_bytes = u64::MAX; save_snapshot(&snapshot, &plain).expect("save_snapshot should succeed"); - let plain_bytes = std::fs::read(plain.session_file_path()).unwrap(); + let plain_bytes = fs::read(plain.session_file_path()).unwrap(); assert!( !is_gzip(&plain_bytes), "expected uncompressed session payload" @@ -488,7 +486,7 @@ fn save_snapshot_respects_auto_compression_threshold() { compressed.compression = CompressionMode::Auto; compressed.auto_compress_threshold_bytes = 1; save_snapshot(&snapshot, &compressed).expect("save_snapshot should succeed"); - let compressed_bytes = std::fs::read(compressed.session_file_path()).unwrap(); + let compressed_bytes = fs::read(compressed.session_file_path()).unwrap(); assert!(is_gzip(&compressed_bytes), "expected gzip payload"); } @@ -529,7 +527,7 @@ fn save_named_file_rejects_symlink_lock_without_truncating_target() { let mut options = SessionOptions::new(temp.path().join("configured"), "named-symlink-save"); options.persist_transparent = true; options.set_named_file_target(named_path); - std::fs::write(&lock_target, b"preserve me").expect("write lock target"); + fs::write(&lock_target, b"preserve me").expect("write lock target"); symlink(&lock_target, options.lock_file_path()).expect("create lock symlink"); let err = @@ -540,7 +538,7 @@ fn save_named_file_rejects_symlink_lock_without_truncating_target() { "{err:#}" ); assert_eq!( - std::fs::read(&lock_target).expect("read lock target"), + fs::read(&lock_target).expect("read lock target"), b"preserve me", "named save must not truncate a symlink lock target" ); @@ -584,8 +582,8 @@ fn load_named_file_rejects_symlink_lock_without_truncating_target() { options.persist_transparent = true; options.set_named_file_target(named_path); save_snapshot(&snapshot, &options).expect("save named snapshot"); - std::fs::remove_file(options.lock_file_path()).expect("remove regular lock"); - std::fs::write(&lock_target, b"preserve me").expect("write lock target"); + fs::remove_file(options.lock_file_path()).expect("remove regular lock"); + fs::write(&lock_target, b"preserve me").expect("write lock target"); symlink(&lock_target, options.lock_file_path()).expect("create lock symlink"); let err = load_snapshot(&options).expect_err("named load should reject symlink lock"); @@ -595,7 +593,7 @@ fn load_named_file_rejects_symlink_lock_without_truncating_target() { "{err:#}" ); assert_eq!( - std::fs::read(&lock_target).expect("read lock target"), + fs::read(&lock_target).expect("read lock target"), b"preserve me", "named load must not truncate a symlink lock target" ); @@ -688,9 +686,7 @@ fn load_snapshot_inner_reads_plain_payload_up_to_the_configured_file_size() { options.persist_transparent = true; options.compression = CompressionMode::Off; save_snapshot(&sample_snapshot(), &options).expect("save_snapshot should succeed"); - let file_size = std::fs::metadata(options.session_file_path()) - .unwrap() - .len(); + let file_size = fs::metadata(options.session_file_path()).unwrap().len(); options.max_file_size_bytes = file_size; let loaded = @@ -719,8 +715,7 @@ fn load_snapshot_inner_drops_all_history_nested_past_the_compound_limit() { save_snapshot(&snapshot, &options).expect("save_snapshot should succeed"); let path = options.session_file_path(); - let mut doc: serde_json::Value = - serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap(); + let mut doc: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap(); let stack = doc["boards"][0]["pages"][0]["undo_stack"] .as_array_mut() .expect("saved undo stack"); @@ -730,7 +725,7 @@ fn load_snapshot_inner_drops_all_history_nested_past_the_compound_limit() { nested = serde_json::json!({ "kind": "compound", "actions": [nested] }); } stack.push(nested); - std::fs::write(&path, serde_json::to_vec(&doc).unwrap()).unwrap(); + fs::write(&path, serde_json::to_vec(&doc).unwrap()).unwrap(); let loaded = load_snapshot_inner(&path, &options) .expect("load_snapshot_inner should succeed") @@ -771,7 +766,7 @@ fn load_named_primary_rejects_symlink_without_following_target() { let temp = tempdir().unwrap(); let target = temp.path().join("real-session.wayscriber-session"); let link = temp.path().join("linked-session.wayscriber-session"); - std::fs::write(&target, b"{not valid json").expect("write invalid target"); + fs::write(&target, b"{not valid json").expect("write invalid target"); symlink(&target, &link).expect("create primary symlink"); let mut options = SessionOptions::new(temp.path().to_path_buf(), "symlink-primary"); @@ -799,18 +794,14 @@ fn load_named_corrupt_primary_preserves_diagnostics_without_removing_selected_fi let temp = tempdir().unwrap(); let named_path = temp.path().join("corrupt.wayscriber-session"); - std::fs::write(&named_path, b"{not valid json").expect("write corrupt named primary"); + fs::write(&named_path, b"{not valid json").expect("write corrupt named primary"); let mut options = SessionOptions::new(temp.path().join("configured"), "named-corrupt"); options.persist_transparent = true; options.set_named_file_target(named_path.clone()); - std::fs::write(options.backup_file_path(), b"older diagnostic bytes") - .expect("seed legacy backup"); - std::fs::set_permissions( - options.backup_file_path(), - std::fs::Permissions::from_mode(0o644), - ) - .expect("make legacy backup public"); + fs::write(options.backup_file_path(), b"older diagnostic bytes").expect("seed legacy backup"); + fs::set_permissions(options.backup_file_path(), Permissions::from_mode(0o644)) + .expect("make legacy backup public"); let outcome = load_snapshot_with_expanded_limit(&options, 64 * 1024) .expect("corrupt named primary should be handled"); @@ -820,21 +811,21 @@ fn load_named_corrupt_primary_preserves_diagnostics_without_removing_selected_fi }; assert_eq!( corrupt_copy, - &crate::session::append_path_suffix(&named_path, ".corrupt-1"), + &session::append_path_suffix(&named_path, ".corrupt-1"), "the reported path is the one holding the preserved bytes" ); assert_eq!( - std::fs::read(&named_path).expect("named primary remains"), + fs::read(&named_path).expect("named primary remains"), b"{not valid json", "named corrupt backup must not remove the selected primary path" ); assert_eq!( - std::fs::read(corrupt_copy).expect("corrupt copy bytes"), + fs::read(corrupt_copy).expect("corrupt copy bytes"), b"{not valid json", "named corrupt primary should still be backed up for diagnostics" ); assert_eq!( - std::fs::metadata(corrupt_copy) + fs::metadata(corrupt_copy) .expect("corrupt copy metadata") .permissions() .mode() @@ -872,7 +863,7 @@ fn load_named_candidate_rejects_symlink_without_following_target() { let temp = tempdir().unwrap(); let target = temp.path().join("real.wayscriber-session"); let link = temp.path().join("linked.wayscriber-session"); - std::fs::write(&target, b"target bytes").expect("write target"); + fs::write(&target, b"target bytes").expect("write target"); symlink(&target, &link).expect("create symlink"); let mut options = SessionOptions::new(temp.path().join("configured"), "candidate-symlink"); @@ -883,7 +874,7 @@ fn load_named_candidate_rejects_symlink_without_following_target() { assert!(err.to_string().contains("symlink"), "{err:#}"); assert_eq!( - std::fs::read(&target).expect("target bytes"), + fs::read(&target).expect("target bytes"), b"target bytes", "symlink target must not be opened or mutated" ); @@ -899,28 +890,28 @@ fn load_named_candidate_waits_for_existing_lock_before_validating_primary() { options.persist_transparent = true; options.set_named_file_target(named_path.clone()); - let lock_file = std::fs::File::options() + let lock_file = File::options() .read(true) .write(true) .create_new(true) .open(options.lock_file_path()) .expect("create lock"); - crate::session::lock::lock_exclusive(&lock_file).expect("exclusive lock"); + lock::lock_exclusive(&lock_file).expect("exclusive lock"); - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let load_options = options.clone(); - let loader = std::thread::spawn(move || { + let loader = thread::spawn(move || { tx.send(load_named_session_candidate(&load_options)) .expect("send candidate result"); }); - std::thread::sleep(Duration::from_millis(50)); - std::fs::write( + thread::sleep(Duration::from_millis(50)); + fs::write( &named_path, serde_json::to_vec_pretty(&sample_session_file()).expect("session json"), ) .expect("write session while lock is held"); - crate::session::lock::unlock(&lock_file).expect("unlock"); + lock::unlock(&lock_file).expect("unlock"); let outcome = rx .recv_timeout(Duration::from_secs(2)) @@ -951,7 +942,7 @@ fn load_named_candidate_rejects_fifo_without_blocking_or_creating_artifacts() { fn load_named_candidate_corrupt_primary_does_not_create_sidecars() { let temp = tempdir().unwrap(); let named_path = temp.path().join("corrupt-open.wayscriber-session"); - std::fs::write(&named_path, b"{not valid json").expect("write corrupt candidate"); + fs::write(&named_path, b"{not valid json").expect("write corrupt candidate"); let mut options = SessionOptions::new(temp.path().join("configured"), "candidate-corrupt"); options.persist_transparent = true; @@ -961,7 +952,7 @@ fn load_named_candidate_corrupt_primary_does_not_create_sidecars() { assert!(format!("{err:#}").contains("failed to parse session json")); assert_eq!( - std::fs::read(&named_path).expect("candidate bytes remain"), + fs::read(&named_path).expect("candidate bytes remain"), b"{not valid json" ); assert_no_candidate_sidecars(&options); @@ -974,23 +965,23 @@ fn load_named_candidate_ignores_contentful_primary_older_than_clear_marker() { let mut options = SessionOptions::new(temp.path().join("configured"), "candidate-cleared"); options.persist_transparent = true; options.set_named_file_target(named_path.clone()); - std::fs::write( + fs::write( &named_path, serde_json::to_vec_pretty(&sample_session_file()).expect("primary session json"), ) .expect("write primary"); - std::fs::write(options.clear_marker_file_path(), b"cleared").expect("write clear marker"); + fs::write(options.clear_marker_file_path(), b"cleared").expect("write clear marker"); let primary_time = SystemTime::UNIX_EPOCH + Duration::from_secs(10); let clear_time = SystemTime::UNIX_EPOCH + Duration::from_secs(20); set_modified(&named_path, primary_time); set_modified(&options.clear_marker_file_path(), clear_time); - let primary_before = std::fs::read(&named_path).expect("primary bytes"); + let primary_before = fs::read(&named_path).expect("primary bytes"); let outcome = load_named_session_candidate(&options).expect("cleared primary should not load"); assert!(matches!(outcome, LoadSnapshotOutcome::Empty)); assert_eq!( - std::fs::read(&named_path).expect("primary remains"), + fs::read(&named_path).expect("primary remains"), primary_before ); assert!( @@ -1014,12 +1005,12 @@ fn load_named_candidate_ignores_symlink_clear_marker() { SessionOptions::new(temp.path().join("configured"), "candidate-symlink-clear"); options.persist_transparent = true; options.set_named_file_target(named_path.clone()); - std::fs::write( + fs::write( &named_path, serde_json::to_vec_pretty(&sample_session_file()).expect("primary session json"), ) .expect("write primary"); - std::fs::write(&clear_marker_target, b"target clear marker").expect("write target marker"); + fs::write(&clear_marker_target, b"target clear marker").expect("write target marker"); symlink(&clear_marker_target, options.clear_marker_file_path()) .expect("create clear marker symlink"); let primary_time = SystemTime::UNIX_EPOCH + Duration::from_secs(10); @@ -1031,7 +1022,7 @@ fn load_named_candidate_ignores_symlink_clear_marker() { assert_loaded_sample_snapshot(outcome); assert!( - std::fs::symlink_metadata(options.clear_marker_file_path()) + fs::symlink_metadata(options.clear_marker_file_path()) .expect("clear marker metadata") .file_type() .is_symlink(), @@ -1049,8 +1040,8 @@ fn load_named_candidate_ignores_corrupt_primary_older_than_clear_marker() { SessionOptions::new(temp.path().join("configured"), "candidate-cleared-corrupt"); options.persist_transparent = true; options.set_named_file_target(named_path.clone()); - std::fs::write(&named_path, b"{not valid json").expect("write corrupt primary"); - std::fs::write(options.clear_marker_file_path(), b"cleared").expect("write clear marker"); + fs::write(&named_path, b"{not valid json").expect("write corrupt primary"); + fs::write(options.clear_marker_file_path(), b"cleared").expect("write clear marker"); let primary_time = SystemTime::UNIX_EPOCH + Duration::from_secs(10); let clear_time = SystemTime::UNIX_EPOCH + Duration::from_secs(20); set_modified(&named_path, primary_time); @@ -1061,7 +1052,7 @@ fn load_named_candidate_ignores_corrupt_primary_older_than_clear_marker() { assert!(matches!(outcome, LoadSnapshotOutcome::Empty)); assert_eq!( - std::fs::read(&named_path).expect("corrupt primary remains"), + fs::read(&named_path).expect("corrupt primary remains"), b"{not valid json" ); assert!( @@ -1087,13 +1078,13 @@ fn load_named_candidate_ignores_symlink_backup_recoverable_marker() { ); options.persist_transparent = true; options.set_named_file_target(named_path.clone()); - std::fs::write( + fs::write( options.backup_file_path(), serde_json::to_vec_pretty(&sample_session_file()).expect("backup session json"), ) .expect("write backup"); write_contentless_session(&named_path); - std::fs::write(&marker_target, b"target backup marker").expect("write target marker"); + fs::write(&marker_target, b"target backup marker").expect("write target marker"); symlink(&marker_target, options.backup_recovery_marker_file_path()) .expect("create backup marker symlink"); let backup_time = SystemTime::UNIX_EPOCH + Duration::from_secs(10); @@ -1124,13 +1115,13 @@ fn load_named_candidate_ignores_symlink_recovery_recoverable_marker() { ); options.persist_transparent = true; options.set_named_file_target(named_path.clone()); - std::fs::write( + fs::write( options.recovery_file_path(), serde_json::to_vec_pretty(&sample_session_file()).expect("recovery session json"), ) .expect("write recovery"); write_contentless_session(&named_path); - std::fs::write(&marker_target, b"target recovery marker").expect("write target marker"); + fs::write(&marker_target, b"target recovery marker").expect("write target marker"); symlink( &marker_target, options.recovery_recoverable_marker_file_path(), @@ -1159,22 +1150,22 @@ fn load_named_candidate_falls_back_to_valid_primary_when_recovery_is_corrupt() { let mut options = SessionOptions::new(temp.path().join("configured"), "candidate-recovery"); options.persist_transparent = true; options.set_named_file_target(named_path.clone()); - std::fs::write( + fs::write( &named_path, serde_json::to_vec_pretty(&sample_session_file()).expect("primary session json"), ) .expect("write primary"); let recovery_bytes = b"{not valid recovery json"; - std::thread::sleep(Duration::from_millis(2)); - std::fs::write(options.recovery_file_path(), recovery_bytes).expect("write corrupt recovery"); + thread::sleep(Duration::from_millis(2)); + fs::write(options.recovery_file_path(), recovery_bytes).expect("write corrupt recovery"); let outcome = load_named_session_candidate(&options).expect("corrupt recovery should not block primary"); assert_loaded_sample_snapshot(outcome); assert_eq!( - std::fs::read(options.recovery_file_path()).expect("corrupt recovery remains"), + fs::read(options.recovery_file_path()).expect("corrupt recovery remains"), recovery_bytes ); assert!( @@ -1197,13 +1188,13 @@ fn load_named_candidate_reports_newer_oversized_recovery_without_loading_primary ); options.persist_transparent = true; options.set_named_file_target(named_path.clone()); - std::fs::write( + fs::write( &named_path, serde_json::to_vec_pretty(&sample_session_file()).expect("primary session json"), ) .expect("write primary"); let recovery_bytes = vec![b' '; 64]; - std::fs::write(options.recovery_file_path(), &recovery_bytes).expect("write recovery"); + fs::write(options.recovery_file_path(), &recovery_bytes).expect("write recovery"); let primary_time = SystemTime::UNIX_EPOCH + Duration::from_secs(10); let recovery_time = SystemTime::UNIX_EPOCH + Duration::from_secs(20); set_modified(&named_path, primary_time); @@ -1220,7 +1211,7 @@ fn load_named_candidate_reports_newer_oversized_recovery_without_loading_primary } if path == options.recovery_file_path() )); assert_eq!( - std::fs::read(options.recovery_file_path()).expect("recovery remains"), + fs::read(options.recovery_file_path()).expect("recovery remains"), recovery_bytes ); assert!( @@ -1241,15 +1232,15 @@ fn load_named_candidate_oversize_primary_does_not_create_sidecars() { options.persist_transparent = true; options.set_named_file_target(named_path.clone()); save_snapshot(&snapshot, &options).expect("save candidate"); - let original_bytes = std::fs::read(&named_path).expect("candidate bytes"); - std::fs::remove_file(options.lock_file_path()).expect("remove save-created lock"); + let original_bytes = fs::read(&named_path).expect("candidate bytes"); + fs::remove_file(options.lock_file_path()).expect("remove save-created lock"); options.max_file_size_bytes = 1; let err = load_named_session_candidate(&options).expect_err("oversize candidate should fail"); assert!(format!("{err:#}").contains("exceeds the configured limit")); assert_eq!( - std::fs::read(&named_path).expect("candidate bytes remain"), + fs::read(&named_path).expect("candidate bytes remain"), original_bytes ); assert_no_candidate_sidecars(&options); @@ -1261,7 +1252,7 @@ fn load_named_candidate_newer_version_does_not_create_sidecars() { let named_path = temp.path().join("newer.wayscriber-session"); let mut file = sample_session_file(); file.version = CURRENT_VERSION + 1; - std::fs::write( + fs::write( &named_path, serde_json::to_vec_pretty(&file).expect("newer session json"), ) @@ -1282,8 +1273,8 @@ fn load_named_candidate_newer_version_does_not_create_sidecars() { } /// Paths of `.vN-preserved-*` copies under `dir`, sorted for stable asserts. -fn preserved_copies_in(dir: &Path) -> Vec { - let mut copies: Vec<_> = std::fs::read_dir(dir) +fn preserved_copies_in(dir: &Path) -> Vec { + let mut copies: Vec<_> = fs::read_dir(dir) .expect("scan for preserved copies") .filter_map(|entry| entry.ok()) .map(|entry| entry.path()) @@ -1298,17 +1289,17 @@ fn preserved_copies_in(dir: &Path) -> Vec { } fn occupy_copy_preservation_candidates(base: &Path) { - std::fs::write(base, b"foreign preservation").expect("occupy base preservation name"); + fs::write(base, b"foreign preservation").expect("occupy base preservation name"); for attempt in 1..16 { - std::fs::write( - std::path::PathBuf::from(format!("{}-{attempt}", base.display())), + fs::write( + PathBuf::from(format!("{}-{attempt}", base.display())), b"foreign preservation", ) .expect("occupy numbered preservation name"); } } -fn occupy_move_preservation_candidates(base: &Path) -> Vec { +fn occupy_move_preservation_candidates(base: &Path) -> Vec { let mut paths = Vec::new(); for attempt in 0..16 { let suffix = if attempt == 0 { @@ -1316,8 +1307,8 @@ fn occupy_move_preservation_candidates(base: &Path) -> Vec { } else { format!("-moved-{attempt}") }; - let path = std::path::PathBuf::from(format!("{}{suffix}", base.display())); - std::fs::write(&path, b"previous preservation").expect("occupy moved preservation name"); + let path = PathBuf::from(format!("{}{suffix}", base.display())); + fs::write(&path, b"previous preservation").expect("occupy moved preservation name"); paths.push(path); } paths @@ -1333,9 +1324,9 @@ fn load_named_candidate_rejects_special_lock_without_creating_replacement() { options.persist_transparent = true; options.set_named_file_target(named_path.clone()); save_snapshot(&snapshot, &options).expect("save candidate"); - std::fs::remove_file(options.lock_file_path()).expect("remove save-created lock"); + fs::remove_file(options.lock_file_path()).expect("remove save-created lock"); make_fifo(&options.lock_file_path()); - let original_bytes = std::fs::read(&named_path).expect("candidate bytes"); + let original_bytes = fs::read(&named_path).expect("candidate bytes"); let err = load_named_session_candidate(&options).expect_err("fifo lock should fail"); @@ -1344,7 +1335,7 @@ fn load_named_candidate_rejects_special_lock_without_creating_replacement() { "{err:#}" ); assert_eq!( - std::fs::read(&named_path).expect("candidate bytes remain"), + fs::read(&named_path).expect("candidate bytes remain"), original_bytes ); assert!( @@ -1387,7 +1378,7 @@ fn load_snapshot_expansion_limit_leaves_primary_file_unchanged() { save_snapshot(&snapshot, &options).expect("save_snapshot should succeed"); let session_path = options.session_file_path(); - let original_bytes = std::fs::read(&session_path).expect("session bytes"); + let original_bytes = fs::read(&session_path).expect("session bytes"); let outcome = load_snapshot_with_expanded_limit(&options, 16) .expect("expanded-cap refusal should not be a load error"); @@ -1399,7 +1390,7 @@ fn load_snapshot_expansion_limit_leaves_primary_file_unchanged() { } )); assert_eq!( - std::fs::read(&session_path).expect("session should remain in place"), + fs::read(&session_path).expect("session should remain in place"), original_bytes ); assert!( @@ -1418,7 +1409,7 @@ fn load_snapshot_reports_successful_recovery_source() { save_snapshot(&snapshot, &options).expect("normal session should save"); let recovery_file = sample_session_file(); - std::fs::write( + fs::write( options.recovery_file_path(), serde_json::to_vec_pretty(&recovery_file).expect("recovery json"), ) @@ -1438,8 +1429,8 @@ fn load_snapshot_prefers_valid_recovery_when_primary_is_non_regular() { let mut options = SessionOptions::new(temp.path().to_path_buf(), "nonregular-recovery"); options.persist_transparent = true; - std::fs::create_dir(options.session_file_path()).expect("primary directory"); - std::fs::write( + fs::create_dir(options.session_file_path()).expect("primary directory"); + fs::write( options.recovery_file_path(), serde_json::to_vec_pretty(&sample_session_file()).expect("recovery json"), ) @@ -1468,7 +1459,7 @@ fn load_snapshot_falls_back_to_normal_when_recovery_is_corrupt() { save_snapshot(&snapshot, &options).expect("normal session should save"); let recovery_path = options.recovery_file_path(); - std::fs::write(&recovery_path, b"{not valid json").expect("recovery write"); + fs::write(&recovery_path, b"{not valid json").expect("recovery write"); let outcome = load_snapshot_with_expanded_limit(&options, 64 * 1024) .expect("corrupt recovery should fall back to normal session"); @@ -1478,7 +1469,7 @@ fn load_snapshot_falls_back_to_normal_when_recovery_is_corrupt() { "corrupt recovery should be moved out of the recovery path" ); assert!( - crate::session::append_path_suffix(&recovery_path, ".corrupt-1").exists(), + session::append_path_suffix(&recovery_path, ".corrupt-1").exists(), "corrupt recovery should be backed up for inspection" ); } @@ -1511,7 +1502,7 @@ fn load_snapshot_falls_back_to_normal_when_recovery_is_empty() { tool_state: None, }; let recovery_path = options.recovery_file_path(); - std::fs::write( + fs::write( &recovery_path, serde_json::to_vec_pretty(&empty_file).expect("empty recovery json"), ) @@ -1542,7 +1533,7 @@ fn load_snapshot_rejects_oversized_plain_recovery_before_falling_back() { save_snapshot(&snapshot, &options).expect("normal session should save"); let recovery_path = options.recovery_file_path(); - std::fs::write( + fs::write( &recovery_path, vec![b' '; usize::try_from(MAX_EXPANDED_SIZE + 1).expect("test size fits")], ) @@ -1571,7 +1562,7 @@ fn load_snapshot_restores_newer_contentful_backup_when_primary_has_no_board_data let mut options = SessionOptions::new(temp.path().to_path_buf(), "blank-primary"); options.persist_transparent = true; save_snapshot(&snapshot, &options).expect("normal session should save"); - std::fs::copy(options.session_file_path(), options.backup_file_path()) + fs::copy(options.session_file_path(), options.backup_file_path()) .expect("backup should be seeded"); write_contentless_session(&options.session_file_path()); @@ -1596,7 +1587,7 @@ fn load_snapshot_ignores_older_contentful_backup_when_primary_has_no_board_data( let mut options = SessionOptions::new(temp.path().to_path_buf(), "stale-backup"); options.persist_transparent = true; save_snapshot(&snapshot, &options).expect("normal session should save"); - std::fs::copy(options.session_file_path(), options.backup_file_path()) + fs::copy(options.session_file_path(), options.backup_file_path()) .expect("backup should be seeded"); write_contentless_session(&options.session_file_path()); set_modified(&options.backup_file_path(), older); @@ -1622,7 +1613,7 @@ fn load_snapshot_restores_contentful_backup_when_primary_is_missing() { let mut options = SessionOptions::new(temp.path().to_path_buf(), "backup-only"); options.persist_transparent = true; save_snapshot(&snapshot, &options).expect("normal session should save"); - std::fs::rename(options.session_file_path(), options.backup_file_path()) + fs::rename(options.session_file_path(), options.backup_file_path()) .expect("primary should be moved to backup"); let outcome = load_snapshot_with_expanded_limit(&options, 64 * 1024) @@ -1641,9 +1632,9 @@ fn save_contentless_snapshot_preserves_backup_when_primary_is_missing() { let mut options = SessionOptions::new(temp.path().to_path_buf(), "backup-preserved"); options.persist_transparent = true; save_snapshot(&snapshot, &options).expect("normal session should save"); - std::fs::rename(options.session_file_path(), options.backup_file_path()) + fs::rename(options.session_file_path(), options.backup_file_path()) .expect("primary should be moved to backup"); - let backup_before = std::fs::read(options.backup_file_path()).expect("backup bytes"); + let backup_before = fs::read(options.backup_file_path()).expect("backup bytes"); let contentless = SessionSnapshot { active_board_id: "transparent".to_string(), @@ -1657,7 +1648,7 @@ fn save_contentless_snapshot_preserves_backup_when_primary_is_missing() { "contentless primary should be saved" ); assert_eq!( - std::fs::read(options.backup_file_path()).expect("backup should remain"), + fs::read(options.backup_file_path()).expect("backup should remain"), backup_before, "contentless save must not delete the only backup when primary was missing" ); @@ -1681,9 +1672,9 @@ fn save_contentless_snapshot_preserves_recovery_when_primary_is_missing() { let mut options = SessionOptions::new(temp.path().to_path_buf(), "recovery-preserved"); options.persist_transparent = true; save_snapshot(&snapshot, &options).expect("normal session should save"); - std::fs::rename(options.session_file_path(), options.recovery_file_path()) + fs::rename(options.session_file_path(), options.recovery_file_path()) .expect("primary should be moved to recovery"); - let recovery_before = std::fs::read(options.recovery_file_path()).expect("recovery bytes"); + let recovery_before = fs::read(options.recovery_file_path()).expect("recovery bytes"); let contentless = SessionSnapshot { active_board_id: "transparent".to_string(), @@ -1697,7 +1688,7 @@ fn save_contentless_snapshot_preserves_recovery_when_primary_is_missing() { "contentless primary should be saved" ); assert_eq!( - std::fs::read(options.recovery_file_path()).expect("recovery should remain"), + fs::read(options.recovery_file_path()).expect("recovery should remain"), recovery_before, "contentless save must not delete the only recovery artifact when primary was missing" ); @@ -1714,8 +1705,7 @@ fn save_contentless_snapshot_preserves_recovery_when_primary_is_missing() { save_snapshot(&contentless, &options).expect("second contentless session should save"); assert_eq!( - std::fs::read(options.recovery_file_path()) - .expect("recovery should remain after second save"), + fs::read(options.recovery_file_path()).expect("recovery should remain after second save"), recovery_before, "a later non-clear contentless save must not remove the recoverable recovery artifact" ); @@ -1730,9 +1720,9 @@ fn clear_empty_snapshot_removes_backup_when_primary_is_missing() { options.persist_transparent = true; options.restore_tool_state = false; save_snapshot(&snapshot, &options).expect("normal session should save"); - std::fs::rename(options.session_file_path(), options.backup_file_path()) + fs::rename(options.session_file_path(), options.backup_file_path()) .expect("primary should be moved to backup"); - std::fs::write(options.backup_recovery_marker_file_path(), b"recoverable") + fs::write(options.backup_recovery_marker_file_path(), b"recoverable") .expect("backup recovery marker"); let cleared = SessionSnapshot { @@ -1770,7 +1760,7 @@ fn clear_empty_snapshot_removes_recovery_when_primary_is_missing() { options.persist_transparent = true; options.restore_tool_state = false; save_snapshot(&snapshot, &options).expect("normal session should save"); - std::fs::rename(options.session_file_path(), options.recovery_file_path()) + fs::rename(options.session_file_path(), options.recovery_file_path()) .expect("primary should be moved to recovery"); let cleared = SessionSnapshot { @@ -1805,9 +1795,9 @@ fn load_snapshot_ignores_backup_older_than_clear_marker() { let mut options = SessionOptions::new(temp.path().to_path_buf(), "marker-backup"); options.persist_transparent = true; save_snapshot(&snapshot, &options).expect("normal session should save"); - std::fs::rename(options.session_file_path(), options.backup_file_path()) + fs::rename(options.session_file_path(), options.backup_file_path()) .expect("primary should be moved to backup"); - std::fs::write(options.clear_marker_file_path(), b"cleared").expect("clear marker"); + fs::write(options.clear_marker_file_path(), b"cleared").expect("clear marker"); set_modified(&options.backup_file_path(), older); set_modified(&options.clear_marker_file_path(), newer); @@ -1828,7 +1818,7 @@ fn load_snapshot_ignores_primary_older_than_clear_marker() { let mut options = SessionOptions::new(temp.path().to_path_buf(), "marker-primary"); options.persist_transparent = true; save_snapshot(&snapshot, &options).expect("normal session should save"); - std::fs::write(options.clear_marker_file_path(), b"cleared").expect("clear marker"); + fs::write(options.clear_marker_file_path(), b"cleared").expect("clear marker"); set_modified(&options.session_file_path(), older); set_modified(&options.clear_marker_file_path(), newer); @@ -1854,10 +1844,10 @@ fn load_snapshot_preserves_backup_when_suppressed_primary_is_corrupt() { let primary_path = options.session_file_path(); let backup_path = options.backup_file_path(); let clear_marker_path = options.clear_marker_file_path(); - let backup_bytes = std::fs::read(&primary_path).expect("saved primary bytes"); - std::fs::write(&backup_path, &backup_bytes).expect("backup write"); - std::fs::write(&primary_path, b"{not valid json").expect("corrupt primary write"); - std::fs::write(&clear_marker_path, b"cleared").expect("clear marker"); + let backup_bytes = fs::read(&primary_path).expect("saved primary bytes"); + fs::write(&backup_path, &backup_bytes).expect("backup write"); + fs::write(&primary_path, b"{not valid json").expect("corrupt primary write"); + fs::write(&clear_marker_path, b"cleared").expect("clear marker"); set_modified(&primary_path, primary_time); set_modified(&clear_marker_path, marker_time); set_modified(&backup_path, backup_time); @@ -1869,7 +1859,7 @@ fn load_snapshot_preserves_backup_when_suppressed_primary_is_corrupt() { }; assert_sample_snapshot(&snapshot); assert_eq!( - std::fs::read(&backup_path).expect("backup should remain readable"), + fs::read(&backup_path).expect("backup should remain readable"), backup_bytes, "suppressed corrupt primary probe must not overwrite the valid backup" ); @@ -1889,9 +1879,9 @@ fn load_snapshot_ignores_recovery_older_than_clear_marker() { let mut options = SessionOptions::new(temp.path().to_path_buf(), "marker-recovery"); options.persist_transparent = true; save_snapshot(&snapshot, &options).expect("normal session should save"); - std::fs::rename(options.session_file_path(), options.recovery_file_path()) + fs::rename(options.session_file_path(), options.recovery_file_path()) .expect("primary should be moved to recovery"); - std::fs::write(options.clear_marker_file_path(), b"cleared").expect("clear marker"); + fs::write(options.clear_marker_file_path(), b"cleared").expect("clear marker"); set_modified(&options.recovery_file_path(), older); set_modified(&options.clear_marker_file_path(), newer); @@ -1912,9 +1902,9 @@ fn load_snapshot_restores_backup_newer_than_clear_marker() { let mut options = SessionOptions::new(temp.path().to_path_buf(), "marker-newer-backup"); options.persist_transparent = true; save_snapshot(&snapshot, &options).expect("normal session should save"); - std::fs::rename(options.session_file_path(), options.backup_file_path()) + fs::rename(options.session_file_path(), options.backup_file_path()) .expect("primary should be moved to backup"); - std::fs::write(options.clear_marker_file_path(), b"cleared").expect("clear marker"); + fs::write(options.clear_marker_file_path(), b"cleared").expect("clear marker"); set_modified(&options.clear_marker_file_path(), older); set_modified(&options.backup_file_path(), newer); @@ -1937,7 +1927,7 @@ fn save_contentless_tool_state_snapshot_marks_clear_boundary_before_backup_rotat options.persist_transparent = true; options.restore_tool_state = true; save_snapshot(&snapshot, &options).expect("normal session should save"); - let stale_backup_bytes = std::fs::read(options.session_file_path()).expect("primary bytes"); + let stale_backup_bytes = fs::read(options.session_file_path()).expect("primary bytes"); let contentless = SessionSnapshot { active_board_id: "transparent".to_string(), @@ -1965,8 +1955,8 @@ fn save_contentless_tool_state_snapshot_marks_clear_boundary_before_backup_rotat "clear marker must not suppress the freshly saved tool state" ); - std::fs::remove_file(options.session_file_path()).expect("remove primary to simulate crash"); - std::fs::write(options.backup_file_path(), stale_backup_bytes).expect("stale backup write"); + fs::remove_file(options.session_file_path()).expect("remove primary to simulate crash"); + fs::write(options.backup_file_path(), stale_backup_bytes).expect("stale backup write"); set_modified(&options.backup_file_path(), older); set_modified(&options.clear_marker_file_path(), newer); @@ -1987,9 +1977,9 @@ fn contentful_save_after_clear_marker_removes_stale_backup_before_marker() { let mut options = SessionOptions::new(temp.path().to_path_buf(), "contentful-after-clear"); options.persist_transparent = true; save_snapshot(&snapshot, &options).expect("normal session should save"); - std::fs::rename(options.session_file_path(), options.backup_file_path()) + fs::rename(options.session_file_path(), options.backup_file_path()) .expect("primary should be moved to backup"); - std::fs::write(options.clear_marker_file_path(), b"cleared").expect("clear marker"); + fs::write(options.clear_marker_file_path(), b"cleared").expect("clear marker"); set_modified(&options.backup_file_path(), older); set_modified(&options.clear_marker_file_path(), newer); @@ -2018,7 +2008,7 @@ fn contentless_save_without_loaded_board_data_preserves_rotated_backup() { options.persist_transparent = true; options.restore_tool_state = true; save_snapshot(&snapshot, &options).expect("normal session should save"); - let primary_before = std::fs::read(options.session_file_path()).expect("primary bytes"); + let primary_before = fs::read(options.session_file_path()).expect("primary bytes"); let contentless = SessionSnapshot { active_board_id: "transparent".to_string(), @@ -2033,7 +2023,7 @@ fn contentless_save_without_loaded_board_data_preserves_rotated_backup() { "contentless primary should be saved" ); assert_eq!( - std::fs::read(options.backup_file_path()).expect("backup should remain"), + fs::read(options.backup_file_path()).expect("backup should remain"), primary_before, "contentless save without loaded board data must preserve the rotated recoverable primary" ); @@ -2056,7 +2046,7 @@ fn contentless_save_without_loaded_board_data_preserves_rotated_backup() { save_snapshot_with_report_and_clear_boundary(&contentless, &options, false) .expect("second dirty tool-state-only session should save"); assert_eq!( - std::fs::read(options.backup_file_path()).expect("backup should remain after second save"), + fs::read(options.backup_file_path()).expect("backup should remain after second save"), primary_before, "a later non-clear contentless save must not replace the recoverable backup with a blank primary" ); @@ -2072,7 +2062,7 @@ fn contentless_save_twice_without_loaded_board_data_preserves_preserved_recovery let preserved_recovery = options .recovery_file_path() .with_extension("recovery.empty"); - std::fs::write(&preserved_recovery, b"unloadable recovery").expect("preserved recovery write"); + fs::write(&preserved_recovery, b"unloadable recovery").expect("preserved recovery write"); let contentless = SessionSnapshot { active_board_id: "transparent".to_string(), @@ -2181,7 +2171,7 @@ fn load_snapshot_inner_skips_newer_versions() { tool_state: None, }; let bytes = serde_json::to_vec_pretty(&file).unwrap(); - std::fs::write(&session_path, bytes).unwrap(); + fs::write(&session_path, bytes).unwrap(); let options = SessionOptions::new(temp.path().to_path_buf(), "skip"); let loaded = @@ -2205,7 +2195,7 @@ fn newer_version_session_survives_downgrade_saves() { let mut file = sample_session_file(); file.version = CURRENT_VERSION + 1; let newer_bytes = serde_json::to_vec_pretty(&file).expect("newer session json"); - std::fs::write(&session_path, &newer_bytes).expect("write newer session"); + fs::write(&session_path, &newer_bytes).expect("write newer session"); let outcome = load_snapshot_with_expanded_limit(&options, 64 * 1024) .expect("newer-version session load is handled"); @@ -2214,12 +2204,12 @@ fn newer_version_session_survives_downgrade_saves() { let copies = preserved_copies_in(temp.path()); assert_eq!(copies.len(), 1, "one preserved copy after first load"); assert_eq!( - std::fs::read(&copies[0]).expect("preserved copy exists"), + fs::read(&copies[0]).expect("preserved copy exists"), newer_bytes, "runtime load must preserve the exact loaded bytes" ); assert_eq!( - std::fs::metadata(&copies[0]) + fs::metadata(&copies[0]) .expect("preserved copy metadata") .permissions() .mode() @@ -2232,7 +2222,7 @@ fn newer_version_session_survives_downgrade_saves() { save_snapshot(&sample_snapshot(), &options).expect("second post-downgrade save"); assert_eq!( - std::fs::read(&copies[0]).expect("preserved copy survives rotation"), + fs::read(&copies[0]).expect("preserved copy survives rotation"), newer_bytes, "save rotation must not touch the preserved newer-version copy" ); @@ -2244,7 +2234,7 @@ fn newer_version_session_survives_downgrade_saves() { changed.version = CURRENT_VERSION + 1; changed.active_board_id = Some("whiteboard".to_string()); let changed_bytes = serde_json::to_vec_pretty(&changed).expect("changed newer session json"); - std::fs::write(&session_path, &changed_bytes).expect("write changed newer session"); + fs::write(&session_path, &changed_bytes).expect("write changed newer session"); let outcome = load_snapshot_with_expanded_limit(&options, 64 * 1024) .expect("repeated newer-version load is handled"); assert!(matches!(outcome, LoadSnapshotOutcome::Empty)); @@ -2252,7 +2242,7 @@ fn newer_version_session_survives_downgrade_saves() { assert_eq!(copies.len(), 2, "each distinct newer session is preserved"); let mut contents: Vec> = copies .iter() - .map(|path| std::fs::read(path).expect("read preserved copy")) + .map(|path| fs::read(path).expect("read preserved copy")) .collect(); contents.sort(); let mut expected = vec![newer_bytes, changed_bytes]; @@ -2272,7 +2262,7 @@ fn compressed_newer_version_session_is_preserved_byte_for_byte() { let json = serde_json::to_vec_pretty(&file).expect("newer session json"); let encoded = compress_bytes(&json).expect("compress newer session"); assert!(is_gzip(&encoded)); - std::fs::write(&session_path, &encoded).expect("write compressed newer session"); + fs::write(&session_path, &encoded).expect("write compressed newer session"); let outcome = load_snapshot_with_expanded_limit(&options, 64 * 1024) .expect("compressed newer-version session load is handled"); @@ -2281,7 +2271,7 @@ fn compressed_newer_version_session_is_preserved_byte_for_byte() { let copies = preserved_copies_in(temp.path()); assert_eq!(copies.len(), 1); assert_eq!( - std::fs::read(&copies[0]).expect("read preserved compressed session"), + fs::read(&copies[0]).expect("read preserved compressed session"), encoded, "preservation must retain the original encoded bytes" ); @@ -2297,11 +2287,11 @@ fn exhausted_copy_names_move_newer_primary_without_replacing_anything() { file.version = CURRENT_VERSION + 1; let newer_bytes = serde_json::to_vec_pretty(&file).expect("newer session json"); - std::fs::write(&session_path, &newer_bytes).expect("write newer session"); + fs::write(&session_path, &newer_bytes).expect("write newer session"); load_snapshot_with_expanded_limit(&options, 64 * 1024).expect("establish base copy"); let base = preserved_copies_in(temp.path()).remove(0); occupy_copy_preservation_candidates(&base); - std::fs::write(&session_path, &newer_bytes).expect("restore newer primary"); + fs::write(&session_path, &newer_bytes).expect("restore newer primary"); let outcome = load_snapshot_with_expanded_limit(&options, 64 * 1024) .expect("free moved candidate should preserve the primary"); @@ -2310,9 +2300,9 @@ fn exhausted_copy_names_move_newer_primary_without_replacing_anything() { !session_path.exists(), "fallback must move the exposed primary" ); - let moved = std::path::PathBuf::from(format!("{}-moved", base.display())); + let moved = PathBuf::from(format!("{}-moved", base.display())); assert_eq!( - std::fs::read(moved).expect("read moved preservation"), + fs::read(moved).expect("read moved preservation"), newer_bytes ); } @@ -2327,18 +2317,18 @@ fn preservation_exhaustion_fails_closed_without_backing_up_or_replacing_files() file.version = CURRENT_VERSION + 1; let newer_bytes = serde_json::to_vec_pretty(&file).expect("newer session json"); - std::fs::write(&session_path, &newer_bytes).expect("write newer session"); + fs::write(&session_path, &newer_bytes).expect("write newer session"); load_snapshot_with_expanded_limit(&options, 64 * 1024).expect("establish base copy"); let base = preserved_copies_in(temp.path()).remove(0); occupy_copy_preservation_candidates(&base); let moved_candidates = occupy_move_preservation_candidates(&base); - std::fs::write(&session_path, &newer_bytes).expect("restore newer primary"); + fs::write(&session_path, &newer_bytes).expect("restore newer primary"); let err = load_snapshot_with_expanded_limit(&options, 64 * 1024) .expect_err("no durable preservation target must abort the load"); assert!(format!("{err:#}").contains("could not durably preserve")); assert_eq!( - std::fs::read(&session_path).expect("primary remains untouched"), + fs::read(&session_path).expect("primary remains untouched"), newer_bytes ); assert!( @@ -2347,7 +2337,7 @@ fn preservation_exhaustion_fails_closed_without_backing_up_or_replacing_files() ); for path in moved_candidates { assert_eq!( - std::fs::read(path).expect("existing moved candidate remains"), + fs::read(path).expect("existing moved candidate remains"), b"previous preservation" ); } @@ -2366,21 +2356,21 @@ fn a_mismatched_preserved_file_is_stepped_over_not_trusted() { let mut file = sample_session_file(); file.version = CURRENT_VERSION + 1; let newer_bytes = serde_json::to_vec_pretty(&file).expect("newer session json"); - std::fs::write(&session_path, &newer_bytes).expect("write newer session"); + fs::write(&session_path, &newer_bytes).expect("write newer session"); // First load establishes the content-addressed name, which is then // clobbered with content that is not the session. load_snapshot_with_expanded_limit(&options, 64 * 1024).expect("first load"); let copies = preserved_copies_in(temp.path()); assert_eq!(copies.len(), 1); - std::fs::write(&copies[0], b"not the session").expect("clobber preserved copy"); + fs::write(&copies[0], b"not the session").expect("clobber preserved copy"); // Re-write the primary (the first load left it in place) and load again. - std::fs::write(&session_path, &newer_bytes).expect("rewrite newer session"); + fs::write(&session_path, &newer_bytes).expect("rewrite newer session"); load_snapshot_with_expanded_limit(&options, 64 * 1024).expect("second load"); assert_eq!( - std::fs::read(&copies[0]).expect("clobbered file remains"), + fs::read(&copies[0]).expect("clobbered file remains"), b"not the session", "a non-matching file at the preserved name must not be overwritten" ); @@ -2388,7 +2378,7 @@ fn a_mismatched_preserved_file_is_stepped_over_not_trusted() { assert!( copies .iter() - .any(|path| std::fs::read(path).is_ok_and(|bytes| bytes == newer_bytes)), + .any(|path| fs::read(path).is_ok_and(|bytes| bytes == newer_bytes)), "the real bytes must be preserved under some name" ); } @@ -2409,7 +2399,7 @@ fn forward_incompatible_newer_session_is_preserved_not_treated_as_corrupt() { CURRENT_VERSION + 1 ) .into_bytes(); - std::fs::write(&session_path, &newer_bytes).expect("write incompatible newer session"); + fs::write(&session_path, &newer_bytes).expect("write incompatible newer session"); let outcome = load_snapshot_with_expanded_limit(&options, 64 * 1024) .expect("incompatible newer-version load is handled"); @@ -2422,7 +2412,7 @@ fn forward_incompatible_newer_session_is_preserved_not_treated_as_corrupt() { let copies = preserved_copies_in(temp.path()); assert_eq!(copies.len(), 1, "the incompatible session is preserved"); assert_eq!( - std::fs::read(&copies[0]).expect("preserved copy exists"), + fs::read(&copies[0]).expect("preserved copy exists"), newer_bytes ); } @@ -2601,7 +2591,7 @@ fn save_snapshot_serializes_compound_undo_history() { save_snapshot(&snapshot, &options).expect("save_snapshot should serialize compound history"); - let raw = std::fs::read_to_string(options.session_file_path()).expect("read session json"); + let raw = fs::read_to_string(options.session_file_path()).expect("read session json"); assert!(raw.contains("\"kind\": \"compound\"")); assert!(raw.contains("\"actions\"")); @@ -2612,7 +2602,7 @@ fn save_snapshot_serializes_compound_undo_history() { assert_eq!(page.undo_stack_len(), 1); } -fn snapshot_for_shape(frame: &Frame, shape_id: crate::draw::ShapeId) -> ShapeSnapshot { +fn snapshot_for_shape(frame: &Frame, shape_id: ShapeId) -> ShapeSnapshot { let drawn = frame.shape(shape_id).expect("shape exists"); ShapeSnapshot { shape: drawn.shape.clone(), @@ -2620,7 +2610,7 @@ fn snapshot_for_shape(frame: &Frame, shape_id: crate::draw::ShapeId) -> ShapeSna } } -fn translate_line(frame: &mut Frame, shape_id: crate::draw::ShapeId, dx: i32, dy: i32) { +fn translate_line(frame: &mut Frame, shape_id: ShapeId, dx: i32, dy: i32) { let shape = frame.shape_mut(shape_id).expect("shape exists"); if let Shape::Line { x1, y1, x2, y2, .. } = &mut shape.shape { *x1 += dx; @@ -2670,7 +2660,7 @@ fn load_snapshot_inner_migrates_legacy_frame_to_pages() { tool_state: None, }; let bytes = serde_json::to_vec_pretty(&file).unwrap(); - std::fs::write(&session_path, bytes).unwrap(); + fs::write(&session_path, bytes).unwrap(); let options = SessionOptions::new(temp.path().to_path_buf(), "legacy"); let loaded = load_snapshot_inner(&session_path, &options) @@ -2716,7 +2706,7 @@ fn load_snapshot_inner_falls_back_when_active_board_is_missing() { tool_state: None, }; let bytes = serde_json::to_vec_pretty(&file).unwrap(); - std::fs::write(&session_path, bytes).unwrap(); + fs::write(&session_path, bytes).unwrap(); let options = SessionOptions::new(temp.path().to_path_buf(), "missing-active-board"); let loaded = load_snapshot_inner(&session_path, &options) @@ -2750,7 +2740,7 @@ fn load_snapshot_inner_normalizes_empty_legacy_page_lists() { tool_state: None, }; let bytes = serde_json::to_vec_pretty(&file).unwrap(); - std::fs::write(&session_path, bytes).unwrap(); + fs::write(&session_path, bytes).unwrap(); let options = SessionOptions::new(temp.path().to_path_buf(), "empty-legacy-pages"); let loaded = load_snapshot_inner(&session_path, &options) @@ -2782,15 +2772,12 @@ fn session_artifacts_are_private_to_their_owner() { // Simulate a primary written by a release that still used the default // 0644 mode. A second save rotates it into the backup slot, which must // tighten the legacy permissions rather than preserve them. - std::fs::set_permissions( - options.session_file_path(), - std::fs::Permissions::from_mode(0o644), - ) - .expect("make legacy primary public"); + fs::set_permissions(options.session_file_path(), Permissions::from_mode(0o644)) + .expect("make legacy primary public"); save_snapshot(&sample_snapshot(), &options).expect("second save"); for path in [options.session_file_path(), options.backup_file_path()] { - let mode = std::fs::metadata(&path) + let mode = fs::metadata(&path) .unwrap_or_else(|err| panic!("stat {}: {err}", path.display())) .permissions() .mode() @@ -2817,7 +2804,7 @@ fn a_failed_save_leaves_no_temporary_file_behind() { let _ = save_snapshot(&sample_snapshot(), &options); - let strays: Vec<_> = std::fs::read_dir(temp.path()) + let strays: Vec<_> = fs::read_dir(temp.path()) .expect("scan session dir") .filter_map(|entry| entry.ok()) .map(|entry| entry.file_name().to_string_lossy().into_owned()) @@ -2858,12 +2845,7 @@ fn maximum_admitted_image_and_create_history_fit_actual_default_session_budget() bytes.push(random_byte()); } assert!(limits.allows_bytes(bytes.len())); - crate::image_decode::decode_rgba( - crate::image_decode::EncodedImageFormat::Png, - &bytes, - limits.into(), - ) - .unwrap(); + image_decode::decode_rgba(EncodedImageFormat::Png, &bytes, limits.into()).unwrap(); let mut snapshot = sample_snapshot(); let mut frame = Frame::new(); @@ -2887,8 +2869,8 @@ fn maximum_admitted_image_and_create_history_fit_actual_default_session_budget() ); snapshot.boards[0].pages.pages[0] = frame; let temp = tempdir().unwrap(); - let options = crate::session::options::options_from_config_for_named_file( - &crate::config::SessionConfig::default(), + let options = options::options_from_config_for_named_file( + &SessionConfig::default(), temp.path().join("maximum-image.wayscriber-session"), Some("test"), ); @@ -2932,14 +2914,14 @@ fn offline_tool_clear_reports_corruption_instead_of_silently_rewriting_restored_ } let snapshot = sample_snapshot(); save_snapshot(&snapshot, &options).unwrap(); - std::fs::rename(options.session_file_path(), options.backup_file_path()).unwrap(); - let backup = std::fs::read(options.backup_file_path()).unwrap(); - std::fs::write(options.session_file_path(), b"unreadable primary").unwrap(); - let error = crate::session::clear_tool_state(&options).unwrap_err(); + fs::rename(options.session_file_path(), options.backup_file_path()).unwrap(); + let backup = fs::read(options.backup_file_path()).unwrap(); + fs::write(options.session_file_path(), b"unreadable primary").unwrap(); + let error = session::clear_tool_state(&options).unwrap_err(); assert!(format!("{error:#}").contains("diagnostic bytes were preserved")); - assert_eq!(std::fs::read(options.backup_file_path()).unwrap(), backup); + assert_eq!(fs::read(options.backup_file_path()).unwrap(), backup); assert_eq!( - std::fs::read(crate::session::append_path_suffix( + fs::read(session::append_path_suffix( &options.session_file_path(), ".corrupt-1" )) @@ -2948,7 +2930,7 @@ fn offline_tool_clear_reports_corruption_instead_of_silently_rewriting_restored_ ); if named { assert_eq!( - std::fs::read(options.session_file_path()).unwrap(), + fs::read(options.session_file_path()).unwrap(), b"unreadable primary" ); } else { @@ -2968,7 +2950,7 @@ fn format_eight_preserves_fractional_ink_and_loads_legacy_integer_history() { let frame = &mut snapshot.boards[0].pages.pages[0]; let shape = Shape::Freehand { points: vec![(300.25, 300.5), (301.75, 301.125)], - color: crate::draw::color::BLACK, + color: color::BLACK, thick: 2.0, }; let id = frame.add_shape(shape.clone()); @@ -2980,7 +2962,7 @@ fn format_eight_preserves_fractional_ink_and_loads_legacy_integer_history() { 100, ); save_snapshot(&snapshot, &options).unwrap(); - let encoded = std::fs::read(options.session_file_path()).unwrap(); + let encoded = fs::read(options.session_file_path()).unwrap(); let mut value: serde_json::Value = serde_json::from_slice(&encoded).unwrap(); assert_eq!(value["version"], 8); let loaded = load_snapshot_inner(&options.session_file_path(), &options) @@ -3021,13 +3003,13 @@ fn format_eight_preserves_fractional_ink_and_loads_legacy_integer_history() { value["version"] = serde_json::json!(7); integer_coordinates(&mut value); let legacy = serde_json::to_vec(&value).unwrap(); - std::fs::write(options.session_file_path(), &legacy).unwrap(); + fs::write(options.session_file_path(), &legacy).unwrap(); let loaded = load_snapshot_inner(&options.session_file_path(), &options) .unwrap() .unwrap(); assert_eq!(loaded.version, 7); assert_eq!( - std::fs::read(options.session_file_path()).unwrap(), + fs::read(options.session_file_path()).unwrap(), legacy, "loading does not migrate on disk" ); @@ -3040,6 +3022,6 @@ fn format_eight_preserves_fractional_ink_and_loads_legacy_integer_history() { assert!(restored.redo_last().is_some()); save_snapshot(&loaded.snapshot, &options).unwrap(); let value: serde_json::Value = - serde_json::from_slice(&std::fs::read(options.session_file_path()).unwrap()).unwrap(); + serde_json::from_slice(&fs::read(options.session_file_path()).unwrap()).unwrap(); assert_eq!(value["version"], 8); } diff --git a/src/session/storage/clear.rs b/src/session/storage/clear.rs index aeb5d6d18..57ec8ed21 100644 --- a/src/session/storage/clear.rs +++ b/src/session/storage/clear.rs @@ -1,3 +1,5 @@ +use crate::session; +use crate::session::artifacts; use crate::session::artifacts::{parse_corrupt_copy_name, remove_corrupt_copies}; use anyhow::{Context, Result}; use std::fs; @@ -11,7 +13,7 @@ use crate::session::options::SessionOptions; pub fn clear_session(options: &SessionOptions) -> Result { let session_path = options.session_file_path(); if options.is_named_file() { - crate::session::validate_named_session_file_for_clear(&session_path)?; + session::validate_named_session_file_for_clear(&session_path)?; } let backup_path = options.backup_file_path(); let backup_recovery_marker_path = options.backup_recovery_marker_file_path(); @@ -92,7 +94,7 @@ fn remove_recovery_files(recovery_path: &Path) -> Result { let Some(name) = path.file_name() else { continue; }; - if crate::session::artifacts::is_recovery_variant(name, recovery_name) { + if artifacts::is_recovery_variant(name, recovery_name) { fs::remove_file(&path) .with_context(|| format!("failed to remove {}", path.display()))?; removed = true; diff --git a/src/session/storage/inspect.rs b/src/session/storage/inspect.rs index f131fee85..6034ae07e 100644 --- a/src/session/storage/inspect.rs +++ b/src/session/storage/inspect.rs @@ -6,6 +6,9 @@ use std::io::ErrorKind; use std::os::unix::fs::OpenOptionsExt; use std::path::{Path, PathBuf}; +use crate::session; +use crate::session::snapshot::SessionSnapshot; + use super::types::{FrameCounts, HistoryCounts, HistoryDepth, SessionInspection}; use crate::session::lock::{lock_shared, unlock}; use crate::session::options::SessionOptions; @@ -21,7 +24,7 @@ pub fn inspect_session(options: &SessionOptions) -> Result { let mut session_path = options.session_file_path(); let mut session_identity = options.output_identity().map(|s| s.to_string()); if options.is_named_file() { - crate::session::validate_named_session_file_for_info(&session_path)?; + session::validate_named_session_file_for_info(&session_path)?; } let mut metadata = primary_metadata_for_inspection(&session_path, options)?; @@ -214,10 +217,7 @@ fn load_snapshot_with_shared_lock( loaded } -fn board_pages<'a>( - snapshot: &'a crate::session::snapshot::SessionSnapshot, - id: &str, -) -> Option<&'a BoardPagesSnapshot> { +fn board_pages<'a>(snapshot: &'a SessionSnapshot, id: &str) -> Option<&'a BoardPagesSnapshot> { snapshot .boards .iter() diff --git a/src/session/storage/tests.rs b/src/session/storage/tests.rs index 2b74dcf1f..6a9e4e80e 100644 --- a/src/session/storage/tests.rs +++ b/src/session/storage/tests.rs @@ -1,3 +1,7 @@ +use crate::draw::EraserKind; +use crate::input::EraserMode; +use crate::{draw, session, test_temp}; + use super::{ClearToolStateOutcome, clear_session, clear_tool_state, inspect_session}; use crate::draw::{Color, FontDescriptor, Frame, Shape}; use crate::session::snapshot::{BoardPagesSnapshot, BoardSnapshot}; @@ -5,10 +9,13 @@ use crate::session::{ CompressionMode, SessionOptions, SessionSnapshot, ToolStateSnapshot, save_snapshot, }; #[cfg(unix)] -use std::{ - ffi::CString, - os::unix::{ffi::OsStrExt, fs::symlink}, -}; +use std::ffi::CString; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +#[cfg(unix)] +use std::os::unix::fs::symlink; +use std::path::Path; +use std::{fs, io}; fn transparent_line_snapshot() -> SessionSnapshot { let mut frame = Frame::new(); @@ -50,8 +57,8 @@ fn sample_tool_state() -> ToolStateSnapshot { }, current_thickness: 3.0, eraser_size: 12.0, - eraser_kind: crate::draw::EraserKind::Circle, - eraser_mode: crate::input::EraserMode::Brush, + eraser_kind: EraserKind::Circle, + eraser_mode: EraserMode::Brush, blur_style: Default::default(), recent_colors: Vec::new(), pen_smoothing: None, @@ -68,7 +75,7 @@ fn sample_tool_state() -> ToolStateSnapshot { arrow_head_at_end: Some(false), arrow_style: None, arrow_label_enabled: Some(false), - polygon_sides: crate::draw::REGULAR_POLYGON_DEFAULT_SIDES, + polygon_sides: draw::REGULAR_POLYGON_DEFAULT_SIDES, board_previous_color: None, tool_settings: None, } @@ -76,41 +83,41 @@ fn sample_tool_state() -> ToolStateSnapshot { #[test] fn clear_session_removes_all_variants_for_prefix() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-1"); options.per_output = true; let base_dir = &options.base_dir; - std::fs::write(base_dir.join("session-display_1.json"), b"{}").unwrap(); - std::fs::write(base_dir.join("session-display_1-DP_1.json"), b"{}").unwrap(); - std::fs::write(base_dir.join("session-display_1.json.cleared"), b"{}").unwrap(); - std::fs::write(base_dir.join("session-display_1-DP_1.json.cleared"), b"{}").unwrap(); - std::fs::write(base_dir.join("session-display_1.json.bak"), b"{}").unwrap(); - std::fs::write(base_dir.join("session-display_1-DP_1.json.bak"), b"{}").unwrap(); - std::fs::write( + fs::write(base_dir.join("session-display_1.json"), b"{}").unwrap(); + fs::write(base_dir.join("session-display_1-DP_1.json"), b"{}").unwrap(); + fs::write(base_dir.join("session-display_1.json.cleared"), b"{}").unwrap(); + fs::write(base_dir.join("session-display_1-DP_1.json.cleared"), b"{}").unwrap(); + fs::write(base_dir.join("session-display_1.json.bak"), b"{}").unwrap(); + fs::write(base_dir.join("session-display_1-DP_1.json.bak"), b"{}").unwrap(); + fs::write( base_dir.join("session-display_1.json.bak.recoverable"), b"{}", ) .unwrap(); - std::fs::write( + fs::write( base_dir.join("session-display_1-DP_1.json.bak.recoverable"), b"{}", ) .unwrap(); - std::fs::write(base_dir.join("session-display_1.json.recovery"), b"{}").unwrap(); - std::fs::write( + fs::write(base_dir.join("session-display_1.json.recovery"), b"{}").unwrap(); + fs::write( base_dir.join("session-display_1.json.recovery.recoverable"), b"{}", ) .unwrap(); - std::fs::write( + fs::write( base_dir.join("session-display_1.json.recovery.empty"), b"{}", ) .unwrap(); - std::fs::write(base_dir.join("session-display_1-DP_1.json.recovery"), b"{}").unwrap(); - std::fs::write(base_dir.join("session-display_1.lock"), b"").unwrap(); - std::fs::write(base_dir.join("session-display_1-DP_1.lock"), b"").unwrap(); + fs::write(base_dir.join("session-display_1-DP_1.json.recovery"), b"{}").unwrap(); + fs::write(base_dir.join("session-display_1.lock"), b"").unwrap(); + fs::write(base_dir.join("session-display_1-DP_1.lock"), b"").unwrap(); let outcome = clear_session(&options).expect("clear_session should succeed"); assert!( @@ -188,7 +195,7 @@ fn clear_session_removes_all_variants_for_prefix() { #[test] fn clear_session_removes_every_outputs_unreadable_copies_and_no_neighbors() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-1"); options.per_output = true; let base_dir = &options.base_dir; @@ -201,7 +208,7 @@ fn clear_session_removes_every_outputs_unreadable_copies_and_no_neighbors() { "session-display_1-DP_1.json.corrupt-01", ]; for name in removed.iter().chain(&kept) { - std::fs::write(base_dir.join(name), b"unreadable").unwrap(); + fs::write(base_dir.join(name), b"unreadable").unwrap(); } let outcome = clear_session(&options).unwrap(); @@ -218,7 +225,7 @@ fn clear_session_removes_every_outputs_unreadable_copies_and_no_neighbors() { #[test] fn clear_session_keeps_neighbor_display_prefix_variants() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "wayland-1"); options.per_output = true; @@ -248,7 +255,7 @@ fn clear_session_keeps_neighbor_display_prefix_variants() { "session-wayland_10.json.recovery.empty", "session-wayland_10.lock", ] { - std::fs::write(base_dir.join(name), b"{}").unwrap(); + fs::write(base_dir.join(name), b"{}").unwrap(); } clear_session(&options).expect("clear_session should succeed"); @@ -295,7 +302,7 @@ fn clear_session_keeps_neighbor_display_prefix_variants() { #[test] fn inspect_session_reports_counts_and_flags() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-inspect"); options.persist_transparent = true; options.persist_whiteboard = false; @@ -365,7 +372,7 @@ fn inspect_session_reports_counts_and_flags() { #[test] fn clear_tool_state_preserves_board_data() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-clear-tool"); options.persist_transparent = true; options.persist_history = true; @@ -383,7 +390,7 @@ fn clear_tool_state_preserves_board_data() { } ); - let loaded = crate::session::load_snapshot(&options) + let loaded = session::load_snapshot(&options) .expect("load should succeed") .expect("snapshot should remain"); assert!(loaded.tool_state.is_none(), "tool state should be removed"); @@ -393,7 +400,7 @@ fn clear_tool_state_preserves_board_data() { #[test] fn clear_tool_state_rewrites_compressed_session() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-compressed"); options.persist_transparent = true; options.restore_tool_state = true; @@ -431,7 +438,7 @@ fn clear_tool_state_rewrites_compressed_session() { #[test] fn clear_tool_state_without_output_identity_clears_all_per_output_variants() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-1"); options.persist_transparent = true; options.persist_history = true; @@ -467,13 +474,13 @@ fn clear_tool_state_without_output_identity_clears_all_per_output_variants() { } ); - let first_loaded = crate::session::load_snapshot(&first_output) + let first_loaded = session::load_snapshot(&first_output) .expect("first output load should succeed") .expect("first output snapshot should remain"); - let second_loaded = crate::session::load_snapshot(&second_output) + let second_loaded = session::load_snapshot(&second_output) .expect("second output load should succeed") .expect("second output snapshot should remain"); - let neighbor_loaded = crate::session::load_snapshot(&neighbor) + let neighbor_loaded = session::load_snapshot(&neighbor) .expect("neighbor load should succeed") .expect("neighbor snapshot should remain"); @@ -489,7 +496,7 @@ fn clear_tool_state_without_output_identity_clears_all_per_output_variants() { #[test] fn clear_tool_state_missing_session_is_nonfatal() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let options = SessionOptions::new(temp.path().to_path_buf(), "display-missing"); let outcome = clear_tool_state(&options).expect("clear_tool_state should succeed"); @@ -499,7 +506,7 @@ fn clear_tool_state_missing_session_is_nonfatal() { #[test] fn clear_tool_state_without_tool_state_is_nonfatal_and_preserves_boards() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-no-tool"); options.persist_transparent = true; options.restore_tool_state = true; @@ -510,7 +517,7 @@ fn clear_tool_state_without_tool_state_is_nonfatal_and_preserves_boards() { let outcome = clear_tool_state(&options).expect("clear_tool_state should succeed"); assert_eq!(outcome, ClearToolStateOutcome::NoToolState); - let loaded = crate::session::load_snapshot(&options) + let loaded = session::load_snapshot(&options) .expect("load should succeed") .expect("snapshot should remain"); assert!(loaded.tool_state.is_none()); @@ -519,7 +526,7 @@ fn clear_tool_state_without_tool_state_is_nonfatal_and_preserves_boards() { #[test] fn clear_named_tool_state_targets_only_selected_file() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let selected = temp.path().join("lecture-04.wayscriber-session"); let sibling = temp.path().join("lecture-05.wayscriber-session"); @@ -550,10 +557,10 @@ fn clear_named_tool_state_targets_only_selected_file() { } ); - let selected_loaded = crate::session::load_snapshot(&selected_options) + let selected_loaded = session::load_snapshot(&selected_options) .expect("selected load should succeed") .expect("selected snapshot should remain"); - let sibling_loaded = crate::session::load_snapshot(&sibling_options) + let sibling_loaded = session::load_snapshot(&sibling_options) .expect("sibling load should succeed") .expect("sibling snapshot should remain"); @@ -566,7 +573,7 @@ fn clear_named_tool_state_targets_only_selected_file() { #[test] fn inspect_named_file_missing_parent_reports_absent_session() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display"); let named_path = temp .path() @@ -583,7 +590,7 @@ fn inspect_named_file_missing_parent_reports_absent_session() { #[test] fn inspect_named_file_does_not_create_missing_lock() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let named_path = temp.path().join("lecture.wayscriber-session"); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display"); options.set_named_file_target(named_path); @@ -593,7 +600,7 @@ fn inspect_named_file_does_not_create_missing_lock() { save_snapshot(&snapshot, &options).expect("save_snapshot should succeed"); let lock_path = options.lock_file_path(); - std::fs::remove_file(&lock_path).expect("remove named session lock"); + fs::remove_file(&lock_path).expect("remove named session lock"); let inspection = inspect_session(&options).expect("inspect should succeed"); @@ -615,7 +622,7 @@ fn inspect_named_file_does_not_create_missing_lock() { #[cfg(unix)] #[test] fn inspect_named_file_rejects_special_lock_without_blocking() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let named_path = temp.path().join("lecture.wayscriber-session"); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display"); options.set_named_file_target(named_path); @@ -624,7 +631,7 @@ fn inspect_named_file_rejects_special_lock_without_blocking() { let snapshot = transparent_line_snapshot(); save_snapshot(&snapshot, &options).expect("save_snapshot should succeed"); let lock_path = options.lock_file_path(); - std::fs::remove_file(&lock_path).expect("remove named session lock"); + fs::remove_file(&lock_path).expect("remove named session lock"); make_fifo(&lock_path); let err = inspect_session(&options).expect_err("special lock should be rejected"); @@ -638,11 +645,11 @@ fn inspect_named_file_rejects_special_lock_without_blocking() { #[test] fn clear_named_file_removes_only_selected_artifacts() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let selected = temp.path().join("lecture-04.wayscriber-session"); let sibling = temp.path().join("lecture-05.wayscriber-session"); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display"); - options.set_named_file_target(selected.clone()); + options.set_named_file_target(selected); for path in [ options.session_file_path(), @@ -653,14 +660,10 @@ fn clear_named_file_removes_only_selected_artifacts() { options.clear_marker_file_path(), options.lock_file_path(), ] { - std::fs::write(path, b"artifact").unwrap(); + fs::write(path, b"artifact").unwrap(); } for suffix in ["", ".bak", ".recovery", ".cleared", ".lock"] { - std::fs::write( - crate::session::append_path_suffix(&sibling, suffix), - b"sibling", - ) - .unwrap(); + fs::write(session::append_path_suffix(&sibling, suffix), b"sibling").unwrap(); } let outcome = clear_session(&options).expect("clear should succeed"); @@ -681,7 +684,7 @@ fn clear_named_file_removes_only_selected_artifacts() { assert!(!path.exists(), "{} should be removed", path.display()); } for suffix in ["", ".bak", ".recovery", ".cleared", ".lock"] { - let path = crate::session::append_path_suffix(&sibling, suffix); + let path = session::append_path_suffix(&sibling, suffix); assert!(path.exists(), "{} should be preserved", path.display()); } } @@ -689,28 +692,28 @@ fn clear_named_file_removes_only_selected_artifacts() { #[cfg(unix)] #[test] fn clear_named_file_rejects_symlink_primary_without_removing_artifacts() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let target = temp.path().join("real-session.wayscriber-session"); let link = temp.path().join("linked-session.wayscriber-session"); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display"); options.set_named_file_target(link.clone()); - std::fs::write(&target, b"target").expect("write target session"); + fs::write(&target, b"target").expect("write target session"); symlink(&target, &link).expect("create symlink primary"); - std::fs::write(options.backup_file_path(), b"backup").expect("write backup sidecar"); + fs::write(options.backup_file_path(), b"backup").expect("write backup sidecar"); let err = clear_session(&options).expect_err("symlink primary should reject clear"); assert!(err.to_string().contains("not a symlink"), "{err:#}"); assert!( - std::fs::symlink_metadata(&link) + fs::symlink_metadata(&link) .expect("symlink should remain") .file_type() .is_symlink(), "clear must not remove the symlink primary" ); assert_eq!( - std::fs::read(&target).expect("target remains"), + fs::read(&target).expect("target remains"), b"target", "clear must not affect the symlink target" ); @@ -721,7 +724,7 @@ fn clear_named_file_rejects_symlink_primary_without_removing_artifacts() { } #[cfg(unix)] -fn make_fifo(path: &std::path::Path) { +fn make_fifo(path: &Path) { let raw_path = CString::new(path.as_os_str().as_bytes()).expect("fifo path has no NUL bytes"); // SAFETY: raw_path is a valid, NUL-terminated filesystem path for this process. let result = unsafe { libc::mkfifo(raw_path.as_ptr(), 0o600) }; @@ -730,6 +733,6 @@ fn make_fifo(path: &std::path::Path) { 0, "mkfifo {} failed: {}", path.display(), - std::io::Error::last_os_error() + io::Error::last_os_error() ); } diff --git a/src/session/storage/tool_state.rs b/src/session/storage/tool_state.rs index b562c69f8..bf93abffc 100644 --- a/src/session/storage/tool_state.rs +++ b/src/session/storage/tool_state.rs @@ -2,6 +2,8 @@ use anyhow::{Context, Result, anyhow}; use std::collections::BTreeSet; use std::fs; +use crate::session; + use super::types::ClearToolStateOutcome; use crate::session::options::SessionOptions; use crate::session::snapshot::{self, LoadSnapshotOutcome}; @@ -9,7 +11,7 @@ use crate::session::snapshot::{self, LoadSnapshotOutcome}; /// Remove only the saved tool state from the session snapshot. pub fn clear_tool_state(options: &SessionOptions) -> Result { if options.is_named_file() { - crate::session::validate_named_session_file_for_clear(&options.session_file_path())?; + session::validate_named_session_file_for_clear(&options.session_file_path())?; } let targets = tool_state_edit_targets(options); diff --git a/src/session/tests/board_appearance.rs b/src/session/tests/board_appearance.rs index 07df34a2c..15584689c 100644 --- a/src/session/tests/board_appearance.rs +++ b/src/session/tests/board_appearance.rs @@ -1,3 +1,8 @@ +use crate::draw::{Frame, TextMeasurer}; +use crate::input::Tool; +use crate::session; +use crate::session::{BoardPagesSnapshot, BoardSnapshot, SessionSnapshot}; + use super::helpers::dummy_input_state; use crate::domain::{BoardGrid, BoardGridKind}; use crate::draw::{BLACK, RED}; @@ -48,7 +53,7 @@ fn board_appearance_same_id_restore_resolves_pen_then_tool_state_wins() { let mut target = dummy_input_state(); target.switch_board_force("whiteboard"); apply_snapshot(&mut target, snapshot, &options); - assert_eq!(target.color_for_tool(crate::input::Tool::Pen), RED); + assert_eq!(target.color_for_tool(Tool::Pen), RED); assert_eq!( target.boards.active_board().pen_origin, BoardPenOrigin::RuntimeContrast @@ -62,7 +67,7 @@ fn board_appearance_same_id_restore_resolves_pen_then_tool_state_wins() { tools.tool_settings = None; tools.board_previous_color = Some(RED); apply_snapshot(&mut target, snapshot, &options); - assert_eq!(target.color_for_tool(crate::input::Tool::Pen), BLACK); + assert_eq!(target.color_for_tool(Tool::Pen), BLACK); assert_eq!(target.board_previous_color(), Some(RED)); } @@ -89,37 +94,37 @@ fn board_appearance_named_replacement_clears_absent_overrides_and_failed_replace input.switch_board_force("whiteboard"); let seed = BoardAppearance::from_spec(&input.boards.active_board().spec); input.set_board_background_color(input.boards.active_index(), RED); - input.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); - input.board_picker_edit_color_selected_with_measurer(&crate::draw::TextMeasurer::default()); - let empty = crate::session::SessionSnapshot { + input.open_board_picker_with_measurer(&TextMeasurer::default()); + input.board_picker_edit_color_selected_with_measurer(&TextMeasurer::default()); + let empty = SessionSnapshot { active_board_id: "whiteboard".into(), boards: Vec::new(), tool_state: None, }; let mut too_many = empty.clone(); for index in 0..input.boards.max_count() + 1 { - too_many.boards.push(crate::session::BoardSnapshot { + too_many.boards.push(BoardSnapshot { id: format!("overflow-{index}"), appearance: None, - pages: crate::session::BoardPagesSnapshot { - pages: vec![crate::draw::Frame::new()], + pages: BoardPagesSnapshot { + pages: vec![Frame::new()], active: 0, }, }); } assert!( - crate::session::apply_snapshot_replacing_boards( + session::apply_snapshot_replacing_boards( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), too_many, &options() ) .is_err() ); assert!(input.board_appearance_edit().is_some()); - crate::session::apply_snapshot_replacing_boards( + session::apply_snapshot_replacing_boards( &mut input, - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), empty, &options(), ) diff --git a/src/session/tests/helpers.rs b/src/session/tests/helpers.rs index 61fe5376e..8256d867d 100644 --- a/src/session/tests/helpers.rs +++ b/src/session/tests/helpers.rs @@ -1,11 +1,12 @@ use crate::config::{Action, Shortcut}; use crate::input::InputState; +use crate::input::state::test_support::TestInputStateBuilder; use std::collections::HashMap; pub(super) fn dummy_input_state() -> InputState { let mut action_map = HashMap::new(); action_map.insert(Shortcut::parse("Escape").unwrap(), Action::Exit); - crate::input::state::test_support::TestInputStateBuilder::default() + TestInputStateBuilder::default() .action_map(action_map) .action_bindings(HashMap::new()) .thickness(3.0) diff --git a/src/session/tests/history.rs b/src/session/tests/history.rs index bc3175c81..48b357a32 100644 --- a/src/session/tests/history.rs +++ b/src/session/tests/history.rs @@ -1,3 +1,6 @@ +use crate::draw::{Frame, ShapeId, TextMeasurer}; +use crate::test_temp; + use super::super::*; use super::helpers::dummy_input_state; use crate::draw::frame::{ShapeSnapshot, UndoAction}; @@ -7,7 +10,7 @@ use std::fs; #[test] fn snapshot_preserves_history_only_frames() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-history"); options.persist_transparent = true; options.persist_history = true; @@ -59,7 +62,7 @@ fn snapshot_preserves_history_only_frames() { #[test] fn modify_delete_cycle_survives_restore() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-modify-delete"); options.persist_transparent = true; options.persist_history = true; @@ -155,7 +158,7 @@ fn modify_delete_cycle_survives_restore() { #[test] fn clear_all_can_be_undone_after_restore() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-clear-all"); options.persist_transparent = true; options.persist_history = true; @@ -203,8 +206,8 @@ fn clear_all_can_be_undone_after_restore() { #[test] fn moved_multi_selection_with_history_survives_save_restore() { - let route_measurer = crate::draw::TextMeasurer::default(); - let temp = crate::test_temp::tempdir().unwrap(); + let route_measurer = TextMeasurer::default(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-multi-move"); options.persist_transparent = true; options.persist_history = true; @@ -279,7 +282,7 @@ fn moved_multi_selection_with_history_survives_save_restore() { ); } -fn freehand_points(frame: &crate::draw::Frame, id: crate::draw::ShapeId) -> Vec<(f64, f64)> { +fn freehand_points(frame: &Frame, id: ShapeId) -> Vec<(f64, f64)> { let shape = frame.shape(id).expect("shape exists"); match &shape.shape { Shape::Freehand { points, .. } => points.clone(), @@ -289,7 +292,7 @@ fn freehand_points(frame: &crate::draw::Frame, id: crate::draw::ShapeId) -> Vec< #[test] fn corrupted_history_is_dropped_but_shapes_load() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-corrupt"); options.persist_transparent = true; options.persist_history = true; diff --git a/src/session/tests/limits.rs b/src/session/tests/limits.rs index 75ac87658..864300ea4 100644 --- a/src/session/tests/limits.rs +++ b/src/session/tests/limits.rs @@ -1,3 +1,8 @@ +use flate2::read::GzDecoder; + +use crate::draw::{EraserKind, Frame}; +use crate::{draw, test_temp}; + use super::super::*; use super::helpers::dummy_input_state; use crate::draw::frame::{ShapeSnapshot, UndoAction}; @@ -8,7 +13,7 @@ use std::path::Path; #[test] fn save_snapshot_errors_when_payload_exceeds_max_file_size() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-too-big"); options.persist_transparent = false; options.persist_whiteboard = false; @@ -30,7 +35,7 @@ fn save_snapshot_errors_when_payload_exceeds_max_file_size() { }, current_thickness: 3.0, eraser_size: 12.0, - eraser_kind: crate::draw::EraserKind::Circle, + eraser_kind: EraserKind::Circle, eraser_mode: EraserMode::Brush, blur_style: Default::default(), recent_colors: Vec::new(), @@ -48,7 +53,7 @@ fn save_snapshot_errors_when_payload_exceeds_max_file_size() { arrow_head_at_end: Some(false), arrow_style: None, arrow_label_enabled: Some(false), - polygon_sides: crate::draw::REGULAR_POLYGON_DEFAULT_SIDES, + polygon_sides: draw::REGULAR_POLYGON_DEFAULT_SIDES, board_previous_color: None, tool_settings: None, }), @@ -69,7 +74,7 @@ fn save_snapshot_errors_when_payload_exceeds_max_file_size() { #[test] fn load_snapshot_refuses_file_larger_than_max() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-large-file"); options.persist_transparent = true; options.max_file_size_bytes = 8; // very small @@ -91,7 +96,7 @@ fn load_snapshot_refuses_file_larger_than_max() { #[test] fn oversized_save_writes_recovery_and_load_prefers_it() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-recovery"); options.persist_transparent = true; options.persist_history = false; @@ -119,7 +124,7 @@ fn oversized_save_writes_recovery_and_load_prefers_it() { assert_eq!(count_images(&loaded), 1); assert_eq!(loaded.boards[0].pages.pages.len(), 1); - let mut roomy_options = options.clone(); + let mut roomy_options = options; roomy_options.max_file_size_bytes = u64::MAX; save_snapshot(&loaded, &roomy_options).expect("normal save of recovered state should succeed"); assert!( @@ -130,7 +135,7 @@ fn oversized_save_writes_recovery_and_load_prefers_it() { #[test] fn load_snapshot_truncates_shapes_when_exceeding_max_shapes_per_frame() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut save_options = SessionOptions::new(temp.path().to_path_buf(), "display-shape-limit"); save_options.persist_transparent = true; @@ -180,14 +185,14 @@ fn load_snapshot_truncates_shapes_when_exceeding_max_shapes_per_frame() { ); } -fn named_frame(name: &str) -> crate::draw::Frame { - let mut frame = crate::draw::Frame::new(); +fn named_frame(name: &str) -> Frame { + let mut frame = Frame::new(); frame.set_page_name(Some(name.to_string())); frame } -fn image_frame(bytes: usize) -> crate::draw::Frame { - let mut frame = crate::draw::Frame::new(); +fn image_frame(bytes: usize) -> Frame { + let mut frame = Frame::new(); frame.add_shape(Shape::Image { x: 12, y: 16, @@ -203,7 +208,7 @@ fn image_frame(bytes: usize) -> crate::draw::Frame { frame } -fn single_page_snapshot(frame: crate::draw::Frame) -> SessionSnapshot { +fn single_page_snapshot(frame: Frame) -> SessionSnapshot { SessionSnapshot { active_board_id: "transparent".to_string(), boards: vec![BoardSnapshot { @@ -249,7 +254,7 @@ fn populate_compressed_limit_pages(input: &mut InputState, page_count: usize, im } } -fn assert_restored_limit_page(page: &crate::draw::Frame, page_index: usize, image_bytes: usize) { +fn assert_restored_limit_page(page: &Frame, page_index: usize, image_bytes: usize) { assert_eq!(page.shapes.len(), 3, "page {page_index} shape count"); assert_eq!(page.undo_stack_len(), 1, "page {page_index} undo depth"); match &page.shapes[0].shape { @@ -285,7 +290,7 @@ fn save_snapshot_allows_compressed_payload_that_fits_limit() { const ACTIVE_PAGE: usize = 10; const IMAGE_BYTES: usize = 64 * 1024; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-compressed-fit"); options.persist_whiteboard = true; options.persist_history = true; @@ -340,7 +345,7 @@ fn save_snapshot_allows_compressed_payload_that_fits_limit() { #[test] fn save_snapshot_drops_history_when_modified_stroke_exceeds_limit() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut input = dummy_input_state(); let point_count = 1_500; @@ -442,7 +447,7 @@ fn save_snapshot_drops_history_when_modified_stroke_exceeds_limit() { #[test] fn save_snapshot_keeps_largest_recent_history_depth_that_fits() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut input = dummy_input_state(); let point_count = 600; @@ -516,7 +521,7 @@ fn save_snapshot_keeps_largest_recent_history_depth_that_fits() { let json = if written.starts_with(&[0x1f, 0x8b]) { use std::io::Read; let mut decoded = Vec::new(); - flate2::read::GzDecoder::new(written.as_slice()) + GzDecoder::new(written.as_slice()) .read_to_end(&mut decoded) .unwrap(); decoded @@ -547,7 +552,7 @@ fn save_snapshot_keeps_largest_recent_history_depth_that_fits() { #[test] fn autosave_snapshot_uses_bounded_history_fallback() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut input = dummy_input_state(); let point_count = 600; @@ -633,7 +638,7 @@ fn autosave_snapshot_uses_bounded_history_fallback() { #[test] fn save_snapshot_keeps_depth_one_when_visible_payload_is_near_limit() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut input = dummy_input_state(); { let frame = input.boards.active_frame_mut(); @@ -764,7 +769,7 @@ fn save_snapshot_report_marks_near_limit_at_ninety_percent() { assert!(!report.is_near_limit()); } -fn add_image_and_annotations(frame: &mut crate::draw::Frame, page_index: usize, bytes: usize) { +fn add_image_and_annotations(frame: &mut Frame, page_index: usize, bytes: usize) { let image_id = frame.add_shape(Shape::Image { x: 12, y: 16, diff --git a/src/session/tests/options.rs b/src/session/tests/options.rs index c228189a6..d9f80731c 100644 --- a/src/session/tests/options.rs +++ b/src/session/tests/options.rs @@ -1,10 +1,15 @@ +use std::env; +use std::path::PathBuf; + +use crate::{test_env, test_temp}; + use super::super::*; use crate::config::{SessionConfig, SessionStorageMode}; use crate::env_vars::WAYLAND_DISPLAY_ENV; #[test] fn options_from_config_custom_storage() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let custom_dir = temp.path().join("sessions"); let cfg = SessionConfig { @@ -30,7 +35,8 @@ fn options_from_config_custom_storage() { #[test] fn options_from_config_config_storage_uses_config_dir() { - let temp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let temp = test_temp::tempdir().unwrap(); let cfg = SessionConfig { persist_whiteboard: true, @@ -38,14 +44,16 @@ fn options_from_config_config_storage_uses_config_dir() { ..SessionConfig::default() }; - let original_display = std::env::var_os(WAYLAND_DISPLAY_ENV); + let original_display = env::var_os(WAYLAND_DISPLAY_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { - std::env::remove_var(WAYLAND_DISPLAY_ENV); + env::remove_var(WAYLAND_DISPLAY_ENV); } let mut options = options_from_config(&cfg, temp.path(), None).unwrap(); if let Some(value) = original_display { - unsafe { std::env::set_var(WAYLAND_DISPLAY_ENV, value) } + // SAFETY: this test still holds the environment mutex in `_guard`. + unsafe { env::set_var(WAYLAND_DISPLAY_ENV, value) } } assert_eq!(options.base_dir, temp.path()); @@ -71,7 +79,7 @@ fn options_from_config_config_storage_uses_config_dir() { #[test] fn session_file_without_per_output_suffix_when_disabled() { - let mut options = SessionOptions::new(std::path::PathBuf::from("/tmp"), "display"); + let mut options = SessionOptions::new(PathBuf::from("/tmp"), "display"); options.per_output = false; let original = options.session_file_path(); options.set_output_identity(Some("DP-1")); diff --git a/src/session/tests/roundtrip.rs b/src/session/tests/roundtrip.rs index be0b6849a..edee74164 100644 --- a/src/session/tests/roundtrip.rs +++ b/src/session/tests/roundtrip.rs @@ -1,3 +1,5 @@ +use crate::{session, test_temp}; + use super::super::*; use super::helpers::dummy_input_state; use crate::draw::{Color, FontDescriptor, Frame, Shape}; @@ -7,7 +9,7 @@ use std::fs; #[test] fn session_roundtrip_preserves_shapes_across_frames() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-2"); options.persist_transparent = true; options.persist_whiteboard = true; @@ -88,7 +90,7 @@ fn session_roundtrip_preserves_pages_beyond_visible_shortcuts() { const PAGE_COUNT: usize = 12; const ACTIVE_PAGE: usize = 10; - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-many-pages"); options.persist_whiteboard = true; @@ -141,7 +143,7 @@ fn session_roundtrip_preserves_pages_beyond_visible_shortcuts() { #[test] fn save_snapshot_rotates_backup_when_enabled() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-backup"); options.persist_transparent = true; options.backup_retention = 1; @@ -188,7 +190,7 @@ fn save_snapshot_rotates_backup_when_enabled() { #[test] fn save_snapshot_skips_backup_when_disabled() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-no-backup"); options.persist_transparent = true; options.backup_retention = 0; @@ -233,7 +235,7 @@ fn save_snapshot_skips_backup_when_disabled() { #[test] fn corrupt_session_is_backed_up_and_reset() { - let temp = crate::test_temp::tempdir().unwrap(); + let temp = test_temp::tempdir().unwrap(); let mut options = SessionOptions::new(temp.path().to_path_buf(), "display-bad"); options.persist_transparent = true; @@ -243,7 +245,7 @@ fn corrupt_session_is_backed_up_and_reset() { let loaded = load_snapshot(&options).expect("load should not error"); assert!(loaded.is_none()); - let copy = crate::session::append_path_suffix(&session_path, ".corrupt-1"); + let copy = session::append_path_suffix(&session_path, ".corrupt-1"); assert_eq!(fs::read(copy).expect("corrupt copy present"), b"not json"); assert!(!options.backup_file_path().exists()); assert!( diff --git a/src/session/tests/snapshot.rs b/src/session/tests/snapshot.rs index f6822726c..9584a03df 100644 --- a/src/session/tests/snapshot.rs +++ b/src/session/tests/snapshot.rs @@ -1,12 +1,15 @@ +use crate::draw; +use crate::draw::TextMeasurer; +use crate::ui_text::UiTextEngine; + use super::super::*; use super::helpers::dummy_input_state; use crate::config::{Action, KeybindingsConfig}; use crate::draw::{ ArrowStyle, BlurStyle, Color, EraserKind, FontDescriptor, Frame, PageDeleteOutcome, Shape, }; -use crate::input::BOARD_ID_BLACKBOARD; use crate::input::state::{MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; -use crate::input::{EraserMode, PerToolDrawingSettings, Tool}; +use crate::input::{BOARD_ID_BLACKBOARD, EraserMode, PerToolDrawingSettings, Tool}; use std::path::PathBuf; #[test] @@ -103,11 +106,7 @@ fn every_persisted_drawing_style_survives_snapshot_serialization_and_restore() { let decoded: ToolStateSnapshot = serde_json::from_slice(&encoded).expect("deserialize tool snapshot fixture"); let mut restored = dummy_input_state(); - apply_tool_state_snapshot( - &mut restored, - &crate::draw::TextMeasurer::default(), - decoded, - ); + apply_tool_state_snapshot(&mut restored, &TextMeasurer::default(), &decoded); let recaptured = ToolStateSnapshot::from_input_state(&restored); assert_eq!( @@ -132,11 +131,7 @@ fn non_default_pen_smoothing_survives_snapshot_serialization_and_restore() { let mut restored = dummy_input_state(); let _ = restored.set_pen_smoothing(1); - apply_tool_state_snapshot( - &mut restored, - &crate::draw::TextMeasurer::default(), - decoded, - ); + apply_tool_state_snapshot(&mut restored, &TextMeasurer::default(), &decoded); assert_eq!(restored.style.pen_smoothing, 5); } @@ -153,11 +148,7 @@ fn shape_recognition_sensitivity_survives_snapshot_serialization_and_restore() { let mut restored = dummy_input_state(); let _ = restored.set_shape_recognition_sensitivity(0); - apply_tool_state_snapshot( - &mut restored, - &crate::draw::TextMeasurer::default(), - decoded, - ); + apply_tool_state_snapshot(&mut restored, &TextMeasurer::default(), &decoded); assert_eq!(restored.style.shape_recognition_sensitivity, 4); } @@ -178,11 +169,7 @@ fn legacy_snapshot_without_pen_smoothing_preserves_the_configured_level() { let mut restored = dummy_input_state(); let _ = restored.set_pen_smoothing(4); - apply_tool_state_snapshot( - &mut restored, - &crate::draw::TextMeasurer::default(), - decoded, - ); + apply_tool_state_snapshot(&mut restored, &TextMeasurer::default(), &decoded); assert_eq!( restored.style.pen_smoothing, 4, @@ -192,8 +179,8 @@ fn legacy_snapshot_without_pen_smoothing_preserves_the_configured_level() { #[test] fn snapshot_uses_pre_light_mode_tool_state() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -313,8 +300,8 @@ fn restoring_a_session_never_carries_a_rebound_shortcut() { /// just the live one. #[test] fn restoring_a_session_leaves_focus_modes_pending_status_bar_value_alone() { - let test_text_measurer = crate::draw::TextMeasurer::default(); - let test_ui_engine = crate::ui_text::UiTextEngine::default(); + let test_text_measurer = TextMeasurer::default(); + let test_ui_engine = UiTextEngine::default(); let test_text_resources = crate::input::state::InputTextResources { measurer: &test_text_measurer, ui_engine: &test_ui_engine, @@ -469,7 +456,7 @@ fn apply_legacy_snapshot_preserves_config_initialized_font_descriptor() { }, current_thickness: 4.0, eraser_size: 12.0, - eraser_kind: crate::draw::EraserKind::Circle, + eraser_kind: EraserKind::Circle, eraser_mode: EraserMode::Brush, blur_style: Default::default(), recent_colors: Vec::new(), @@ -487,7 +474,7 @@ fn apply_legacy_snapshot_preserves_config_initialized_font_descriptor() { arrow_head_at_end: Some(false), arrow_style: None, arrow_label_enabled: Some(false), - polygon_sides: crate::draw::REGULAR_POLYGON_DEFAULT_SIDES, + polygon_sides: draw::REGULAR_POLYGON_DEFAULT_SIDES, board_previous_color: None, tool_settings: None, }), @@ -526,7 +513,7 @@ fn apply_snapshot_clamps_restored_per_tool_thicknesses() { current_color: desired_color, current_thickness: 3.0, eraser_size: 12.0, - eraser_kind: crate::draw::EraserKind::Circle, + eraser_kind: EraserKind::Circle, eraser_mode: EraserMode::Brush, blur_style: Default::default(), recent_colors: Vec::new(), @@ -544,7 +531,7 @@ fn apply_snapshot_clamps_restored_per_tool_thicknesses() { arrow_head_at_end: Some(false), arrow_style: None, arrow_label_enabled: Some(false), - polygon_sides: crate::draw::REGULAR_POLYGON_DEFAULT_SIDES, + polygon_sides: draw::REGULAR_POLYGON_DEFAULT_SIDES, board_previous_color: None, tool_settings: Some(tool_settings), }), @@ -578,7 +565,7 @@ fn apply_legacy_snapshot_uses_font_derived_step_marker_size() { current_color: color, current_thickness: 3.0, eraser_size: 12.0, - eraser_kind: crate::draw::EraserKind::Circle, + eraser_kind: EraserKind::Circle, eraser_mode: EraserMode::Brush, blur_style: Default::default(), recent_colors: Vec::new(), @@ -596,7 +583,7 @@ fn apply_legacy_snapshot_uses_font_derived_step_marker_size() { arrow_head_at_end: Some(false), arrow_style: None, arrow_label_enabled: Some(false), - polygon_sides: crate::draw::REGULAR_POLYGON_DEFAULT_SIDES, + polygon_sides: draw::REGULAR_POLYGON_DEFAULT_SIDES, board_previous_color: None, tool_settings: None, }), diff --git a/src/shortcut_hint.rs b/src/shortcut_hint.rs index e3aa4df28..8b08c2c55 100644 --- a/src/shortcut_hint.rs +++ b/src/shortcut_hint.rs @@ -1,4 +1,4 @@ -use std::fs; +use std::{env, fs}; pub use crate::env_vars::{PORTAL_APP_ID_ENV, PORTAL_SHORTCUT_ENV, PORTAL_SHORTCUT_OPT_IN_ENV}; use crate::env_vars::{XDG_CURRENT_DESKTOP_ENV, XDG_SESSION_DESKTOP_ENV}; @@ -32,19 +32,23 @@ pub enum ShortcutRuntimeBackend { Manual, } +#[must_use] pub const fn portal_runtime_supported() -> bool { cfg!(feature = "portal") } +#[must_use] pub fn gnome_shortcut_schema_with_path() -> String { format!("{GNOME_CUSTOM_KEYBINDING_SCHEMA}:{GNOME_WAYSCRIBER_KEYBINDING_PATH}") } +#[must_use] pub fn is_gnome_desktop(current: &str, session: &str) -> bool { let combined = format!("{current};{session}").to_lowercase(); combined.contains("gnome") } +#[must_use] pub fn resolve_shortcut_runtime_backend(inputs: ShortcutRuntimeInputs) -> ShortcutRuntimeBackend { if inputs.gnome_desktop { if inputs.portal_runtime_supported @@ -61,9 +65,10 @@ pub fn resolve_shortcut_runtime_backend(inputs: ShortcutRuntimeInputs) -> Shortc } } +#[must_use] pub fn current_shortcut_runtime_backend() -> ShortcutRuntimeBackend { - let current = std::env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_default(); - let session = std::env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_default(); + let current = env::var(XDG_CURRENT_DESKTOP_ENV).unwrap_or_default(); + let session = env::var(XDG_SESSION_DESKTOP_ENV).unwrap_or_default(); resolve_shortcut_runtime_backend(ShortcutRuntimeInputs { gnome_desktop: is_gnome_desktop(¤t, &session), portal_runtime_supported: portal_runtime_supported(), @@ -71,6 +76,7 @@ pub fn current_shortcut_runtime_backend() -> ShortcutRuntimeBackend { }) } +#[must_use] pub fn normalize_shortcut_hint(value: Option<&str>) -> Option { let trimmed = value?.trim(); if trimmed.is_empty() { @@ -79,6 +85,7 @@ pub fn normalize_shortcut_hint(value: Option<&str>) -> Option { Some(trimmed.to_string()) } +#[must_use] pub fn normalize_binding_hint(value: Option<&str>) -> Option { let trimmed = value?.trim(); if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("disabled") { @@ -87,6 +94,7 @@ pub fn normalize_binding_hint(value: Option<&str>) -> Option { Some(trimmed.to_string()) } +#[must_use] pub fn parse_gsettings_string_value(raw: &str) -> Option { let trimmed = raw.trim(); if trimmed.is_empty() || trimmed == "''" { @@ -127,6 +135,7 @@ pub fn parse_gsettings_path_list(raw: &str) -> Result, String> { Ok(values) } +#[must_use] pub fn read_portal_shortcut_dropin_state() -> PortalShortcutDropInState { portal_shortcut_dropin_path() .and_then(|path| fs::read_to_string(path).ok()) @@ -134,6 +143,7 @@ pub fn read_portal_shortcut_dropin_state() -> PortalShortcutDropInState { .unwrap_or_default() } +#[must_use] pub fn parse_portal_shortcut_dropin_state(content: &str) -> PortalShortcutDropInState { let mut state = PortalShortcutDropInState::default(); for line in content.lines() { @@ -156,6 +166,7 @@ pub fn parse_portal_shortcut_dropin_state(content: &str) -> PortalShortcutDropIn state } +#[must_use] pub fn parse_portal_shortcut_from_dropin(content: &str) -> Option { content.lines().find_map(|line| { let (name, value) = parse_systemd_environment_assignment(line)?; @@ -166,6 +177,7 @@ pub fn parse_portal_shortcut_from_dropin(content: &str) -> Option { }) } +#[must_use] pub fn gnome_effective_shortcut(custom_keybindings_raw: &str, binding_raw: &str) -> Option { let configured_paths = parse_gsettings_path_list(custom_keybindings_raw).ok()?; if !configured_paths @@ -178,6 +190,7 @@ pub fn gnome_effective_shortcut(custom_keybindings_raw: &str, binding_raw: &str) normalize_binding_hint(Some(binding.as_str())) } +#[must_use] pub fn resolve_toggle_shortcut_hint( portal_shortcut_env: Option<&str>, gnome_desktop: bool, diff --git a/src/systemd_user_service.rs b/src/systemd_user_service.rs index a8f786dd9..f677f66c0 100644 --- a/src/systemd_user_service.rs +++ b/src/systemd_user_service.rs @@ -1,3 +1,4 @@ +use std::iter; use std::path::{Path, PathBuf}; use crate::env_vars::{PATH_ENV, WAYLAND_DISPLAY_ENV, XDG_RUNTIME_DIR_ENV}; @@ -5,14 +6,17 @@ use crate::paths::config_dir; pub const USER_SERVICE_NAME: &str = "wayscriber.service"; +#[must_use] pub fn user_service_unit_path() -> Option { config_dir().map(|root| user_service_unit_path_from_config_root(&root)) } +#[must_use] pub fn portal_shortcut_dropin_path() -> Option { config_dir().map(|root| portal_shortcut_dropin_path_from_config_root(&root)) } +#[must_use] pub fn user_service_unit_path_from_config_root(config_root: &Path) -> PathBuf { config_root .join("systemd") @@ -20,6 +24,7 @@ pub fn user_service_unit_path_from_config_root(config_root: &Path) -> PathBuf { .join(USER_SERVICE_NAME) } +#[must_use] pub fn portal_shortcut_dropin_path_from_config_root(config_root: &Path) -> PathBuf { config_root .join("systemd") @@ -33,6 +38,7 @@ pub fn portal_shortcut_dropin_path_from_config_root(config_root: &Path) -> PathB /// systemd expands `%` specifiers in the command word, but `$VAR` substitution /// applies only to the arguments after it, so a `$` in the path stays as it is: /// doubling it would name an executable that does not exist. +#[must_use] pub fn quote_systemd_exec(path: &Path) -> String { let mut quoted = String::from("\""); for character in path.to_string_lossy().chars() { @@ -44,6 +50,7 @@ pub fn quote_systemd_exec(path: &Path) -> String { /// Escapes `value` for a double-quoted `Environment=` assignment, where `$` /// has no special meaning and stays as it is. +#[must_use] pub fn escape_systemd_env_value(value: &str) -> String { let mut escaped = String::with_capacity(value.len()); for character in value.chars() { @@ -80,6 +87,7 @@ const BASE_SERVICE_PATH: [&str; 3] = ["/usr/local/bin", "/usr/bin", "/bin"]; /// `/usr/bin` install, pinned by `packaged_service_unit_matches_the_renderer`. /// The two used to be maintained by hand and had already drifted apart on the /// PATH they set. +#[must_use] pub fn render_user_service_unit(binary_path: &Path) -> String { let quoted_exec = quote_systemd_exec(binary_path); let binary_dir = binary_path @@ -92,7 +100,7 @@ pub fn render_user_service_unit(binary_path: &Path) -> String { let service_path = if BASE_SERVICE_PATH.contains(&binary_dir.as_str()) { BASE_SERVICE_PATH.join(":") } else { - std::iter::once(binary_dir.as_str()) + iter::once(binary_dir.as_str()) .chain(BASE_SERVICE_PATH) .collect::>() .join(":") diff --git a/src/test_env.rs b/src/test_env.rs index d35674ae9..bd8f9a543 100644 --- a/src/test_env.rs +++ b/src/test_env.rs @@ -1,24 +1,81 @@ -use std::sync::{Mutex, MutexGuard}; +use std::sync::{Condvar, Mutex}; -static ENV_MUTEX: Mutex<()> = Mutex::new(()); +use crate::env_vars; +use std::env; +use std::ffi::{OsStr, OsString}; +use std::path::Path; +use std::process::Command; -pub(crate) fn lock() -> MutexGuard<'static, ()> { - ENV_MUTEX - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) +static ENV_LEASE: Lease = Lease::new(); + +pub(crate) fn lock() -> LeaseGuard<'static> { + ENV_LEASE.acquire() +} + +/// Exclusive access to a process-wide test resource, such as the environment. +/// +/// A `MutexGuard` held across a test body is poisoned when that test panics, +/// and every later test would then either fail or reach inside the poisoned +/// lock. The lease's mutex only guards a flag in short sections that cannot +/// panic, and the guard hands the lease on while unwinding, so one failing +/// test does not fail the rest. +pub(crate) struct Lease { + held: Mutex, + released: Condvar, } -struct SavedEnv(Vec<(&'static str, Option)>); +/// Holds a [`Lease`] until dropped, including when the holder panics. +pub(crate) struct LeaseGuard<'lease> { + lease: &'lease Lease, +} + +impl Lease { + pub(crate) const fn new() -> Self { + Self { + held: Mutex::new(false), + released: Condvar::new(), + } + } + + pub(crate) fn acquire(&self) -> LeaseGuard<'_> { + let mut held = self + .held + .lock() + .expect("the lease flag is never locked across a panic"); + while *held { + held = self + .released + .wait(held) + .expect("the lease flag is never locked across a panic"); + } + *held = true; + + LeaseGuard { lease: self } + } +} + +impl Drop for LeaseGuard<'_> { + fn drop(&mut self) { + // Dropping must not panic while a failed test unwinds, and the flag's + // mutex is never poisoned, so a failed lock only skips the release. + if let Ok(mut held) = self.lease.held.lock() { + *held = false; + } + self.lease.released.notify_one(); + } +} + +struct SavedEnv(Vec<(&'static str, Option)>); impl Drop for SavedEnv { fn drop(&mut self) { for (key, value) in self.0.drain(..) { - // SAFETY: every caller creates this guard while holding - // `ENV_MUTEX`, and it is dropped before that mutex guard. + // SAFETY: every caller creates this guard while holding the + // environment lease, and it is dropped before that lease guard. unsafe { match value { - Some(value) => std::env::set_var(key, value), - None => std::env::remove_var(key), + Some(value) => env::set_var(key, value), + None => env::remove_var(key), } } } @@ -28,7 +85,7 @@ impl Drop for SavedEnv { #[cfg(test)] pub(crate) fn with_env_var( key: &'static str, - value: Option<&std::ffi::OsStr>, + value: Option<&OsStr>, body: impl FnOnce() -> T, ) -> T { with_env_vars(&[(key, value)], body) @@ -38,13 +95,13 @@ pub(crate) fn with_env_var( /// all of them afterwards, including when `body` panics. #[cfg(test)] pub(crate) fn with_env_vars( - vars: &[(&'static str, Option<&std::ffi::OsStr>)], + vars: &[(&'static str, Option<&OsStr>)], body: impl FnOnce() -> T, ) -> T { let _guard = lock(); let _saved = SavedEnv( vars.iter() - .map(|(key, _)| (*key, std::env::var_os(key))) + .map(|(key, _)| (*key, env::var_os(key))) .collect(), ); @@ -52,8 +109,8 @@ pub(crate) fn with_env_vars( // SAFETY: serialized by the environment lock held above. unsafe { match value { - Some(value) => std::env::set_var(key, value), - None => std::env::remove_var(key), + Some(value) => env::set_var(key, value), + None => env::remove_var(key), } } } @@ -70,13 +127,13 @@ pub(crate) fn with_env_vars( #[cfg(test)] pub(crate) fn with_scrubbed_desktop_env(body: impl FnOnce() -> T) -> T { let _guard = lock(); - let saved: Vec<(&'static str, Option)> = crate::env_vars::DESKTOP_ENV_KEYS + let saved: Vec<(&'static str, Option)> = env_vars::DESKTOP_ENV_KEYS .iter() - .map(|key| (*key, std::env::var_os(key))) + .map(|key| (*key, env::var_os(key))) .collect(); for (key, _) in &saved { // SAFETY: serialized by the environment lock held above. - unsafe { std::env::remove_var(key) }; + unsafe { env::remove_var(key) }; } let _saved = SavedEnv(saved); body() @@ -99,12 +156,12 @@ pub(crate) fn in_language_isolated_child(test_path: &str, body: impl FnOnce()) { /// override directory. pub(crate) fn in_language_isolated_child_with_overrides( test_path: &str, - locale_dir: &std::path::Path, + locale_dir: &Path, body: impl FnOnce(), ) { const CHILD_ENV: &str = "WAYSCRIBER_TEST_LANGUAGE_ISOLATED_CHILD"; - if std::env::var_os(CHILD_ENV).is_some() { + if env::var_os(CHILD_ENV).is_some() { body(); return; } @@ -112,13 +169,13 @@ pub(crate) fn in_language_isolated_child_with_overrides( let test_name = test_path .strip_prefix(concat!(env!("CARGO_CRATE_NAME"), "::")) .expect("test path starts with the crate name"); - let mut command = std::process::Command::new(std::env::current_exe().expect("test binary")); + let mut command = Command::new(env::current_exe().expect("test binary")); command .args([test_name, "--exact", "--test-threads=1"]) .env(CHILD_ENV, "1") - .env(crate::env_vars::LOCALE_DIR_ENV, locale_dir) - .env_remove(crate::env_vars::LANGUAGE_OVERRIDE_ENV); - for key in crate::env_vars::LOCALE_ENV_KEYS { + .env(env_vars::LOCALE_DIR_ENV, locale_dir) + .env_remove(env_vars::LANGUAGE_OVERRIDE_ENV); + for key in env_vars::LOCALE_ENV_KEYS { command.env_remove(key); } @@ -139,24 +196,28 @@ pub(crate) fn in_language_isolated_child_with_overrides( #[cfg(test)] mod tests { + use std::ffi::OsStr; + use std::{env, thread}; + #[test] fn environment_is_restored_when_the_body_panics() { const KEY: &str = "WAYSCRIBER_TEST_ENV_PANIC_RESTORE"; // SAFETY: this key is private to this test. - unsafe { std::env::set_var(KEY, "before") }; - - let result = std::panic::catch_unwind(|| { - super::with_env_var(KEY, Some(std::ffi::OsStr::new("during")), || { - panic!("exercise unwind restoration"); - }); + unsafe { env::set_var(KEY, "before") }; + + let result = thread::scope(|scope| { + scope + .spawn(|| { + super::with_env_var(KEY, Some(OsStr::new("during")), || { + panic!("exercise unwind restoration"); + }); + }) + .join() }); assert!(result.is_err()); - assert_eq!( - std::env::var_os(KEY).as_deref(), - Some(std::ffi::OsStr::new("before")) - ); + assert_eq!(env::var_os(KEY).as_deref(), Some(OsStr::new("before"))); // SAFETY: this key is private to this test. - unsafe { std::env::remove_var(KEY) }; + unsafe { env::remove_var(KEY) }; } } diff --git a/src/test_fake_helper.rs b/src/test_fake_helper.rs index 20082f0d4..6eac2008e 100644 --- a/src/test_fake_helper.rs +++ b/src/test_fake_helper.rs @@ -11,10 +11,15 @@ use std::ffi::{OsStr, OsString}; use std::io::Read; use std::os::unix::ffi::{OsStrExt, OsStringExt}; +use std::os::unix::fs as unix_fs; use std::path::{Path, PathBuf}; +use std::{env, fs, io, process}; use anyhow::{Context, Result}; +use crate::durable_io; +use crate::durable_io::AtomicWriteOptions; + /// The path a helper start must have been launched under to play a role. const LINK_ENV: &str = "WAYSCRIBER_TEST_FAKE_HELPER"; const ROLE_ENV: &str = "WAYSCRIBER_TEST_FAKE_HELPER_ROLE"; @@ -55,7 +60,7 @@ impl Role { /// A link named `name` in `directory` to this test binary. pub(crate) fn link(directory: &Path, name: &str) -> PathBuf { let link = directory.join(name); - std::os::unix::fs::symlink(std::env::current_exe().expect("test binary"), &link) + unix_fs::symlink(env::current_exe().expect("test binary"), &link) .expect("link the test binary under the helper's name"); link } @@ -75,19 +80,20 @@ pub(crate) fn environment<'a>( ] } -// SAFETY: the loader calls each `.init_array` entry once, before `main`, with -// the C calling convention. glibc passes `argc`, `argv`, and `envp`; under the -// C ABI a function that declares no parameters ignores extra arguments, so an -// `extern "C" fn()` is sound to register here. Unless this start is a helper -// start, the function only reads the environment and `/proc` and returns. A -// helper start never returns into `main`: it exits, and a panic aborts instead -// of unwinding through the loader because the function is `extern "C"`. +// The `unsafe(link_section)` attribute below is sound: the loader calls each +// `.init_array` entry once, before `main`, with the C calling convention. +// glibc passes `argc`, `argv`, and `envp`; under the C ABI a function that +// declares no parameters ignores extra arguments, so an `extern "C" fn()` is +// sound to register here. Unless this start is a helper start, the function +// only reads the environment and `/proc` and returns. A helper start never +// returns into `main`: it exits, and a panic aborts instead of unwinding +// through the loader because the function is `extern "C"`. #[used] #[unsafe(link_section = ".init_array")] static PLAY_FAKE_HELPER: extern "C" fn() = play_fake_helper; extern "C" fn play_fake_helper() { - let Some(link) = std::env::var_os(LINK_ENV) else { + let Some(link) = env::var_os(LINK_ENV) else { return; }; let Ok(mut arguments) = launch_arguments() else { @@ -104,22 +110,22 @@ extern "C" fn play_fake_helper() { 1 } }; - std::process::exit(status); + process::exit(status); } fn play(arguments: &[OsString]) -> Result<()> { - let role = std::env::var(ROLE_ENV).context("fake helper role")?; + let role = env::var(ROLE_ENV).context("fake helper role")?; let role = Role::ALL .into_iter() .find(|known| known.name() == role) .with_context(|| format!("unknown fake helper role {role:?}"))?; - let output = std::env::var_os(OUTPUT_ENV).context("fake helper output")?; + let output = env::var_os(OUTPUT_ENV).context("fake helper output")?; let report: Vec = match role { Role::Exit => return Ok(()), Role::ReportProcessGroup => { // SAFETY: getpgrp has no preconditions and cannot fail. - let leader = unsafe { libc::getpgrp() } == std::process::id() as libc::pid_t; + let leader = unsafe { libc::getpgrp() } == process::id() as libc::pid_t; if leader { "process-group-leader" } else { @@ -129,7 +135,7 @@ fn play(arguments: &[OsString]) -> Result<()> { } Role::CountInput => { let mut input = Vec::new(); - std::io::stdin() + io::stdin() .read_to_end(&mut input) .context("read the helper's input")?; format!("{}\n", input.len()).into() @@ -140,10 +146,10 @@ fn play(arguments: &[OsString]) -> Result<()> { .collect(), }; - crate::durable_io::write_atomic( + durable_io::write_atomic( Path::new(&output), &report, - crate::durable_io::AtomicWriteOptions::private_runtime_file(), + AtomicWriteOptions::private_runtime_file(), )?; Ok(()) } @@ -153,7 +159,7 @@ fn play(arguments: &[OsString]) -> Result<()> { /// run yet. They are kept as bytes, so an argument that is not UTF-8 cannot /// stop a helper start from recognising itself. pub(crate) fn launch_arguments() -> Result> { - let raw = std::fs::read("/proc/self/cmdline").context("read /proc/self/cmdline")?; + let raw = fs::read("/proc/self/cmdline").context("read /proc/self/cmdline")?; let raw = raw.strip_suffix(b"\0").unwrap_or(&raw); Ok(raw diff --git a/src/time_utils.rs b/src/time_utils.rs index 2fd4dcfa2..2644e4da0 100644 --- a/src/time_utils.rs +++ b/src/time_utils.rs @@ -1,3 +1,4 @@ +use std::mem::MaybeUninit; use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -21,6 +22,7 @@ pub fn now_local() -> LocalDateTime { /// /// Supported directives: `%Y`, `%m`, `%d`, `%H`, `%M`, `%S`, and `%%`. /// Unknown directives are emitted literally (e.g., `%q` becomes `%q`). +#[must_use] pub fn format_with_template(dt: LocalDateTime, template: &str) -> String { let mut out = String::with_capacity(template.len() + 8); let mut chars = template.chars(); @@ -57,6 +59,7 @@ pub fn format_system_time(time: SystemTime, template: &str) -> Option { } /// Format a UNIX timestamp in milliseconds using the strftime-like subset. +#[must_use] pub fn format_unix_millis(ms: u64, template: &str) -> Option { const MAX_FORMAT_UNIX_MILLIS: u64 = 253_402_300_799_999; // 9999-12-31T23:59:59Z if ms > MAX_FORMAT_UNIX_MILLIS { @@ -155,7 +158,7 @@ fn utc_datetime_from_unix(secs: i64) -> Option { #[cfg(unix)] fn libc_datetime_from_unix(secs: i64, local: bool) -> Option { let raw = unix_seconds_to_time_t(secs)?; - let mut out = std::mem::MaybeUninit::::uninit(); + let mut out = MaybeUninit::::uninit(); // SAFETY: `raw` is a valid time_t and `out` points to writable storage for // one `tm`. The reentrant variants keep no pointer to either. let ptr = unsafe { diff --git a/src/toolbar_gtk/bridge.rs b/src/toolbar_gtk/bridge.rs index b5a9b532b..cc88118f4 100644 --- a/src/toolbar_gtk/bridge.rs +++ b/src/toolbar_gtk/bridge.rs @@ -2,9 +2,11 @@ use std::collections::VecDeque; use std::sync::atomic::{AtomicU8, Ordering}; -use std::sync::{Arc, Mutex}; -use std::thread::JoinHandle; +use std::sync::mpsc::{Receiver, Sender}; +use std::sync::{Arc, Mutex, mpsc}; +use std::thread::{Builder, JoinHandle}; use std::time::{Duration, Instant}; +use std::{io, thread}; use tokio::sync::watch; use crate::backend::wayland::RuntimeWakeHandle; @@ -108,7 +110,7 @@ impl BridgeHealth { self.status.load(Ordering::Acquire) == STATUS_FAILED } - fn wake_owner(&self) -> std::io::Result<()> { + fn wake_owner(&self) -> io::Result<()> { self.runtime_wake.wake() } } @@ -150,17 +152,14 @@ impl FeedbackPublisher { pub(super) fn publish(&self, feedback: GtkToolbarFeedback) -> Result<(), FeedbackPublishError> { let mut overflowed = false; { - let mut state = match self.mailbox.state.lock() { - Ok(state) => state, - Err(poisoned) => { - let mut state = poisoned.into_inner(); - state.accepting = false; - drop(state); - self.mailbox.health.fail( - "GTK feedback mailbox state was poisoned; restoring built-in toolbars", - ); - return Err(FeedbackPublishError::Failed); - } + // A poisoned mailbox may hold a half-updated queue, so it is + // never read again: every later lock fails the same way, which + // also closes admission. + let Ok(mut state) = self.mailbox.state.lock() else { + self.mailbox + .health + .fail("GTK feedback mailbox state was poisoned; restoring built-in toolbars"); + return Err(FeedbackPublishError::Failed); }; if !state.accepting { return Err(if self.mailbox.health.failed() { @@ -248,17 +247,14 @@ impl FeedbackPublisher { fn drain(&self, limit: usize) -> Vec { let (drained, residual) = { - let mut state = match self.mailbox.state.lock() { - Ok(state) => state, - Err(poisoned) => { - let mut state = poisoned.into_inner(); - state.accepting = false; - drop(state); - self.mailbox.health.fail( - "GTK feedback mailbox state was poisoned; restoring built-in toolbars", - ); - return Vec::new(); - } + // A poisoned mailbox may hold a half-updated queue, so it is + // never read again: every later lock fails the same way, which + // also closes admission. + let Ok(mut state) = self.mailbox.state.lock() else { + self.mailbox + .health + .fail("GTK feedback mailbox state was poisoned; restoring built-in toolbars"); + return Vec::new(); }; let take = limit.min(state.queue.len()); let drained = state.queue.drain(..take).collect::>(); @@ -290,9 +286,9 @@ impl FeedbackPublisher { } fn close_admission(&self) { - match self.mailbox.state.lock() { - Ok(mut state) => state.accepting = false, - Err(poisoned) => poisoned.into_inner().accepting = false, + // A poisoned mailbox already refuses every publish. + if let Ok(mut state) = self.mailbox.state.lock() { + state.accepting = false; } } @@ -301,7 +297,7 @@ impl FeedbackPublisher { self.mailbox .state .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) + .expect("feedback mailbox state") .queue .len() } @@ -401,7 +397,7 @@ impl LatestValueReceiver { } } -struct ThreadCompletion(std::sync::mpsc::Sender<()>); +struct ThreadCompletion(Sender<()>); impl Drop for ThreadCompletion { fn drop(&mut self) { @@ -418,7 +414,7 @@ enum ThreadShutdownOutcome { fn finish_thread_within( thread: &mut Option>, - completion: &std::sync::mpsc::Receiver<()>, + completion: &Receiver<()>, timeout: Duration, ) -> ThreadShutdownOutcome { let Some(handle) = thread.as_ref() else { @@ -428,7 +424,7 @@ fn finish_thread_within( let completion_wait = deadline.saturating_duration_since(Instant::now()); let _ = completion.recv_timeout(completion_wait); while !handle.is_finished() && Instant::now() < deadline { - std::thread::yield_now(); + thread::yield_now(); } if !handle.is_finished() { // Bounded shutdown: drop the JoinHandle to detach. GTK may keep @@ -452,7 +448,7 @@ pub struct GtkToolbarBridge { feedback: FeedbackPublisher, health: BridgeHealth, thread: Option>, - completion: std::sync::mpsc::Receiver<()>, + completion: Receiver<()>, } impl GtkToolbarBridge { @@ -460,17 +456,15 @@ impl GtkToolbarBridge { /// created; GTK-level failures are published asynchronously and wake the runtime. pub fn spawn(runtime_wake: RuntimeWakeHandle) -> Option { let (updates, update_rx) = latest_value_channel(); - let (completion_tx, completion_rx) = std::sync::mpsc::channel(); + let (completion_tx, completion_rx) = mpsc::channel(); let health = BridgeHealth::new(runtime_wake); let feedback = FeedbackPublisher::new(health.clone()); let thread_health = health.clone(); let thread_feedback = feedback.clone(); - let spawned = std::thread::Builder::new() - .name("gtk-toolbar".into()) - .spawn(move || { - let _completion = ThreadCompletion(completion_tx); - super::runtime::run(update_rx, thread_feedback, thread_health); - }); + let spawned = Builder::new().name("gtk-toolbar".into()).spawn(move || { + let _completion = ThreadCompletion(completion_tx); + super::runtime::run(update_rx, thread_feedback, &thread_health); + }); match spawned { Ok(thread) => Some(Self { updates, @@ -532,6 +526,8 @@ impl Drop for GtkToolbarBridge { #[cfg(test)] mod tests { + use std::fs; + use glib::MainContext; use std::os::fd::AsRawFd; use std::sync::mpsc; @@ -569,7 +565,7 @@ mod tests { } fn channel() -> (RuntimeWakeSource, BridgeHealth, FeedbackPublisher) { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let health = BridgeHealth::new(wake.handle()); let publisher = FeedbackPublisher::new(health.clone()); (wake, health, publisher) @@ -591,7 +587,7 @@ mod tests { let stat_path = format!("/proc/self/task/{tid}/stat"); let deadline = Instant::now() + Duration::from_secs(1); loop { - let state = std::fs::read_to_string(&stat_path).ok().and_then(|stat| { + let state = fs::read_to_string(&stat_path).ok().and_then(|stat| { stat.rsplit_once(") ") .and_then(|(_, suffix)| suffix.chars().next()) }); @@ -602,7 +598,7 @@ mod tests { Instant::now() < deadline, "polling thread did not block (last state: {state:?})" ); - std::thread::yield_now(); + thread::yield_now(); } } @@ -740,7 +736,7 @@ mod tests { } publisher.publish(event()).unwrap(); let kept_move = drag(GtkToolbarKind::Top, GtkToolbarDragPhase::Move, 1); - publisher.publish(kept_move.clone()).unwrap(); + publisher.publish(kept_move).unwrap(); let latest_top = drag(GtkToolbarKind::Top, GtkToolbarDragPhase::Move, 999); publisher.publish(latest_top.clone()).unwrap(); @@ -819,7 +815,7 @@ mod tests { fn feedback_unblocks_a_runtime_already_waiting_in_poll() { let (wake, _health, publisher) = channel(); let (tid_tx, tid_rx) = mpsc::channel(); - let poller = std::thread::spawn(move || { + let poller = thread::spawn(move || { // SAFETY: gettid has no preconditions and is used only to observe this test thread. let tid = unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t }; tid_tx.send(tid).unwrap(); @@ -865,7 +861,7 @@ mod tests { #[test] fn completed_thread_is_joined_without_initializing_gtk() { let (completion_tx, completion_rx) = mpsc::channel(); - let mut thread = Some(std::thread::spawn(move || { + let mut thread = Some(thread::spawn(move || { let _completion = ThreadCompletion(completion_tx); })); assert_eq!( @@ -878,7 +874,7 @@ mod tests { #[test] fn panicked_thread_is_joined_with_panicked_outcome() { let (completion_tx, completion_rx) = mpsc::channel(); - let mut thread = Some(std::thread::spawn(move || { + let mut thread = Some(thread::spawn(move || { let _completion = ThreadCompletion(completion_tx); panic!("intentional GTK bridge shutdown test panic"); })); @@ -893,7 +889,7 @@ mod tests { fn stuck_thread_is_detached_at_the_bounded_deadline() { let (completion_tx, completion_rx) = mpsc::channel(); let (release_tx, release_rx) = mpsc::channel(); - let mut thread = Some(std::thread::spawn(move || { + let mut thread = Some(thread::spawn(move || { let _completion = ThreadCompletion(completion_tx); let _ = release_rx.recv(); })); diff --git a/src/toolbar_gtk/css.rs b/src/toolbar_gtk/css.rs index 25fbc2cf7..caafd7d2a 100644 --- a/src/toolbar_gtk/css.rs +++ b/src/toolbar_gtk/css.rs @@ -10,6 +10,7 @@ use crate::ui::theme::css::GtkStylesheetValues; // Tokens the GTK widgets also paint directly (swatch fills and the slider // DrawingArea); re-exported under their pre-M1 local names. +use crate::ui::theme::css; pub(super) use crate::ui::theme::toolbar::{ COLOR_ACCENT as ACCENT, COLOR_TRACK_BACKGROUND as TRACK_BACKGROUND, COLOR_TRACK_KNOB as TRACK_KNOB, @@ -381,8 +382,8 @@ tooltip {{ scrollbar_slider = v.scrollbar_slider, track = v.track, track_hover = v.track_hover, - meter_bar_h = crate::ui::theme::css::scaled_px(8.0, scale), - meter_bar_radius = crate::ui::theme::css::scaled_px(4.0, scale), + meter_bar_h = css::scaled_px(8.0, scale), + meter_bar_radius = css::scaled_px(4.0, scale), radius_panel = v.radius_panel, radius_button = v.radius_button, radius_sm = v.radius_sm, @@ -495,6 +496,8 @@ tooltip.{class} {{ #[cfg(test)] mod tests { + use std::collections::HashSet; + use super::*; use crate::ui::theme::css::{hex_css, rgba_css, rgba_css_compact, transparent_css}; use crate::ui::theme::toolbar as t; @@ -641,7 +644,7 @@ mod tests { #[test] fn every_theme_token_color_appears_in_the_stylesheet() { - let css = stylesheet(1.0) + &capture_stylesheet(1.0); + let css = format!("{}{}", stylesheet(1.0), capture_stylesheet(1.0)); for (token, value) in emitted_colors() { assert!( css.contains(&value), @@ -652,8 +655,8 @@ mod tests { #[test] fn stylesheet_has_no_color_literal_outside_the_theme_tokens() { - let css = stylesheet(1.0) + &capture_stylesheet(1.0); - let known: std::collections::HashSet = emitted_colors() + let css = format!("{}{}", stylesheet(1.0), capture_stylesheet(1.0)); + let known: HashSet = emitted_colors() .into_iter() .map(|(_, value)| value) .collect(); diff --git a/src/toolbar_gtk/icons.rs b/src/toolbar_gtk/icons.rs index 1a7d9c69f..77a49f5df 100644 --- a/src/toolbar_gtk/icons.rs +++ b/src/toolbar_gtk/icons.rs @@ -8,6 +8,7 @@ //! recolor them for free. use std::cell::Cell; +use std::ptr; use std::rc::Rc; use gtk4::prelude::*; @@ -57,7 +58,7 @@ impl IconWidget { pub(super) fn set_painter(&self, painter: IconPainter) { // Address equality is only an optimization: merged functions draw // identical output, so a false match never skips a needed redraw. - if !std::ptr::fn_addr_eq(self.painter.get(), painter) { + if !ptr::fn_addr_eq(self.painter.get(), painter) { self.painter.set(painter); self.area.queue_draw(); } diff --git a/src/toolbar_gtk/mod.rs b/src/toolbar_gtk/mod.rs index 61b642438..091a98be9 100644 --- a/src/toolbar_gtk/mod.rs +++ b/src/toolbar_gtk/mod.rs @@ -31,7 +31,12 @@ mod view; #[cfg(feature = "toolbar-gtk")] mod widgets; +#[cfg(feature = "toolbar-gtk")] +use std::env; + use crate::config::ToolbarRebindModifier; +#[cfg(feature = "toolbar-gtk")] +use crate::env_vars; use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; #[cfg(feature = "toolbar-gtk")] @@ -40,7 +45,7 @@ pub(crate) fn drag_debug_log(message: impl AsRef) { static ENABLED: OnceLock = OnceLock::new(); let enabled = *ENABLED.get_or_init(|| { - let raw = std::env::var(crate::env_vars::DEBUG_TOOLBAR_DRAG_ENV) + let raw = env::var(env_vars::DEBUG_TOOLBAR_DRAG_ENV) .unwrap_or_default() .to_ascii_lowercase(); !(raw.is_empty() || raw == "0" || raw == "false" || raw == "off") diff --git a/src/toolbar_gtk/runtime.rs b/src/toolbar_gtk/runtime.rs index 1bbdb2b1a..87bff301f 100644 --- a/src/toolbar_gtk/runtime.rs +++ b/src/toolbar_gtk/runtime.rs @@ -2,6 +2,9 @@ //! toolbar windows. Nothing in here touches backend state; all traffic //! goes through the bridge channels. +use std::cell::Cell; +use std::rc::Rc; + use gtk4::glib; use std::future::Future; @@ -13,7 +16,7 @@ use super::bridge::{BridgeHealth, FeedbackPublisher, LatestValueReceiver}; /// panics and an unexpectedly exiting main loop restore the built-in bars. struct FailureGuard { health: BridgeHealth, - clean: std::cell::Cell, + clean: Cell, } fn spawn_monitored_local(context: &glib::MainContext, future: F, completed: C) @@ -42,11 +45,11 @@ impl Drop for FailureGuard { pub(super) fn run( mut updates: LatestValueReceiver, feedback: FeedbackPublisher, - health: BridgeHealth, + health: &BridgeHealth, ) { - let guard = std::rc::Rc::new(FailureGuard { + let guard = Rc::new(FailureGuard { health: health.clone(), - clean: std::cell::Cell::new(false), + clean: Cell::new(false), }); if health.stopping() { @@ -130,11 +133,11 @@ mod tests { #[test] fn unexpected_runtime_exit_publishes_failure_before_wake() { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let health = BridgeHealth::new(wake.handle()); drop(FailureGuard { health: health.clone(), - clean: std::cell::Cell::new(false), + clean: Cell::new(false), }); let mut pollfd = libc::pollfd { @@ -151,11 +154,11 @@ mod tests { #[test] fn clean_runtime_exit_does_not_publish_failure() { - let wake = RuntimeWakeSource::new().unwrap(); + let wake = RuntimeWakeSource::try_new().unwrap(); let health = BridgeHealth::new(wake.handle()); drop(FailureGuard { health: health.clone(), - clean: std::cell::Cell::new(true), + clean: Cell::new(true), }); assert!(!health.failed()); diff --git a/src/toolbar_gtk/select.rs b/src/toolbar_gtk/select.rs index 07d1e94dc..7f5712625 100644 --- a/src/toolbar_gtk/select.rs +++ b/src/toolbar_gtk/select.rs @@ -6,6 +6,7 @@ //! inline fallback (overlay-layer canvas on niri/sway, forced inline) //! would cover separate GTK surfaces just the same. +use std::env; use std::sync::OnceLock; use crate::config::{Config, ToolbarBackendKind}; @@ -97,7 +98,7 @@ fn parse_backend_env(raw: &str) -> Option { fn backend_env_override() -> Option { static OVERRIDE: OnceLock> = OnceLock::new(); *OVERRIDE.get_or_init(|| { - std::env::var(TOOLBAR_BACKEND_ENV) + env::var(TOOLBAR_BACKEND_ENV) .ok() .and_then(|raw| parse_backend_env(&raw)) }) diff --git a/src/toolbar_gtk/view/capture_proof.rs b/src/toolbar_gtk/view/capture_proof.rs index 43c793c04..ad4177735 100644 --- a/src/toolbar_gtk/view/capture_proof.rs +++ b/src/toolbar_gtk/view/capture_proof.rs @@ -1,10 +1,9 @@ use std::cell::{Cell, RefCell}; use std::rc::Rc; -use gtk4::gdk; -use gtk4::glib; use gtk4::prelude::*; use gtk4::subclass::prelude::*; +use gtk4::{gdk, glib}; #[derive(Clone, Copy)] pub(super) struct ProofRender { @@ -15,7 +14,12 @@ pub(super) struct ProofRender { pub(super) type ProofObservation = Rc>>; mod imp { - use super::*; + use gdk::subclass::prelude::PaintableImpl; + + use super::{ + Cell, ObjectImpl, ObjectSubclass, PaintableExt, ProofObservation, ProofRender, RefCell, + WidgetExt, gdk, glib, + }; #[derive(Default)] pub struct CaptureProof { @@ -34,7 +38,7 @@ mod imp { impl ObjectImpl for CaptureProof {} - impl gdk::subclass::prelude::PaintableImpl for CaptureProof { + impl PaintableImpl for CaptureProof { fn snapshot(&self, snapshot: &gdk::Snapshot, width: f64, height: f64) { let texture = self.texture.borrow(); let Some(texture) = texture.as_ref() else { diff --git a/src/toolbar_gtk/view/capture_suppression.rs b/src/toolbar_gtk/view/capture_suppression.rs index e9b9c17bf..60f65709e 100644 --- a/src/toolbar_gtk/view/capture_suppression.rs +++ b/src/toolbar_gtk/view/capture_suppression.rs @@ -2,9 +2,13 @@ mod native_popovers; mod quiescence; use std::cell::{Cell, RefCell}; +use std::future::Future; use std::rc::Rc; use std::time::{Duration, Instant}; +use glib::WeakRef; +use gtk4::cairo::Region; +use gtk4::glib; use gtk4::prelude::*; use super::{CaptureProofTarget, CaptureSurfaceContent, widget_native_is_mapped}; @@ -38,8 +42,8 @@ pub(super) struct TooltipCapture { struct TooltipCaptureInner { content: CaptureSurfaceContent, label: gtk4::Label, - installed_sources: RefCell>>, - active_source: RefCell>>, + installed_sources: RefCell>>, + active_source: RefCell>>, native_popovers: NativePopoverCapture, suppressed: Cell, popup_input_enabled: Cell, @@ -101,7 +105,7 @@ impl TooltipCapture { installed.retain(|source| source.upgrade().is_some()); installed .iter() - .filter_map(gtk4::glib::WeakRef::upgrade) + .filter_map(WeakRef::upgrade) .any(|candidate| candidate == *source) }; if already_installed { @@ -153,7 +157,7 @@ impl TooltipCapture { .active_source .borrow() .as_ref() - .and_then(gtk4::glib::WeakRef::upgrade) + .and_then(WeakRef::upgrade) .is_some_and(|active| active == *source) } @@ -225,7 +229,7 @@ impl TooltipCapture { ) -> Result<(), String> where F: FnMut(Vec, Instant) -> Fut, - Fut: std::future::Future>, + Fut: Future>, { quiescence::wait_for_popover_quiescence( generation, @@ -287,7 +291,7 @@ fn set_native_widget_input_enabled(widget: >k4::Widget, enabled: bool) { if enabled { surface.set_input_region(None); } else { - let empty = gtk4::cairo::Region::create(); + let empty = Region::create(); surface.set_input_region(Some(&empty)); } } @@ -350,7 +354,7 @@ pub(super) async fn wait_for_presented_transparency_until( "GTK capture suppression generation {generation} timed out waiting for normally visible toolbar surfaces to map ({states})" )); } - gtk4::glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; + glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; } // Popup surfaces inherit their parent's frame clock. Queueing every proof @@ -416,7 +420,7 @@ async fn wait_for_surface_presentation( "GTK capture suppression generation {generation} timed out waiting for a dedicated transparent render from {name}" )); } - gtk4::glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; + glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; } let Some(frame_counter) = target.content.proof_frame() else { @@ -456,7 +460,7 @@ async fn wait_for_surface_presentation( proof_started.elapsed().as_millis() )); } - gtk4::glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; + glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; } } @@ -491,6 +495,12 @@ mod tests { mod process; mod wayland_proxy; + use std::env; + + use glib::{MainContext, Object}; + use gtk4::gio::MenuModel; + use gtk4::prelude::WidgetExt; + use super::*; #[test] @@ -518,7 +528,7 @@ mod tests { const CHILD_ENV: &str = "WAYSCRIBER_GTK_POPUP_CAPTURE_CHILD"; const TEST_NAME: &str = "toolbar_gtk::view::capture_suppression::tests::capture_aware_gtk_popups_use_transparent_proof_surfaces"; - if std::env::var_os(CHILD_ENV).is_none() { + if env::var_os(CHILD_ENV).is_none() { let status = process::run_isolated_gtk_test(CHILD_ENV, TEST_NAME); assert!(status.success(), "isolated GTK popup capture test failed"); return; @@ -558,7 +568,7 @@ mod tests { context_popover: gtk4::Popover, context_menu: gtk4::PopoverMenu, original_context_child: gtk4::Widget, - original_context_model: gtk4::gio::MenuModel, + original_context_model: MenuModel, } fn gtk_popup_fixture() -> GtkPopupFixture { @@ -567,7 +577,7 @@ mod tests { window.set_child(Some(&root)); // GtkText creates its private popup against the nearest native. Realize // that native for the test without presenting or focusing a window. - gtk4::prelude::WidgetExt::realize(&window); + WidgetExt::realize(&window); let button = gtk4::Button::new(); button.set_tooltip_text(Some("Zoom")); root.append(&button); @@ -607,7 +617,7 @@ mod tests { capture.install_tree(fixture.root.upcast_ref()); assert_eq!(capture.installed_source_count(), 1); - let tooltip = gtk4::glib::Object::builder::().build(); + let tooltip = Object::builder::().build(); assert!( fixture .button @@ -622,7 +632,7 @@ mod tests { // and map the real GtkText-owned surface for this structural test. The // parent window remains hidden throughout. capture.enroll_native_popover(&fixture.context_popover, true); - gtk4::prelude::WidgetExt::map(&fixture.context_popover); + WidgetExt::map(&fixture.context_popover); assert_eq!( fixture.context_popover.child().as_ref(), Some(&fixture.original_context_child), @@ -647,9 +657,9 @@ mod tests { } fn assert_context_popover_remap(capture: &TooltipCapture, fixture: &GtkPopupFixture) { - gtk4::prelude::WidgetExt::unmap(&fixture.context_popover); + WidgetExt::unmap(&fixture.context_popover); assert!(capture.capture_popover_targets().is_empty()); - gtk4::prelude::WidgetExt::map(&fixture.context_popover); + WidgetExt::map(&fixture.context_popover); assert_eq!(capture.pending_capture_popover_targets().len(), 1); assert_eq!( fixture.context_popover.child().as_ref(), @@ -743,7 +753,7 @@ mod tests { &capture.inner.content, move || withdrawn_callback.set(true), ); - let error = gtk4::glib::MainContext::default() + let error = MainContext::default() .block_on(wait_for_presented_transparency_until( 90, vec![closed_target], @@ -764,7 +774,7 @@ mod tests { let asynchronous_root = gtk4::Box::new(gtk4::Orientation::Vertical, 0); let asynchronous_window = gtk4::Window::new(); asynchronous_window.set_child(Some(&asynchronous_root)); - gtk4::prelude::WidgetExt::realize(&asynchronous_window); + WidgetExt::realize(&asynchronous_window); let asynchronous_entry = gtk4::Entry::new(); let second_asynchronous_entry = gtk4::Entry::new(); asynchronous_root.append(&asynchronous_entry); @@ -783,7 +793,7 @@ mod tests { let created_popovers = Rc::new(RefCell::new(Vec::<(gtk4::Popover, gtk4::Widget)>::new())); let created_popovers_callback = Rc::clone(&created_popovers); - gtk4::glib::timeout_add_local_once(Duration::from_millis(10), move || { + glib::timeout_add_local_once(Duration::from_millis(10), move || { created_popovers_callback .borrow_mut() .push(open_hidden_test_text_popover(&asynchronous_text)); @@ -792,8 +802,8 @@ mod tests { let proof_calls = Rc::new(Cell::new(0usize)); let proof_calls_callback = Rc::clone(&proof_calls); let second_created_popovers = Rc::clone(&created_popovers); - let roots = [asynchronous_root.clone().upcast::()]; - gtk4::glib::MainContext::default() + let roots = [asynchronous_root.upcast::()]; + MainContext::default() .block_on(asynchronous_capture.wait_for_popover_quiescence( 91, &roots, @@ -802,7 +812,7 @@ mod tests { if proof_calls.get() == 0 { let text = second_asynchronous_text.clone(); let popovers = Rc::clone(&second_created_popovers); - gtk4::glib::timeout_add_local_once(Duration::from_millis(10), move || { + glib::timeout_add_local_once(Duration::from_millis(10), move || { popovers .borrow_mut() .push(open_hidden_test_text_popover(&text)); @@ -814,7 +824,7 @@ mod tests { // Real presentation proof yields the GTK main context. // Mirror that so the second popup is created during // the first proof round, after its discovery scan. - gtk4::glib::timeout_future(Duration::from_millis(20)).await; + glib::timeout_future(Duration::from_millis(20)).await; proof_calls.set(proof_calls.get() + 1); Ok(()) } @@ -852,7 +862,7 @@ mod tests { // Keep the toplevel hidden so this test never foregrounds a window, // but mark the native popup mapped to exercise the production // visibility predicate and discovery path. - gtk4::prelude::WidgetExt::map(&popover); + WidgetExt::map(&popover); (popover, original_child) } } diff --git a/src/toolbar_gtk/view/capture_suppression/native_popovers.rs b/src/toolbar_gtk/view/capture_suppression/native_popovers.rs index 2e88d72ea..c05aa6f66 100644 --- a/src/toolbar_gtk/view/capture_suppression/native_popovers.rs +++ b/src/toolbar_gtk/view/capture_suppression/native_popovers.rs @@ -3,7 +3,9 @@ use std::cell::{Cell, RefCell}; use std::rc::Rc; +use gtk4::gio::{Menu, MenuItem, MenuModel}; use gtk4::glib::variant::ToVariant; +use gtk4::glib::{SignalHandlerId, WeakRef}; use gtk4::prelude::*; use super::set_native_widget_input_enabled; @@ -108,14 +110,14 @@ impl NativePopoverCapture { } struct NativePopoverSurface { - popover: gtk4::glib::WeakRef, + popover: WeakRef, content: NativePopoverContent, selected_for_capture: Rc>, proof_epoch: Rc>, proven_epoch: Cell, in_flight_epoch: Cell>, - map_handler: gtk4::glib::SignalHandlerId, - unmap_handler: gtk4::glib::SignalHandlerId, + map_handler: SignalHandlerId, + unmap_handler: SignalHandlerId, } impl NativePopoverSurface { @@ -269,8 +271,8 @@ enum NativePopoverContent { Wrapped(CaptureSurfaceContent), Menu { surface: CaptureSurfaceContent, - proof_model: gtk4::gio::Menu, - original_model: RefCell>, + proof_model: Menu, + original_model: RefCell>, original_child_size: RefCell>, original_scroll_layout: RefCell>, }, @@ -344,7 +346,7 @@ impl NativePopoverContent { let Ok(menu) = popover.clone().downcast::() else { return; }; - let proof_model_object = proof_model.clone().upcast::(); + let proof_model_object = proof_model.clone().upcast::(); if menu.menu_model().as_ref() != Some(&proof_model_object) { // Keep the class-owned child's allocation while replacing its // model. The proof slot must not resize the native popup, and the @@ -403,7 +405,7 @@ impl NativePopoverContent { let Ok(menu) = popover.clone().downcast::() else { return; }; - let proof_model_object = proof_model.clone().upcast::(); + let proof_model_object = proof_model.clone().upcast::(); if menu.menu_model().as_ref() == Some(&proof_model_object) { if surface.widget().parent().is_some() { if let Some(slot) = surface.widget().parent() { @@ -424,9 +426,9 @@ impl NativePopoverContent { } } -fn capture_menu_proof_model() -> gtk4::gio::Menu { - let model = gtk4::gio::Menu::new(); - let item = gtk4::gio::MenuItem::new(None, None); +fn capture_menu_proof_model() -> Menu { + let model = Menu::new(); + let item = MenuItem::new(None, None); item.set_attribute_value("custom", Some(&CAPTURE_MENU_PROOF_ID.to_variant())); model.append_item(&item); model diff --git a/src/toolbar_gtk/view/capture_suppression/quiescence.rs b/src/toolbar_gtk/view/capture_suppression/quiescence.rs index 571b866de..46beb402b 100644 --- a/src/toolbar_gtk/view/capture_suppression/quiescence.rs +++ b/src/toolbar_gtk/view/capture_suppression/quiescence.rs @@ -1,8 +1,12 @@ //! Bounded GTK-main-context settling for asynchronously created popovers. +use std::cmp; use std::future::Future; use std::time::{Duration, Instant}; +use glib::Priority; +use gtk4::glib; + use super::CaptureProofTarget; pub(super) const CAPTURE_BARRIER_TIMEOUT: Duration = Duration::from_secs(1); @@ -33,7 +37,7 @@ where // Run only after all default-priority sources that are already ready. // They may include an input callback that schedules GtkText's delayed // selection popup, so start the quiet clock after this checkpoint. - gtk4::glib::timeout_future_with_priority(gtk4::glib::Priority::LOW, Duration::ZERO).await; + glib::timeout_future_with_priority(Priority::LOW, Duration::ZERO).await; let mut quiet_until = Instant::now() + GTK_POPUP_QUIET_PERIOD; loop { @@ -67,16 +71,16 @@ where return Ok(()); } - let checkpoint_at = std::cmp::min(quiet_until, deadline); - gtk4::glib::timeout_future_with_priority( - gtk4::glib::Priority::LOW, + let checkpoint_at = cmp::min(quiet_until, deadline); + glib::timeout_future_with_priority( + Priority::LOW, checkpoint_at.saturating_duration_since(now), ) .await; } }; - match gtk4::glib::future_with_timeout(CAPTURE_BARRIER_TIMEOUT, settle).await { + match glib::future_with_timeout(CAPTURE_BARRIER_TIMEOUT, settle).await { Ok(result) => result, Err(_) => Err(quiescence_timeout(generation, started, rounds)), } diff --git a/src/toolbar_gtk/view/capture_suppression/tests/presentation.rs b/src/toolbar_gtk/view/capture_suppression/tests/presentation.rs index 98ea38062..e0fffb11e 100644 --- a/src/toolbar_gtk/view/capture_suppression/tests/presentation.rs +++ b/src/toolbar_gtk/view/capture_suppression/tests/presentation.rs @@ -1,3 +1,11 @@ +use std::{env, io}; + +use gtk4::gdk::FrameClockPhase; +use gtk4::gio::Menu; +use gtk4::glib; +use gtk4::glib::MainContext; +use gtk4::gsk::GLRenderer; + use super::wayland_proxy::{CONTROL_ENV, ProxyResponse, ProxyStatus, run_in_private_wayland}; use super::*; use std::future::{Future, poll_fn}; @@ -29,7 +37,7 @@ fn popup_capture_waits_for_its_own_render_after_a_shared_clock_paint() { popover.set_parent(&anchor); window.present(); - gtk4::glib::MainContext::default().block_on(async { + MainContext::default().block_on(async { wait_for_native_mapping(window.upcast_ref()).await; require_gl_renderer(&window); @@ -62,7 +70,7 @@ fn menu_capture_presents_its_empty_proof_and_restores_the_menu() { gtk4::init().expect("private GTK display"); let (window, anchor) = popup_parent(); - let model = gtk4::gio::Menu::new(); + let model = Menu::new(); model.append(Some("Copy"), Some("clipboard.copy")); let menu = gtk4::PopoverMenu::from_model(Some(&model)); menu.set_autohide(false); @@ -73,7 +81,7 @@ fn menu_capture_presents_its_empty_proof_and_restores_the_menu() { let capture = TooltipCapture::new(); window.present(); - gtk4::glib::MainContext::default().block_on(async { + MainContext::default().block_on(async { wait_for_native_mapping(window.upcast_ref()).await; require_gl_renderer(&window); @@ -105,10 +113,10 @@ fn menu_capture_presents_its_empty_proof_and_restores_the_menu() { } fn run_in_private_display(test_name: &str) -> bool { - if std::env::var_os(CHILD_ENV).is_some() { + if env::var_os(CHILD_ENV).is_some() { return true; } - if std::env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none() { + if env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none() { eprintln!("skipping native popup presentation test: run tools/test-gtk-widgets.sh"); return false; } @@ -117,8 +125,8 @@ fn run_in_private_display(test_name: &str) -> bool { .strip_prefix(concat!(env!("CARGO_CRATE_NAME"), "::")) .expect("test name contains the crate prefix"); let output = run_in_private_wayland(CHILD_ENV, test_name); - std::io::stdout().write_all(&output.stdout).unwrap(); - std::io::stderr().write_all(&output.stderr).unwrap(); + io::stdout().write_all(&output.stdout).unwrap(); + io::stderr().write_all(&output.stderr).unwrap(); assert!( output.status.success(), @@ -146,7 +154,7 @@ fn require_gl_renderer(window: >k4::Window) { window .renderer() .expect("window renderer") - .is::() + .is::() ); } @@ -161,7 +169,7 @@ async fn wait_for_native_mapping(widget: >k4::Widget) { Instant::now() < deadline, "native surface did not map with nonzero bounds" ); - gtk4::glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; + glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; } } @@ -173,7 +181,7 @@ async fn wait_for_initial_popup_callback() { "initial popup frame did not complete" ); - gtk4::glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; + glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; } } @@ -199,12 +207,12 @@ async fn assert_popup_proof(window: >k4::Window, target: CaptureProofTarget) { .await .is_pending() ); - clock.request_phase(gtk4::gdk::FrameClockPhase::PAINT); + clock.request_phase(FrameClockPhase::PAINT); let deadline = Instant::now() + Duration::from_millis(250); while shared_paint.get().is_none() { assert!(Instant::now() < deadline, "shared clock did not paint"); - gtk4::glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; + glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; } clock.disconnect(handler); @@ -237,12 +245,12 @@ async fn wait_for_held_popup_callback() { "popup never submitted the held callback" ); - gtk4::glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; + glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await; } } fn control(command: &str) -> ProxyStatus { - let path = std::env::var_os(CONTROL_ENV).expect("private proxy control"); + let path = env::var_os(CONTROL_ENV).expect("private proxy control"); let mut socket = UnixStream::connect(path).expect("connect proxy control"); socket.set_read_timeout(Some(FIXTURE_WAIT)).unwrap(); diff --git a/src/toolbar_gtk/view/capture_suppression/tests/process.rs b/src/toolbar_gtk/view/capture_suppression/tests/process.rs index 2437a0f20..b0e7726c7 100644 --- a/src/toolbar_gtk/view/capture_suppression/tests/process.rs +++ b/src/toolbar_gtk/view/capture_suppression/tests/process.rs @@ -1,5 +1,8 @@ -pub(super) fn run_isolated_gtk_test(child_env: &str, test_name: &str) -> std::process::ExitStatus { - std::process::Command::new(std::env::current_exe().expect("test binary")) +use std::env; +use std::process::{Command, ExitStatus}; + +pub(super) fn run_isolated_gtk_test(child_env: &str, test_name: &str) -> ExitStatus { + Command::new(env::current_exe().expect("test binary")) .arg(test_name) .arg("--exact") .arg("--test-threads=1") diff --git a/src/toolbar_gtk/view/capture_suppression/tests/wayland_proxy.rs b/src/toolbar_gtk/view/capture_suppression/tests/wayland_proxy.rs index 9622421cd..d01ce5897 100644 --- a/src/toolbar_gtk/view/capture_suppression/tests/wayland_proxy.rs +++ b/src/toolbar_gtk/view/capture_suppression/tests/wayland_proxy.rs @@ -17,6 +17,7 @@ use std::path::{Path, PathBuf}; use std::process::{Child, Command, ExitStatus, Output, Stdio}; use std::thread::JoinHandle; use std::time::{Duration, Instant}; +use std::{env, fs, io, thread}; /// Names the control socket for the GTK test running behind the proxy. pub(super) const CONTROL_ENV: &str = "WAYSCRIBER_GTK_WAYLAND_CONTROL"; @@ -123,11 +124,11 @@ impl Weston { while !is_socket(&self.socket) { let exited = self.process.try_wait().expect("poll Weston").is_some(); if exited || Instant::now() >= deadline { - let log = std::fs::read_to_string(log_path).unwrap_or_default(); + let log = fs::read_to_string(log_path).unwrap_or_default(); panic!("private Weston did not create its socket:\n{log}"); } - std::thread::sleep(POLL_INTERVAL); + thread::sleep(POLL_INTERVAL); } } } @@ -149,14 +150,14 @@ impl Drop for Weston { } fn is_socket(path: &Path) -> bool { - std::fs::metadata(path).is_ok_and(|metadata| metadata.file_type().is_socket()) + fs::metadata(path).is_ok_and(|metadata| metadata.file_type().is_socket()) } fn run_test_session(runtime: &Path, child_env: &str, test_name: &str) -> Output { let mut command = Command::new("dbus-run-session"); command .arg("--") - .arg(std::env::current_exe().expect("test binary")) + .arg(env::current_exe().expect("test binary")) .args([test_name, "--exact", "--test-threads=1", "--nocapture"]); private_environment(&mut command, runtime, PROXY_SOCKET) .env(child_env, "1") @@ -187,10 +188,10 @@ fn run_test_session(runtime: &Path, child_env: &str, test_name: &str) -> Output } } -fn start_session() -> std::io::Result<()> { +fn start_session() -> io::Result<()> { // SAFETY: setsid has no memory-safety preconditions. if unsafe { libc::setsid() } < 0 { - return Err(std::io::Error::last_os_error()); + return Err(io::Error::last_os_error()); } Ok(()) @@ -228,14 +229,14 @@ fn kill_session(leader: &Child) { fn drain(pipe: Option) -> JoinHandle> { let mut pipe = pipe.expect("piped test output"); - std::thread::spawn(move || { + thread::spawn(move || { let mut bytes = Vec::new(); pipe.read_to_end(&mut bytes).expect("read test output"); bytes }) } -fn wait_until(process: &mut Child, timeout: Duration) -> std::io::Result> { +fn wait_until(process: &mut Child, timeout: Duration) -> io::Result> { let deadline = Instant::now() + timeout; loop { if let Some(status) = process.try_wait()? { @@ -245,6 +246,6 @@ fn wait_until(process: &mut Child, timeout: Duration) -> std::io::Result Schema { XdgWmBase::interface(), ]; while let Some(interface) = pending.pop() { - if std::ptr::eq(interface, &ANONYMOUS_INTERFACE) + if ptr::eq(interface, &ANONYMOUS_INTERFACE) || schema.insert(interface.name, interface).is_some() { continue; @@ -125,8 +130,8 @@ pub(super) fn decode(message: &MessageDesc, payload: &[u8]) -> Result Ok(arguments) } -struct Payload<'a> { - bytes: &'a [u8], +struct Payload<'bytes> { + bytes: &'bytes [u8], offset: usize, } diff --git a/src/toolbar_gtk/view/capture_suppression/tests/wayland_proxy/relay.rs b/src/toolbar_gtk/view/capture_suppression/tests/wayland_proxy/relay.rs index b2ababdbc..e477e0857 100644 --- a/src/toolbar_gtk/view/capture_suppression/tests/wayland_proxy/relay.rs +++ b/src/toolbar_gtk/view/capture_suppression/tests/wayland_proxy/relay.rs @@ -7,8 +7,9 @@ use std::net::Shutdown; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; +use std::sync::{Arc, Mutex, MutexGuard}; use std::thread::JoinHandle; +use std::{mem, thread}; use anyhow::{Context, Result}; @@ -46,9 +47,9 @@ impl Proxy { let commands = UnixListener::bind(&control_path)?; let accept_shared = Arc::clone(&shared); - let accept = std::thread::spawn(move || accept_shared.accept_clients(clients)); + let accept = thread::spawn(move || accept_shared.accept_clients(&clients)); let control_shared = Arc::clone(&shared); - let control = std::thread::spawn(move || control_shared.answer_commands(commands)); + let control = thread::spawn(move || control_shared.answer_commands(&commands)); Ok(Self { shared, @@ -75,7 +76,7 @@ impl Drop for Proxy { // No listener runs now, so the connection list is final; close any // accepted while the listeners were stopping. self.shared.shutdown_connections(); - let forwarders = std::mem::take(&mut *lock(&self.shared.forwarders)); + let forwarders = mem::take(&mut *lock(&self.shared.forwarders)); for forwarder in forwarders { let _ = forwarder.join(); } @@ -89,7 +90,7 @@ impl Shared { } } - fn accept_clients(&self, listener: UnixListener) { + fn accept_clients(&self, listener: &UnixListener) { for client in listener.incoming() { if self.stopping.load(Ordering::SeqCst) { return; @@ -103,14 +104,14 @@ impl Shared { for direction in [Direction::Request, Direction::Event] { let connection = Arc::clone(&connection); - let forwarder = std::thread::spawn(move || connection.forward(direction)); + let forwarder = thread::spawn(move || connection.forward(direction)); lock(&self.forwarders).push(forwarder); } } } /// Answers one line command per control connection with one JSON line. - fn answer_commands(&self, listener: UnixListener) { + fn answer_commands(&self, listener: &UnixListener) { for client in listener.incoming() { if self.stopping.load(Ordering::SeqCst) { return; @@ -239,7 +240,7 @@ impl Connection { if state.inspect(&self.schema, &packet, direction)? { state.held.push(packet); } else { - wire::send(destination, packet)?; + wire::send(destination, &packet)?; } } @@ -282,9 +283,9 @@ impl Connection { /// Delivers the withheld events in their original order. fn release(&self, state: &mut State) -> Result<()> { state.selected = None; - for packet in std::mem::take(&mut state.held) { + for packet in mem::take(&mut state.held) { state.inspect(&self.schema, &packet, Direction::Event)?; - wire::send(&self.client, packet)?; + wire::send(&self.client, &packet)?; } Ok(()) @@ -414,8 +415,10 @@ fn is_disconnect(error: &anyhow::Error) -> bool { }) } -/// Relay threads report failures through `State::error`, so a poisoned lock -/// carries no extra information and cleanup must still proceed. +/// Relay threads report failures through `State::error` instead of panicking, +/// so their locks are never poisoned. fn lock(mutex: &Mutex) -> MutexGuard<'_, T> { - mutex.lock().unwrap_or_else(PoisonError::into_inner) + mutex + .lock() + .expect("relay threads never hold relay state across a panic") } diff --git a/src/toolbar_gtk/view/capture_suppression/tests/wayland_proxy/wire.rs b/src/toolbar_gtk/view/capture_suppression/tests/wayland_proxy/wire.rs index e0e3405b6..ac2460838 100644 --- a/src/toolbar_gtk/view/capture_suppression/tests/wayland_proxy/wire.rs +++ b/src/toolbar_gtk/view/capture_suppression/tests/wayland_proxy/wire.rs @@ -1,17 +1,17 @@ //! Wayland stream framing. Descriptors stay with the message during whose //! bytes they arrived and are passed on, then closed, when it is sent. -use std::io; use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::os::unix::net::UnixStream; +use std::{io, mem, ptr}; use anyhow::{Result, ensure}; const HEADER_LEN: usize = 8; /// Ancillary space reserved for each read, in descriptors. const RECEIVE_DESCRIPTORS: usize = 256; -// SAFETY: CMSG_SPACE only performs arithmetic on its argument. const RECEIVE_CONTROL_LEN: usize = + // SAFETY: CMSG_SPACE only performs arithmetic on its argument. unsafe { libc::CMSG_SPACE((RECEIVE_DESCRIPTORS * size_of::()) as u32) } as usize; pub(super) struct Packet { @@ -78,7 +78,7 @@ fn receive_chunk( iov_len: buffer.len(), }; // SAFETY: zero is a valid initial state for msghdr. - let mut message: libc::msghdr = unsafe { std::mem::zeroed() }; + let mut message: libc::msghdr = unsafe { mem::zeroed() }; message.msg_iov = &mut iovec; message.msg_iovlen = 1; message.msg_control = control.as_mut_ptr().cast(); @@ -144,7 +144,7 @@ fn take_descriptors(message: &libc::msghdr, descriptors: &mut Vec) -> R } /// Writes the whole message with its descriptors attached to the first chunk. -pub(super) fn send(socket: &UnixStream, packet: Packet) -> Result<()> { +pub(super) fn send(socket: &UnixStream, packet: &Packet) -> Result<()> { let mut sent = send_chunk(socket, &packet.data, &packet.descriptors)?; while sent < packet.data.len() { sent += send_chunk(socket, &packet.data[sent..], &[])?; @@ -163,7 +163,7 @@ fn send_chunk(socket: &UnixStream, bytes: &[u8], descriptors: &[OwnedFd]) -> Res iov_len: bytes.len(), }; // SAFETY: zero is a valid initial state for msghdr. - let mut message: libc::msghdr = unsafe { std::mem::zeroed() }; + let mut message: libc::msghdr = unsafe { mem::zeroed() }; message.msg_iov = &mut iovec; message.msg_iovlen = 1; @@ -176,7 +176,7 @@ fn send_chunk(socket: &UnixStream, bytes: &[u8], descriptors: &[OwnedFd]) -> Res (*header).cmsg_level = libc::SOL_SOCKET; (*header).cmsg_type = libc::SCM_RIGHTS; (*header).cmsg_len = libc::CMSG_LEN(size_of_val(raw.as_slice()) as u32) as usize; - std::ptr::copy_nonoverlapping( + ptr::copy_nonoverlapping( raw.as_ptr(), libc::CMSG_DATA(header).cast::(), raw.len(), diff --git a/src/toolbar_gtk/view/drag.rs b/src/toolbar_gtk/view/drag.rs index 9b5f4c841..0929826fe 100644 --- a/src/toolbar_gtk/view/drag.rs +++ b/src/toolbar_gtk/view/drag.rs @@ -6,8 +6,13 @@ use std::cell::{Cell, RefCell}; use std::collections::VecDeque; +use gtk4::gdk::Display; use gtk4::prelude::*; +use crate::backend::wayland; +use crate::toolbar_gtk; +use crate::toolbar_gtk::GtkToolbarSurfaceSize; + use super::super::widgets::FeedbackSender; use super::super::{GtkToolbarDragPhase, GtkToolbarFeedback, GtkToolbarKind}; @@ -145,7 +150,7 @@ pub(super) fn cancel_move_drag( return; } drag_active.set(false); - crate::toolbar_gtk::drag_debug_log(format!( + toolbar_gtk::drag_debug_log(format!( "{kind:?} cancel generation={generation} action={action:?}" )); @@ -156,7 +161,7 @@ pub(super) fn cancel_move_drag( seq.set(seq.get().wrapping_add(1)); let (x, y) = offsets.get(); - let surface_size = crate::toolbar_gtk::GtkToolbarSurfaceSize::from_window(window); + let surface_size = GtkToolbarSurfaceSize::from_window(window); let end = match kind { GtkToolbarKind::Top => GtkToolbarFeedback::SetTopOffset { x, @@ -180,18 +185,18 @@ pub(super) fn clamp_drag_offsets( (end_x, end_y): (f64, f64), ) -> (f64, f64) { if let Some(surface) = window.surface() - && let Some(display) = gtk4::gdk::Display::default() + && let Some(display) = Display::default() && let Some(monitor) = display.monitor_at_surface(&surface) { let geometry = monitor.geometry(); - let (x, _, _) = crate::backend::wayland::clamp_floating_axis_offset( + let (x, _, _) = wayland::clamp_floating_axis_offset( x, geometry.width() as f64, window.width() as f64, base_x, end_x, ); - let (y, _, _) = crate::backend::wayland::clamp_floating_axis_offset( + let (y, _, _) = wayland::clamp_floating_axis_offset( y, geometry.height() as f64, window.height() as f64, diff --git a/src/toolbar_gtk/view/mod.rs b/src/toolbar_gtk/view/mod.rs index b37930bf4..6f04aa12d 100644 --- a/src/toolbar_gtk/view/mod.rs +++ b/src/toolbar_gtk/view/mod.rs @@ -7,9 +7,17 @@ mod drag; mod sections; mod top_bar; -use gtk4::prelude::*; +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +use gtk4::cairo::Region; +use gtk4::gdk::{Display, FrameClockPhase, MemoryFormat, MemoryTexture, Monitor}; +use gtk4::glib::Bytes; +use gtk4::prelude::{GtkWindowExt, *}; use gtk4_layer_shell::{KeyboardMode, LayerShell}; +use crate::toolbar_gtk; + use super::widgets::FeedbackSender; use super::{GtkToolbarFeedback, GtkToolbarKind, GtkToolbarUpdate}; use crate::ui::toolbar::ToolbarSnapshot; @@ -27,18 +35,27 @@ struct ToolbarSurfacePresentation { input_enabled: bool, } -fn toolbar_surface_presentation( +/// The toolbar surface state one update presents. +#[derive(Debug, Clone, Copy, Default)] +struct ToolbarSurfaceState { + /// The configuration asks for this toolbar to show. configured_visible: bool, + /// A capture is in progress and the toolbar must not appear in it. capture_suppressed: bool, + /// A drag preview stands in for this toolbar's visuals. drag_visual_hidden: bool, + /// The surface was still mapped when capture suppression began. mapped_before_capture: bool, -) -> ToolbarSurfacePresentation { - let window_visible = configured_visible || (capture_suppressed && mapped_before_capture); +} + +fn toolbar_surface_presentation(state: ToolbarSurfaceState) -> ToolbarSurfacePresentation { + let window_visible = + state.configured_visible || (state.capture_suppressed && state.mapped_before_capture); ToolbarSurfacePresentation { window_visible, - capture_transparent: capture_suppressed, - visual_hidden: drag_visual_hidden, - input_enabled: window_visible && !capture_suppressed, + capture_transparent: state.capture_suppressed, + visual_hidden: state.drag_visual_hidden, + input_enabled: window_visible && !state.capture_suppressed, } } @@ -119,7 +136,7 @@ pub(super) fn set_visual_hidden( if visual.is_child_visible() == child_visible { return; } - crate::toolbar_gtk::drag_debug_log(format!( + toolbar_gtk::drag_debug_log(format!( "{kind:?} visual child -> {} (window remains mapped for drag input)", if hidden { "hidden" } else { "visible" } )); @@ -141,7 +158,7 @@ pub(super) fn set_visual_hidden( pub(super) struct CaptureSurfaceContent { root: gtk4::Overlay, proof: gtk4::Picture, - proof_serial: std::rc::Rc>, + proof_serial: Rc>, proof_observation: capture_proof::ProofObservation, } @@ -150,10 +167,9 @@ impl CaptureSurfaceContent { // Keep non-zero RGB data behind a zero alpha channel. Renderers cannot // infer an empty node from the texture format or pixels, while normal // source-over composition still contributes exactly zero opacity. - let bytes = gtk4::glib::Bytes::from_static(&[0xff, 0x00, 0xff, 0x00]); - let texture = - gtk4::gdk::MemoryTexture::new(1, 1, gtk4::gdk::MemoryFormat::R8g8b8a8, &bytes, 4); - let proof_observation = std::rc::Rc::new(std::cell::Cell::new(None)); + let bytes = Bytes::from_static(&[0xff, 0x00, 0xff, 0x00]); + let texture = MemoryTexture::new(1, 1, MemoryFormat::R8g8b8a8, &bytes, 4); + let proof_observation = Rc::new(Cell::new(None)); let proof = gtk4::Picture::new(); let paintable = capture_proof::CaptureProof::new(&texture, &proof, proof_observation.clone(), 0); @@ -176,7 +192,7 @@ impl CaptureSurfaceContent { Self { root, proof, - proof_serial: std::rc::Rc::new(std::cell::Cell::new(0)), + proof_serial: Rc::new(Cell::new(0)), proof_observation, } } @@ -234,9 +250,8 @@ impl CaptureSurfaceContent { } else { [0x00, 0xff, 0xff, 0x00] }; - let bytes = gtk4::glib::Bytes::from_owned(bytes); - let texture = - gtk4::gdk::MemoryTexture::new(1, 1, gtk4::gdk::MemoryFormat::R8g8b8a8, &bytes, 4); + let bytes = Bytes::from_owned(bytes); + let texture = MemoryTexture::new(1, 1, MemoryFormat::R8g8b8a8, &bytes, 4); self.proof_observation.set(None); let paintable = capture_proof::CaptureProof::new( &texture, @@ -284,7 +299,7 @@ fn set_capture_transparent_class(widget: &impl IsA, transparent: b fn install_capture_css_provider(scale: f64) -> gtk4::CssProvider { let provider = gtk4::CssProvider::new(); provider.load_from_string(&super::css::capture_stylesheet(scale)); - if let Some(display) = gtk4::gdk::Display::default() { + if let Some(display) = Display::default() { gtk4::style_context_add_provider_for_display( &display, &provider, @@ -301,7 +316,7 @@ struct CaptureProofTarget { widget: gtk4::Widget, content: CaptureSurfaceContent, lifetime: CaptureProofLifetime, - on_withdrawn: Option>, + on_withdrawn: Option>, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -352,7 +367,7 @@ impl CaptureProofTarget { widget: widget.clone().upcast(), content: content.clone(), lifetime: CaptureProofLifetime::WhileMapped, - on_withdrawn: Some(std::rc::Rc::new(on_withdrawn)), + on_withdrawn: Some(Rc::new(on_withdrawn)), } } @@ -375,17 +390,17 @@ impl CaptureProofTarget { } } -pub(super) struct CaptureSurfaceLog<'a> { - name: &'a str, +pub(super) struct CaptureSurfaceLog<'surface> { + name: &'surface str, configured_visible: bool, mapped_before_capture: bool, presentation: ToolbarSurfacePresentation, - window: &'a gtk4::Window, - visual: &'a gtk4::Box, - capture_surface: &'a CaptureSurfaceContent, + window: &'surface gtk4::Window, + visual: &'surface gtk4::Box, + capture_surface: &'surface CaptureSurfaceContent, } -fn log_capture_surface_state(generation: u64, state: CaptureSurfaceLog<'_>) { +fn log_capture_surface_state(generation: u64, state: &CaptureSurfaceLog<'_>) { let CaptureSurfaceLog { name, configured_visible, @@ -394,7 +409,7 @@ fn log_capture_surface_state(generation: u64, state: CaptureSurfaceLog<'_>) { window, visual, capture_surface, - } = state; + } = *state; let surface_mapped = window.surface().is_some_and(|surface| surface.is_mapped()); log::info!( "capture.preflight id={generation} component=gtk surface={name} phase=applied configured_visible={configured_visible} mapped_before_capture={mapped_before_capture} planned_window_visible={} planned_capture_transparent={} planned_visual_hidden={} planned_input_enabled={} window_visible={} window_mapped={} surface_mapped={surface_mapped} visual_child_visible={} can_target={} size={}x{} window_opacity={:.3} content_opacity={:.3?} transparent_proof_visible={}", @@ -422,13 +437,13 @@ fn set_surface_input_enabled(window: >k4::Window, enabled: bool) { KeyboardMode::None }); if !enabled { - gtk4::prelude::GtkWindowExt::set_focus(window, gtk4::Widget::NONE); + GtkWindowExt::set_focus(window, gtk4::Widget::NONE); } if let Some(surface) = window.surface() { if enabled { surface.set_input_region(None); } else { - let empty = gtk4::cairo::Region::create(); + let empty = Region::create(); surface.set_input_region(Some(&empty)); } } @@ -448,9 +463,9 @@ where return; }; - let callback = std::rc::Rc::new(std::cell::RefCell::new(Some(callback))); - let handler = std::rc::Rc::new(std::cell::RefCell::new(None)); - let callback_slot = callback.clone(); + let callback = Rc::new(RefCell::new(Some(callback))); + let handler = Rc::new(RefCell::new(None)); + let callback_slot = callback; let handler_slot = handler.clone(); let callback_clock = frame_clock.clone(); let handler_id = frame_clock.connect_after_paint(move |_| { @@ -464,7 +479,7 @@ where *handler.borrow_mut() = Some(handler_id); widget.queue_draw(); - frame_clock.request_phase(gtk4::gdk::FrameClockPhase::PAINT); + frame_clock.request_phase(FrameClockPhase::PAINT); } pub(super) struct Windows { @@ -482,7 +497,7 @@ impl Windows { pub(super) fn new(feedback: FeedbackSender) -> Self { let css_provider = gtk4::CssProvider::new(); css_provider.load_from_string(&super::css::stylesheet(1.0)); - if let Some(display) = gtk4::gdk::Display::default() { + if let Some(display) = Display::default() { gtk4::style_context_add_provider_for_display( &display, &css_provider, @@ -531,7 +546,7 @@ impl Windows { self.wait_for_capture_paints(generation, top_mapped) .await?; - let display = gtk4::gdk::Display::default().ok_or_else(|| { + let display = Display::default().ok_or_else(|| { "GTK display disappeared during capture suppression".to_string() })?; display.sync(); @@ -668,14 +683,11 @@ fn widget_native_is_mapped(widget: >k4::Widget) -> bool { .is_some_and(|surface| surface.is_mapped()) } -fn monitor_by_connector(connector: &str) -> Option { - let display = gtk4::gdk::Display::default()?; +fn monitor_by_connector(connector: &str) -> Option { + let display = Display::default()?; let monitors = display.monitors(); for index in 0..monitors.n_items() { - let monitor = monitors - .item(index)? - .downcast::() - .ok()?; + let monitor = monitors.item(index)?.downcast::().ok()?; if monitor.connector().as_deref() == Some(connector) { return Some(monitor); } @@ -722,7 +734,11 @@ mod tests { #[test] fn capture_suppression_keeps_a_configured_toolbar_mapped_but_transparent() { assert_eq!( - toolbar_surface_presentation(true, true, false, false), + toolbar_surface_presentation(ToolbarSurfaceState { + configured_visible: true, + capture_suppressed: true, + ..ToolbarSurfaceState::default() + }), ToolbarSurfacePresentation { window_visible: true, capture_transparent: true, @@ -735,7 +751,10 @@ mod tests { #[test] fn capture_suppression_does_not_map_a_configured_hidden_toolbar() { assert_eq!( - toolbar_surface_presentation(false, true, false, false), + toolbar_surface_presentation(ToolbarSurfaceState { + capture_suppressed: true, + ..ToolbarSurfaceState::default() + }), ToolbarSurfacePresentation { window_visible: false, capture_transparent: true, @@ -748,7 +767,11 @@ mod tests { #[test] fn capture_suppression_preserves_a_surface_that_was_still_mapped() { assert_eq!( - toolbar_surface_presentation(false, true, false, true), + toolbar_surface_presentation(ToolbarSurfaceState { + capture_suppressed: true, + mapped_before_capture: true, + ..ToolbarSurfaceState::default() + }), ToolbarSurfacePresentation { window_visible: true, capture_transparent: true, @@ -761,7 +784,11 @@ mod tests { #[test] fn drag_preview_keeps_the_mapped_surface_interactive() { assert_eq!( - toolbar_surface_presentation(true, false, true, false), + toolbar_surface_presentation(ToolbarSurfaceState { + configured_visible: true, + drag_visual_hidden: true, + ..ToolbarSurfaceState::default() + }), ToolbarSurfacePresentation { window_visible: true, capture_transparent: false, @@ -774,7 +801,10 @@ mod tests { #[test] fn ending_capture_restores_pixels_and_input_without_remapping() { assert_eq!( - toolbar_surface_presentation(true, false, false, false), + toolbar_surface_presentation(ToolbarSurfaceState { + configured_visible: true, + ..ToolbarSurfaceState::default() + }), ToolbarSurfacePresentation { window_visible: true, capture_transparent: false, @@ -786,7 +816,10 @@ mod tests { #[test] fn idle_hidden_strip_stays_mapped_transparent_and_click_through() { - let shown = toolbar_surface_presentation(true, false, false, false); + let shown = toolbar_surface_presentation(ToolbarSurfaceState { + configured_visible: true, + ..ToolbarSurfaceState::default() + }); assert_eq!(shown.with_idle_hidden(false), shown); assert_eq!( shown.with_idle_hidden(true), @@ -799,7 +832,11 @@ mod tests { ); // Capture suppression already owns transparency and input. - let capturing = toolbar_surface_presentation(true, true, false, false); + let capturing = toolbar_surface_presentation(ToolbarSurfaceState { + configured_visible: true, + capture_suppressed: true, + ..ToolbarSurfaceState::default() + }); assert_eq!(capturing.with_idle_hidden(true), capturing); assert_eq!(capturing.with_idle_hidden(false), capturing); } diff --git a/src/toolbar_gtk/view/sections/mod.rs b/src/toolbar_gtk/view/sections/mod.rs index f2280f0f9..c76d28a45 100644 --- a/src/toolbar_gtk/view/sections/mod.rs +++ b/src/toolbar_gtk/view/sections/mod.rs @@ -13,12 +13,12 @@ use super::Updater; /// Everything a section builder needs; `updaters` collects the closures /// that keep the built widgets in sync with later snapshots. -pub(in crate::toolbar_gtk) struct SectionCtx<'a> { - pub(in crate::toolbar_gtk) snapshot: &'a ToolbarSnapshot, +pub(in crate::toolbar_gtk) struct SectionCtx<'view> { + pub(in crate::toolbar_gtk) snapshot: &'view ToolbarSnapshot, pub(in crate::toolbar_gtk) feedback: FeedbackSender, pub(in crate::toolbar_gtk) scale: f64, pub(in crate::toolbar_gtk) use_icons: bool, - pub(in crate::toolbar_gtk) updaters: &'a mut Vec, + pub(in crate::toolbar_gtk) updaters: &'view mut Vec, } impl SectionCtx<'_> { diff --git a/src/toolbar_gtk/view/sections/session_pane.rs b/src/toolbar_gtk/view/sections/session_pane.rs index 00fce0922..ec50b21a6 100644 --- a/src/toolbar_gtk/view/sections/session_pane.rs +++ b/src/toolbar_gtk/view/sections/session_pane.rs @@ -3,6 +3,7 @@ //! while one is pending), the way back to the home session, and the //! recent-session list. +use gtk4::pango::EllipsizeMode; use gtk4::prelude::*; use crate::i18n::{tr, trf}; @@ -24,14 +25,14 @@ pub(in crate::toolbar_gtk) fn build_popover_content( let column = gtk4::Box::new(gtk4::Orientation::Vertical, ctx.px(6.0)); let name_label = gtk4::Label::new(Some(&session.active_name)); name_label.set_xalign(0.0); - name_label.set_ellipsize(gtk4::pango::EllipsizeMode::Middle); + name_label.set_ellipsize(EllipsizeMode::Middle); column.append(&name_label); // Keep the tail of the path — the leading directories are the least // informative part of a session path. let path_label = gtk4::Label::new(Some(&session.active_path_label)); path_label.add_css_class("hint"); path_label.set_xalign(0.0); - path_label.set_ellipsize(gtk4::pango::EllipsizeMode::Start); + path_label.set_ellipsize(EllipsizeMode::Start); column.append(&path_label); // A pending Save-As overwrite confirmation replaces the button grid. @@ -110,7 +111,7 @@ fn overwrite_confirmation_rows( let rows = gtk4::Box::new(gtk4::Orientation::Vertical, ctx.px(5.0)); let message = gtk4::Label::new(Some(&trf("Replace {}?", &[&confirmation.label]))); message.set_xalign(0.0); - message.set_ellipsize(gtk4::pango::EllipsizeMode::Middle); + message.set_ellipsize(EllipsizeMode::Middle); rows.append(&message); let buttons = gtk4::Box::new(gtk4::Orientation::Horizontal, ctx.px(5.0)); @@ -140,7 +141,7 @@ fn home_button(ctx: &SectionCtx, home: &model::session::ToolbarSessionHome) -> g let button = gtk4::Button::new(); button.set_size_request(-1, ctx.px(24.0)); let label = gtk4::Label::new(Some(&home.label)); - label.set_ellipsize(gtk4::pango::EllipsizeMode::Middle); + label.set_ellipsize(EllipsizeMode::Middle); button.set_child(Some(&label)); button.set_tooltip_text(Some(&home.label)); button.set_sensitive(home.enabled); @@ -165,7 +166,7 @@ fn recent_row(ctx: &SectionCtx, recent: &model::ToolbarSessionRecent) -> gtk4::B let label = gtk4::Label::new(Some(strip_session_extension(&recent.label))); label.set_xalign(0.0); label.set_hexpand(true); - label.set_ellipsize(gtk4::pango::EllipsizeMode::Middle); + label.set_ellipsize(EllipsizeMode::Middle); row.append(&label); button.set_child(Some(&row)); button.set_tooltip_text(Some(&format_binding_label( diff --git a/src/toolbar_gtk/view/sections/settings_pane.rs b/src/toolbar_gtk/view/sections/settings_pane.rs index b1e7040bb..49f443fea 100644 --- a/src/toolbar_gtk/view/sections/settings_pane.rs +++ b/src/toolbar_gtk/view/sections/settings_pane.rs @@ -2,6 +2,8 @@ //! button grid, and the toolbar-item customization sub-panel (group //! chooser plus per-item show/hide and reorder rows). +use gtk4::glib::SignalHandlerId; +use gtk4::pango::{EllipsizeMode, WrapMode}; use gtk4::prelude::*; use crate::i18n::tr; @@ -30,7 +32,7 @@ pub(in crate::toolbar_gtk) fn build_popover_content( let label = gtk4::Label::new(Some(notice.text.as_ref())); label.set_xalign(0.0); label.set_wrap(true); - label.set_wrap_mode(gtk4::pango::WrapMode::WordChar); + label.set_wrap_mode(WrapMode::WordChar); label.set_selectable(true); match notice.severity { model::ToolbarSettingsNoticeSeverity::Info => {} @@ -118,7 +120,7 @@ fn layout_mode_segments(ctx: &mut SectionCtx) -> gtk4::Box { struct ToggleSync { id: model::ToolbarControlId, check: gtk4::CheckButton, - handler: gtk4::glib::SignalHandlerId, + handler: SignalHandlerId, } fn settings_toggle_event(template: &ToolbarEvent, checked: bool) -> ToolbarEvent { @@ -241,7 +243,7 @@ fn settings_button(ctx: &SectionCtx, button_model: &model::ToolbarSettingsButton let label = gtk4::Label::new(Some(button_model.label.as_ref())); label.set_xalign(0.0); label.set_hexpand(true); - label.set_ellipsize(gtk4::pango::EllipsizeMode::End); + label.set_ellipsize(EllipsizeMode::End); row.append(&label); button.set_child(Some(&row)); } else { diff --git a/src/toolbar_gtk/view/top_bar.rs b/src/toolbar_gtk/view/top_bar.rs index e63815b3a..9e4a6216b 100644 --- a/src/toolbar_gtk/view/top_bar.rs +++ b/src/toolbar_gtk/view/top_bar.rs @@ -23,7 +23,7 @@ mod meter; mod pen_feel; mod popover_owner; mod popovers; -use popover_owner::{PopoverOwner, PopoverResources}; +use popover_owner::PopoverOwner; mod stepper; mod strip; mod style_pill; @@ -31,25 +31,36 @@ mod style_pill; mod tests; use std::cell::{Cell, RefCell}; +use std::path::PathBuf; use std::rc::Rc; -use gtk4::prelude::*; +use gtk4::prelude::{GtkWindowExt, *}; use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; use crate::backend::wayland::{plan_top_strip, top_toolbar_size}; -use crate::config::{Action, ToolbarLayoutMode, action_short_label}; +#[cfg(test)] +use crate::config::Action; +use crate::config::{ + QuickColorPalette, ResolvedToolbarItems, ToolbarLayoutMode, ToolbarStrokeControls, +}; +use crate::draw::ArrowStyle; use crate::input::Tool; +use crate::toolbar_gtk; use crate::toolbar_icons::top_toolbar_icon_painter; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot, model}; +use crate::ui::theme::toolbar; +#[cfg(test)] +use crate::ui::toolbar::ToolbarEvent; +use crate::ui::toolbar::{ + PresetSlotSnapshot, RuntimeUiPersistenceSnapshot, SessionRecentSnapshot, ToolbarBindingHints, + ToolbarItemCustomizeGroup, ToolbarSnapshot, model, +}; +use crate::ui_text::UiTextEngine; use model::TopStripPlan; -use super::super::icons::IconWidget; use super::super::widgets::{ - FeedbackSender, SliderRow, SwatchButton, add_button_shortcut_hint, icon_button, - install_click_modifier_capture, install_key_relay, install_quick_color_recolor, - install_shortcut_focus_policy, send_event, set_active_class, sized_button, text_button, + FeedbackSender, icon_button, install_key_relay, install_shortcut_focus_policy, text_button, }; -use super::super::{GtkToolbarDragPhase, GtkToolbarFeedback, GtkToolbarKind}; +use super::super::{GtkToolbarFeedback, GtkToolbarKind}; // Spec-unit design tokens mirrored from the built-in layout // (`layout/spec/top.rs`); the plan/natural-width math reuses the builtin @@ -105,11 +116,11 @@ const STYLE_PEN_FEEL_W: f64 = 92.0; const STYLE_TAB_H: f64 = 22.0; /// Extra clear gap before a segmented control in the pill (M7-C3), on top of /// the standard control gap, sharing `theme::toolbar::SEGMENT_LEADING_GAP`. -const STYLE_SEGMENT_LEAD: f64 = crate::ui::theme::toolbar::SEGMENT_LEADING_GAP; +const STYLE_SEGMENT_LEAD: f64 = toolbar::SEGMENT_LEADING_GAP; const BASE_MARGIN: (i32, i32) = (12, 12); const END_MARGIN: (f64, f64) = (12.0, 0.0); -use super::{CaptureProofTarget, CaptureSurfaceContent, Updater}; +use super::{CaptureProofTarget, CaptureSurfaceContent, ToolbarSurfaceState, Updater}; fn control_face_button( control: model::TopToolbarControl, @@ -159,7 +170,7 @@ type OverflowContentKey = (Tool, Option, bool, bool, bool, bool, bool, boo /// harmless there. #[derive(PartialEq)] struct CanvasMenuContentKey { - items: crate::config::ResolvedToolbarItems, + items: ResolvedToolbarItems, use_icons: bool, show_actions_section: bool, show_boards_section: bool, @@ -218,13 +229,13 @@ impl CanvasMenuContentKey { /// text modes. #[derive(PartialEq)] struct SessionMenuContentKey { - items: crate::config::ResolvedToolbarItems, + items: ResolvedToolbarItems, active_session_name: Option, - active_session_path: Option, - recent_sessions: Vec, + active_session_path: Option, + recent_sessions: Vec, home_session_name: Option, at_home_session: bool, - pending_save_as_overwrite_path: Option, + pending_save_as_overwrite_path: Option, use_icons: bool, } @@ -252,19 +263,19 @@ struct SettingsMenuContentKey { /// Settings action buttons render differently in icon and text mode. use_icons: bool, customize_items_open: bool, - customize_items_group: Option, + customize_items_group: Option, /// The resolved item store is structural for every Settings pane: the /// main page filters its toggle grid and offers "Restore built-in /// visibility" from it, and the customization rows bake visibility, /// ordering, and button enabled state into their widgets. Restoring /// visibility keeps the popover open, so the controls it revives can /// only appear through a keyed rebuild. - items: crate::config::ResolvedToolbarItems, + items: ResolvedToolbarItems, status_bar_contents_open: bool, /// The "Details" disclosure adds and removes notice rows. settings_details_open: bool, layout_mode: ToolbarLayoutMode, - runtime_ui_persistence: Option, + runtime_ui_persistence: Option, } impl SettingsMenuContentKey { @@ -285,12 +296,12 @@ impl SettingsMenuContentKey { /// Canvas/Session/Settings popover content width and internal-scroll cap, in spec /// units — the same theme tokens the builtin `view/top/menus.rs` popovers /// build from, so the frontends cannot drift. -const MENU_CONTENT_W: f64 = crate::ui::theme::toolbar::MENU_CONTENT_W; +const MENU_CONTENT_W: f64 = toolbar::MENU_CONTENT_W; /// The Canvas popover content column, shared with the builtin canvas nodes via /// the theme token. It is wider than [`MENU_CONTENT_W`] so the five-action rows /// and Step Undo/Redo controls have breathing room. -const CANVAS_MENU_CONTENT_W: f64 = crate::ui::theme::toolbar::CANVAS_MENU_CONTENT_W; -const MENU_MAX_CONTENT_H: f64 = crate::ui::theme::toolbar::MENU_MAX_CONTENT_H; +const CANVAS_MENU_CONTENT_W: f64 = toolbar::CANVAS_MENU_CONTENT_W; +const MENU_MAX_CONTENT_H: f64 = toolbar::MENU_MAX_CONTENT_H; /// Inputs that force a rebuild of the bar's widget structure. #[derive(PartialEq)] @@ -303,8 +314,8 @@ struct StructureKey { /// Exact renderer-neutral top structure. Using the full resolved item store /// here made popover-only section changes tear down this bar and its popovers. top_spec: model::TopToolbarSpec, - quick_colors: crate::config::QuickColorPalette, - binding_hints: crate::ui::toolbar::ToolbarBindingHints, + quick_colors: QuickColorPalette, + binding_hints: ToolbarBindingHints, plan: TopStripPlan, ring_row: bool, /// Ordered style-pill control ids: captures the pill's morph state @@ -313,13 +324,13 @@ struct StructureKey { style_pill: Vec, /// The smoothing meter and stepper share their control id, so the /// stroke-controls style is keyed on its own. - stroke_controls: crate::config::ToolbarStrokeControls, + stroke_controls: ToolbarStrokeControls, /// Presets-island structure: the display toggle, slot count, and the /// saved slots. A change here (toggled visibility, a saved/cleared slot) /// rebuilds the island; the applied-slot highlight rides an updater. show_presets: bool, preset_slot_count: usize, - presets: Vec>, + presets: Vec>, } impl StructureKey { @@ -356,7 +367,7 @@ fn effective_scale(snapshot: &ToolbarSnapshot) -> f64 { } } -fn top_default_width(engine: &crate::ui_text::UiTextEngine, snapshot: &ToolbarSnapshot) -> i32 { +fn top_default_width(engine: &UiTextEngine, snapshot: &ToolbarSnapshot) -> i32 { top_toolbar_size(engine, snapshot).0.min(i32::MAX as u32) as i32 } @@ -397,7 +408,7 @@ fn set_island_widget_id(_widget: &impl IsA, _island: model::TopToo } pub(in crate::toolbar_gtk) struct TopBar { - ui_text: crate::ui_text::UiTextEngine, + ui_text: UiTextEngine, pub(in crate::toolbar_gtk) window: gtk4::Window, feedback: FeedbackSender, root: gtk4::Box, @@ -414,7 +425,7 @@ pub(in crate::toolbar_gtk) struct TopBar { /// The style pill's Pen feel panel, keyed on the sections it shows. feel: PopoverOwner>, /// The style pill's arrow style menu, keyed on the current style. - arrow_style: PopoverOwner, + arrow_style: PopoverOwner, drag_active: Rc>, drag_blocked: Rc>, move_drag: Option, @@ -474,7 +485,7 @@ impl TopBar { // Mapping must not give the first focusable slider keyboard ownership. // Tab navigation can still focus it explicitly afterwards. window.connect_map(|window| { - gtk4::prelude::GtkWindowExt::set_focus(window, gtk4::Widget::NONE); + GtkWindowExt::set_focus(window, gtk4::Widget::NONE); }); // Report top-window hover to the backend: GTK runs on its own @@ -495,7 +506,7 @@ impl TopBar { install_key_relay(&window, &feedback); Self { - ui_text: crate::ui_text::UiTextEngine::default(), + ui_text: UiTextEngine::default(), window, feedback, root, @@ -539,18 +550,18 @@ impl TopBar { if entering_capture_suppression && let Some(cancel) = self.move_drag_cancel.as_ref() { cancel(); } - let presentation = super::toolbar_surface_presentation( - update.top_visible, - update.capture_suppressed, - super::drag_visual_should_be_hidden( + let presentation = super::toolbar_surface_presentation(ToolbarSurfaceState { + configured_visible: update.top_visible, + capture_suppressed: update.capture_suppressed, + drag_visual_hidden: super::drag_visual_should_be_hidden( update.drag_preview, GtkToolbarKind::Top, self.drag_active.get(), self.offset_seq.get(), update.top_offset_seq, ), - self.mapped_before_capture, - ); + mapped_before_capture: self.mapped_before_capture, + }); if !presentation.window_visible { // Suppress the dismissal echoes a hide-triggered popover close // would send, so an open picker survives a hide/show cycle @@ -566,7 +577,7 @@ impl TopBar { if let Some(generation) = update.capture_suppression_generation { super::log_capture_surface_state( generation, - super::CaptureSurfaceLog { + &super::CaptureSurfaceLog { name: "top", configured_visible: update.top_visible, mapped_before_capture: self.mapped_before_capture, @@ -630,7 +641,7 @@ impl TopBar { if let Some(generation) = update.capture_suppression_generation { super::log_capture_surface_state( generation, - super::CaptureSurfaceLog { + &super::CaptureSurfaceLog { name: "top", configured_visible: update.top_visible, mapped_before_capture: self.mapped_before_capture, @@ -657,7 +668,7 @@ impl TopBar { /// flight or the echo is older than what this bar already sent. fn apply_offsets(&self, offsets: (f64, f64), echo_seq: u64) { if self.drag_active.get() || echo_seq < self.offset_seq.get() { - crate::toolbar_gtk::drag_debug_log(format!( + toolbar_gtk::drag_debug_log(format!( "top echo rejected echo_seq={echo_seq} local_seq={} active={} backend=({:.3},{:.3}) local=({:.3},{:.3})", self.offset_seq.get(), self.drag_active.get(), @@ -673,7 +684,7 @@ impl TopBar { self.offsets.set((x, y)); self.window.set_margin(Edge::Left, left); self.window.set_margin(Edge::Top, top); - crate::toolbar_gtk::drag_debug_log(format!( + toolbar_gtk::drag_debug_log(format!( "top echo applied echo_seq={echo_seq} backend=({:.3},{:.3}) stored=({x:.3},{y:.3}) margin=({left},{top}) size={}x{}", offsets.0, offsets.1, diff --git a/src/toolbar_gtk/view/top_bar/arrow_menu.rs b/src/toolbar_gtk/view/top_bar/arrow_menu.rs index 9235d4a0c..0bac15424 100644 --- a/src/toolbar_gtk/view/top_bar/arrow_menu.rs +++ b/src/toolbar_gtk/view/top_bar/arrow_menu.rs @@ -7,10 +7,22 @@ //! click-away), keys relayed to the overlay, and `closed` echoing a user //! dismissal. Choosing a style closes the menu from the backend. +use gtk4::accessible::Property; + use crate::draw::ArrowStyle; use crate::i18n::trf; - -use super::*; +use crate::toolbar_icons; +use crate::ui::theme::toolbar; + +use super::super::CaptureSurfaceContent; +use super::{STYLE_ROW_H, TopBar, set_semantic_widget_id}; +use crate::toolbar_gtk::widgets::{ + install_click_modifier_capture, install_key_relay, send_event, set_active_class, sized_button, +}; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot, model}; +use gtk4::prelude::*; +use std::cell::Cell; +use std::rc::Rc; /// Space between a preview and the name after it, as in the builtin menu. const PREVIEW_GAP: f64 = 8.0; @@ -54,7 +66,7 @@ impl TopBar { let handle = chip.clone(); let refresh = move |snapshot: &ToolbarSnapshot| { handle.set_tooltip_text(control.tooltip(snapshot).as_deref()); - handle.update_property(&[gtk4::accessible::Property::Label(&trf( + handle.update_property(&[Property::Label(&trf( "{}: {}", &[&control.label(snapshot), &snapshot.arrow_style.label()], ))]); @@ -132,7 +144,7 @@ impl TopBar { ); set_semantic_widget_id(&row, &id); row.set_tooltip_text(Some(&entry.tooltip())); - row.update_property(&[gtk4::accessible::Property::Label(&trf( + row.update_property(&[Property::Label(&trf( "{} arrow: {}", &[&entry.label, &entry.hint], ))]); @@ -183,11 +195,11 @@ impl ArrowStylePreviewArea { let style = Rc::new(Cell::new(style)); let draw_style = style.clone(); area.set_draw_func(move |_, ctx, width, height| { - crate::toolbar_icons::draw_arrow_style_preview( + toolbar_icons::draw_arrow_style_preview( ctx, (0.0, 0.0, f64::from(width), f64::from(height)), draw_style.get(), - crate::ui::theme::toolbar::COLOR_TEXT_PRIMARY, + toolbar::COLOR_TEXT_PRIMARY, ); }); Self { area, style } diff --git a/src/toolbar_gtk/view/top_bar/controls.rs b/src/toolbar_gtk/view/top_bar/controls.rs index cd56db7a9..b4b2cd382 100644 --- a/src/toolbar_gtk/view/top_bar/controls.rs +++ b/src/toolbar_gtk/view/top_bar/controls.rs @@ -2,7 +2,19 @@ //! //! Owns construction and state-updater wiring for toolbar buttons and chrome. -use super::*; +use gtk4::accessible::Property; + +use super::super::CaptureSurfaceContent; +use super::{TopBar, control_face_button, effective_scale, set_control_widget_id}; +use crate::toolbar_gtk::icons::IconWidget; +use crate::toolbar_gtk::widgets::{ + add_button_shortcut_hint, install_key_relay, send_event, set_active_class, sized_button, +}; +use crate::toolbar_icons::top_toolbar_icon_painter; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot, model}; +use gtk4::prelude::*; +use std::cell::Cell; +use std::rc::Rc; use crate::ui::theme::set_color; use crate::ui::theme::toolbar::{ @@ -10,6 +22,7 @@ use crate::ui::theme::toolbar::{ PRESET_SLOT_ICON_RATIO, PRESET_SLOT_NUMBER_BOX, PRESET_SLOT_SWATCH_INSET, PRESET_SLOT_SWATCH_RADIUS, PRESET_SLOT_SWATCH_RATIO, }; +use crate::{draw, ui}; use super::super::super::widgets::rounded_rect_path; @@ -54,7 +67,10 @@ impl TopBar { use_icons: bool, show_badge: bool, ) -> gtk4::Button { - assert!(matches!(control, model::TopToolbarControl::Tool(_))); + assert!( + matches!(control, model::TopToolbarControl::Tool(_)), + "tool_button builds only tool controls, got {control:?}" + ); self.action_button( snapshot, control, @@ -75,7 +91,11 @@ impl TopBar { icon_size: f64, use_icons: bool, ) -> gtk4::Button { - assert_eq!(control, model::TopToolbarControl::ShapePicker); + assert_eq!( + control, + model::TopToolbarControl::ShapePicker, + "shapes_picker_button builds only the shape picker" + ); let tooltip = control.tooltip(snapshot); let label = control.label(snapshot); let button = control_face_button( @@ -89,7 +109,7 @@ impl TopBar { ); set_control_widget_id(&button, control); let accessible_label = control.accessible_label(snapshot); - button.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + button.update_property(&[Property::Label(&accessible_label)]); let sender = self.feedback.clone(); let expected = self.shapes.expected_open.clone(); button.connect_clicked(move |_| { @@ -153,10 +173,13 @@ impl TopBar { use_icons: bool, show_badge: bool, ) -> gtk4::Button { - assert!(matches!( - control, - model::TopToolbarControl::Undo | model::TopToolbarControl::Redo - )); + assert!( + matches!( + control, + model::TopToolbarControl::Undo | model::TopToolbarControl::Redo + ), + "history_button builds only undo or redo, got {control:?}" + ); self.action_button( snapshot, control, @@ -189,7 +212,7 @@ impl TopBar { ); set_control_widget_id(&button, control); let accessible_label = control.accessible_label(snapshot); - button.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + button.update_property(&[Property::Label(&accessible_label)]); if show_badge { let badge = control.shortcut_badge(snapshot); add_button_shortcut_hint(&button, badge.as_deref(), use_icons); @@ -236,12 +259,15 @@ impl TopBar { index: usize, button_size: (f64, f64), ) -> gtk4::Button { - assert!(matches!(control, model::TopToolbarControl::Preset(_))); + assert!( + matches!(control, model::TopToolbarControl::Preset(_)), + "preset_button builds only preset controls, got {control:?}" + ); let button = sized_button(button_size.0, button_size.1); set_control_widget_id(&button, control); button.add_css_class("preset"); let accessible_label = control.accessible_label(snapshot); - button.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + button.update_property(&[Property::Label(&accessible_label)]); button.set_tooltip_text(Some(&control.tooltip(snapshot))); set_active_class(&button, control.active(snapshot)); @@ -284,11 +310,11 @@ impl TopBar { // checkerboard, so the slot previews what it will apply. let swatch_path = |ctx: &cairo::Context| rounded_rect_path(ctx, sx, sy, sw, sw, radius); - crate::ui::checkerboard_behind(ctx, a, swatch_path); + ui::checkerboard_behind(ctx, a, swatch_path); ctx.set_source_rgba(r, g, b, a); swatch_path(ctx); let _ = ctx.fill(); - let luminance = crate::draw::perceived_luminance(r, g, b); + let luminance = draw::perceived_luminance(r, g, b); set_color( ctx, if luminance < 0.3 { @@ -340,12 +366,16 @@ impl TopBar { control: model::TopToolbarControl, size: f64, ) -> gtk4::Button { - assert_eq!(control, model::TopToolbarControl::Pin); + assert_eq!( + control, + model::TopToolbarControl::Pin, + "pin_button builds only the pin control" + ); let button = sized_button(size, size); set_control_widget_id(&button, control); button.add_css_class("chrome"); let accessible_label = control.accessible_label(snapshot); - button.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + button.update_property(&[Property::Label(&accessible_label)]); let glyph = if snapshot.top_pinned { model::TopToolbarIcon::Pin } else { @@ -374,7 +404,7 @@ impl TopBar { handle.remove_css_class("pinned"); } let accessible_label = control.accessible_label(snapshot); - handle.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + handle.update_property(&[Property::Label(&accessible_label)]); handle.set_tooltip_text(Some(&control.tooltip(snapshot))); })); button @@ -389,11 +419,15 @@ impl TopBar { button_size: (f64, f64), icon_size: f64, ) -> gtk4::Button { - assert_eq!(control, model::TopToolbarControl::Overflow); + assert_eq!( + control, + model::TopToolbarControl::Overflow, + "overflow_button builds only the overflow menu" + ); let button = sized_button(button_size.0, button_size.1); set_control_widget_id(&button, control); let accessible_label = control.accessible_label(snapshot); - button.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + button.update_property(&[Property::Label(&accessible_label)]); button.set_tooltip_text(Some(&control.tooltip(snapshot))); let icon = IconWidget::new( top_toolbar_icon_painter(model::TopToolbarIcon::Overflow), @@ -486,10 +520,13 @@ impl TopBar { control: model::TopToolbarControl, size: f64, ) -> gtk4::Button { - assert!(matches!( - control, - model::TopToolbarControl::About | model::TopToolbarControl::Exit - )); + assert!( + matches!( + control, + model::TopToolbarControl::About | model::TopToolbarControl::Exit + ), + "about_button builds only about or exit, got {control:?}" + ); let button = sized_button(size, size); set_control_widget_id(&button, control); button.add_css_class("chrome"); @@ -499,7 +536,7 @@ impl TopBar { "exit" }); let accessible_label = control.accessible_label(snapshot); - button.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + button.update_property(&[Property::Label(&accessible_label)]); button.set_tooltip_text(Some(&control.tooltip(snapshot))); let icon = IconWidget::new( top_toolbar_icon_painter(control.glyph(snapshot)), @@ -524,13 +561,17 @@ impl TopBar { control: model::TopToolbarControl, size: f64, ) -> gtk4::Button { - assert_eq!(control, model::TopToolbarControl::LayoutMode); + assert_eq!( + control, + model::TopToolbarControl::LayoutMode, + "layout_mode_button builds only the layout mode control" + ); let button = sized_button(size, size); set_control_widget_id(&button, control); button.add_css_class("chrome"); button.add_css_class("layout"); let accessible_label = control.accessible_label(snapshot); - button.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + button.update_property(&[Property::Label(&accessible_label)]); button.set_tooltip_text(Some(&control.tooltip(snapshot))); let icon = IconWidget::new( top_toolbar_icon_painter(model::TopToolbarIcon::for_layout_mode(snapshot.layout_mode)), @@ -574,13 +615,17 @@ impl TopBar { control: model::TopToolbarControl, size: f64, ) -> gtk4::Button { - assert_eq!(control, model::TopToolbarControl::Minimize); + assert_eq!( + control, + model::TopToolbarControl::Minimize, + "minimize_button builds only the minimize control" + ); let button = sized_button(size, size); set_control_widget_id(&button, control); button.add_css_class("chrome"); button.add_css_class("minimize"); let accessible_label = control.accessible_label(snapshot); - button.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + button.update_property(&[Property::Label(&accessible_label)]); button.set_tooltip_text(Some(&control.tooltip(snapshot))); let icon = IconWidget::new( top_toolbar_icon_painter(model::TopToolbarIcon::Minimize), diff --git a/src/toolbar_gtk/view/top_bar/drag.rs b/src/toolbar_gtk/view/top_bar/drag.rs index 76ed85696..00ad59504 100644 --- a/src/toolbar_gtk/view/top_bar/drag.rs +++ b/src/toolbar_gtk/view/top_bar/drag.rs @@ -3,11 +3,22 @@ //! Coalesces stable start-relative gesture coordinates while the GTK input //! surface remains parked, then reports explicit lifecycle phases to the backend. +use gtk4::glib::ControlFlow; +use gtk4::graphene::Point; + +use crate::toolbar_gtk; +use crate::toolbar_gtk::GtkToolbarSurfaceSize; + use super::super::drag::{ FrameCoalescedDrag, ReservedDragSequence, cancel_move_drag, clamp_drag_offsets, drag_frame_position, }; -use super::*; +use super::{BASE_MARGIN, END_MARGIN, TopBar}; +use crate::toolbar_gtk::{GtkToolbarDragPhase, GtkToolbarFeedback, GtkToolbarKind}; +use gtk4::prelude::*; +use gtk4_layer_shell::{Edge, LayerShell}; +use std::cell::Cell; +use std::rc::Rc; impl TopBar { /// Park the GTK input surface at its origin while the main overlay renders @@ -39,13 +50,13 @@ impl TopBar { let begin_pending = pending.clone(); let begin_generation = active_generation.clone(); let begin_ready = ready_generation.clone(); - let begin_origin = drag_origin.clone(); + let begin_origin = drag_origin; let begin_window = window.downgrade(); let begin_visual = self.root.downgrade(); let begin_grip = grip.downgrade(); let frame_offsets = offsets.clone(); let frame_feedback = feedback.clone(); - let frame_base = base_x.clone(); + let frame_base = base_x; let frame_seq = seq.clone(); drag.connect_drag_begin(move |gesture, start_x, start_y| { if begin_blocked.get() { @@ -60,7 +71,7 @@ impl TopBar { gesture.set_state(gtk4::EventSequenceState::Denied); return; }; - let start = gtk4::graphene::Point::new(start_x as f32, start_y as f32); + let start = Point::new(start_x as f32, start_y as f32); if !grip .compute_bounds(&frame_window) .is_some_and(|bounds| bounds.contains_point(&start)) @@ -77,7 +88,7 @@ impl TopBar { let start = GtkToolbarFeedback::SetTopOffset { x: origin.0, y: origin.1, - surface_size: crate::toolbar_gtk::GtkToolbarSurfaceSize::from_window( + surface_size: GtkToolbarSurfaceSize::from_window( &frame_window, ), seq: start_seq.value(), @@ -91,7 +102,7 @@ impl TopBar { ); let start_feedback = frame_feedback.clone(); let start_window = frame_window.clone(); - let start_visual = visual.clone(); + let start_visual = visual; let start_active = begin_active.clone(); let start_generation = begin_generation.clone(); let start_ready = begin_ready.clone(); @@ -127,13 +138,13 @@ impl TopBar { let drag_origin = begin_origin.clone(); frame_window.add_tick_callback(move |_, _| { if active_generation.get() != generation { - return gtk4::glib::ControlFlow::Break; + return ControlFlow::Break; } if ready_generation.get() != generation { - return gtk4::glib::ControlFlow::Continue; + return ControlFlow::Continue; } let Some(frame) = pending.take_frame(generation) else { - return gtk4::glib::ControlFlow::Continue; + return ControlFlow::Continue; }; let base = base_x.get(); let (cx, cy) = offsets.get(); @@ -142,7 +153,7 @@ impl TopBar { clamp_drag_offsets(&window, (x, y), (base, BASE_MARGIN.0 as f64), END_MARGIN); offsets.set((x, y)); seq.set(seq.get() + 1); - crate::toolbar_gtk::drag_debug_log(format!( + toolbar_gtk::drag_debug_log(format!( "top frame generation={generation} seq={} phase={:?} delta=({:.3},{:.3}) origin=({:.3},{:.3}) before=({cx:.3},{cy:.3}) preview=({x:.3},{y:.3}) parked_margin=({}, {}) size={}x{}", seq.get(), frame.phase, @@ -158,7 +169,7 @@ impl TopBar { let _ = feedback.send(GtkToolbarFeedback::SetTopOffset { x, y, - surface_size: crate::toolbar_gtk::GtkToolbarSurfaceSize::from_window(&window), + surface_size: GtkToolbarSurfaceSize::from_window(&window), seq: seq.get(), phase: frame.phase, }); @@ -168,9 +179,9 @@ impl TopBar { active_generation.set(0); ready_generation.set(0); } - gtk4::glib::ControlFlow::Break + ControlFlow::Break } else { - gtk4::glib::ControlFlow::Continue + ControlFlow::Continue } }); }); @@ -179,16 +190,16 @@ impl TopBar { let update_generation = active_generation.clone(); drag.connect_drag_update(move |_, dx, dy| { let generation = update_generation.get(); - crate::toolbar_gtk::drag_debug_log(format!( + toolbar_gtk::drag_debug_log(format!( "top raw generation={generation} start_relative=({dx:.3},{dy:.3})", )); update_pending.update(generation, dx, dy); }); - let end_pending = pending.clone(); + let end_pending = pending; let end_generation = active_generation.clone(); drag.connect_drag_end(move |_, dx, dy| { - crate::toolbar_gtk::drag_debug_log(format!( + toolbar_gtk::drag_debug_log(format!( "top end generation={} delta=({dx:.3},{dy:.3})", end_generation.get(), )); diff --git a/src/toolbar_gtk/view/top_bar/layout_menu.rs b/src/toolbar_gtk/view/top_bar/layout_menu.rs index de782e66c..b0ebf4f05 100644 --- a/src/toolbar_gtk/view/top_bar/layout_menu.rs +++ b/src/toolbar_gtk/view/top_bar/layout_menu.rs @@ -3,9 +3,16 @@ //! Rows come from the shared `model::layout_menu_entries`, so this menu lists //! the same presets with the same wording as the built-in one. +use gtk4::accessible::Property; + use crate::i18n::trf; -use super::*; +use super::{TopBar, set_semantic_widget_id}; +use crate::toolbar_gtk::widgets::{ + install_click_modifier_capture, send_event, set_active_class, sized_button, +}; +use crate::ui::toolbar::{ToolbarSnapshot, model}; +use gtk4::prelude::*; impl TopBar { /// Keep the layout menu's content and open state in line with the @@ -54,7 +61,7 @@ impl TopBar { &format!("top.layout.{}", model::layout_mode_id(entry.mode)), ); row.set_tooltip_text(Some(&trf("Switch to the {} layout", &[&entry.label]))); - row.update_property(&[gtk4::accessible::Property::Label(&trf( + row.update_property(&[Property::Label(&trf( "{} layout: {}", &[&entry.label, &entry.description], ))]); diff --git a/src/toolbar_gtk/view/top_bar/meter.rs b/src/toolbar_gtk/view/top_bar/meter.rs index dc0ae76dd..48a667a21 100644 --- a/src/toolbar_gtk/view/top_bar/meter.rs +++ b/src/toolbar_gtk/view/top_bar/meter.rs @@ -2,7 +2,18 @@ //! button per level. Mirrors the builtin `view/top/meter.rs` slot for slot; //! the Pen feel panel lays the same bars across its wider column. -use super::*; +use gtk4::accessible::Property; +use gtk4::glib::Propagation; + +use crate::toolbar_icons; + +use super::super::Updater; +use super::{STYLE_CAPTION_W, STYLE_METER_W, STYLE_ROW_H, TopBar, set_semantic_widget_id}; +use crate::toolbar_gtk::widgets::{FeedbackSender, send_event, sized_button}; +use crate::ui::toolbar::{ToolbarSnapshot, model}; +use gtk4::prelude::*; +use std::cell::Cell; +use std::rc::Rc; /// Gap between neighbouring bars; matches the builtin painter's `BAR_GAP`. const METER_BAR_GAP: f64 = 3.0; @@ -70,7 +81,7 @@ pub(super) fn append_meter_bars( if steps != 0 { send_event(&sender, setting.nudge_event(steps)); } - gtk4::glib::Propagation::Stop + Propagation::Stop }); row.add_controller(wheel); @@ -95,7 +106,7 @@ pub(super) fn append_meter_bars( bar.remove_css_class("filled"); } bar.set_tooltip_text(Some(&segment.tooltip)); - bar.update_property(&[gtk4::accessible::Property::Label(&segment.tooltip)]); + bar.update_property(&[Property::Label(&segment.tooltip)]); bar.set_sensitive(enabled); } }; @@ -127,7 +138,7 @@ pub(super) fn append_meter_zero( let (active, hover, enabled) = state.get(); let _ = ctx.save(); ctx.scale(scale, scale); - crate::toolbar_icons::draw_meter_dot( + toolbar_icons::draw_meter_dot( ctx, (3.0, 0.0, model::METER_ZERO_SLOT_W, row_h), active, @@ -181,7 +192,7 @@ pub(super) fn append_meter_zero( area.queue_draw(); let zero = setting.zero_segment(snapshot, &id_prefix); dot.set_tooltip_text(Some(&zero.tooltip)); - dot.update_property(&[gtk4::accessible::Property::Label(&zero.tooltip)]); + dot.update_property(&[Property::Label(&zero.tooltip)]); dot.set_sensitive(enabled); } } @@ -206,7 +217,7 @@ impl TopBar { // bars out abutting, and the width planner budgets exactly that. let row = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); set_semantic_widget_id(&row, control.id().as_ref()); - row.update_property(&[gtk4::accessible::Property::Label(&control.label(snapshot))]); + row.update_property(&[Property::Label(&control.label(snapshot))]); row.set_valign(gtk4::Align::Center); if let Some(caption) = control.caption() { let caption_label = gtk4::Label::new(Some(caption)); diff --git a/src/toolbar_gtk/view/top_bar/pen_feel.rs b/src/toolbar_gtk/view/top_bar/pen_feel.rs index 6679eb587..d7920881f 100644 --- a/src/toolbar_gtk/view/top_bar/pen_feel.rs +++ b/src/toolbar_gtk/view/top_bar/pen_feel.rs @@ -8,10 +8,21 @@ //! autohide grab (the backend's dismissal policy owns click-away), keys //! relayed to the overlay, and `closed` echoing a user dismissal. +use gtk4::accessible::Property; + use crate::i18n::trf; +use crate::toolbar_icons; +use super::super::{CaptureSurfaceContent, Updater}; use super::meter::{MeterBarsSpec, append_meter_bars}; -use super::*; +use super::{STYLE_PEN_FEEL_W, STYLE_ROW_H, TopBar, set_semantic_widget_id}; +use crate::toolbar_gtk::widgets::{ + install_click_modifier_capture, install_key_relay, send_event, set_active_class, sized_button, +}; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot, model}; +use gtk4::prelude::*; +use std::cell::Cell; +use std::rc::Rc; impl TopBar { /// Appends the Pen feel chip to `pill`, followed by `gap_px` of margin, @@ -36,7 +47,7 @@ impl TopBar { let handle = chip.clone(); let refresh = move |snapshot: &ToolbarSnapshot| { handle.set_tooltip_text(control.tooltip(snapshot).as_deref()); - handle.update_property(&[gtk4::accessible::Property::Label( + handle.update_property(&[Property::Label( &model::StylePillControl::pen_feel_accessible_label(snapshot), )]); set_active_class(&handle, control.active(snapshot)); @@ -145,10 +156,7 @@ impl TopBar { let bars = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); bars.set_margin_top(px(model::PEN_FEEL_ROW_GAP)); set_semantic_widget_id(&bars, §ion.id("bars")); - bars.update_property(&[gtk4::accessible::Property::Label(&trf( - "{} level", - &[&setting.name()], - ))]); + bars.update_property(&[Property::Label(&trf("{} level", &[&setting.name()]))]); updaters.push(append_meter_bars( &self.feedback, &bars, @@ -211,7 +219,7 @@ impl SmoothingPreviewArea { } self.area - .update_property(&[gtk4::accessible::Property::Label(&preview_label(level))]); + .update_property(&[Property::Label(&preview_label(level))]); self.area.queue_draw(); } } @@ -226,14 +234,12 @@ fn smoothing_preview_area(section: &model::PenFeelSection, scale: f64) -> Smooth area.set_margin_top(px(model::PEN_FEEL_ROW_GAP)); area.set_can_target(false); set_semantic_widget_id(&area, §ion.id("preview")); - area.update_property(&[gtk4::accessible::Property::Label(&preview_label( - section.level, - ))]); + area.update_property(&[Property::Label(&preview_label(section.level))]); let level = Rc::new(Cell::new(section.level)); let draw_level = level.clone(); area.set_draw_func(move |_, ctx, width, height| { - crate::toolbar_icons::draw_smoothing_preview( + toolbar_icons::draw_smoothing_preview( ctx, (0.0, 0.0, f64::from(width), f64::from(height)), draw_level.get(), diff --git a/src/toolbar_gtk/view/top_bar/popover_owner.rs b/src/toolbar_gtk/view/top_bar/popover_owner.rs index 59a2bf9ff..b3693715c 100644 --- a/src/toolbar_gtk/view/top_bar/popover_owner.rs +++ b/src/toolbar_gtk/view/top_bar/popover_owner.rs @@ -1,5 +1,8 @@ //! A popover and its capture resources have one lifecycle. -use super::*; +use super::super::{CaptureSurfaceContent, Updater}; +use gtk4::prelude::*; +use std::cell::Cell; +use std::rc::Rc; #[derive(Clone)] pub(super) struct PopoverResources { diff --git a/src/toolbar_gtk/view/top_bar/popovers.rs b/src/toolbar_gtk/view/top_bar/popovers.rs index 03a8e1cf7..3390e1826 100644 --- a/src/toolbar_gtk/view/top_bar/popovers.rs +++ b/src/toolbar_gtk/view/top_bar/popovers.rs @@ -4,9 +4,30 @@ //! popovers synchronized without rebuilding content on unrelated snapshot //! changes. -use super::*; +use gtk4::accessible::Property; +use gtk4::cairo::Region; + +use crate::config::toolbar_item_ids; + +use super::super::{CaptureProofTarget, CaptureSurfaceContent, Updater}; +use super::popover_owner::PopoverResources; +use super::{ + CANVAS_MENU_CONTENT_W, COMPACT_BUTTON, CanvasMenuContentKey, GAP, ICON_BUTTON, ICON_SIZE, + MENU_CONTENT_W, MENU_MAX_CONTENT_H, SessionMenuContentKey, SettingsMenuContentKey, + TEXT_BUTTON_H, TEXT_BUTTON_W, TopBar, control_face_button, effective_scale, + set_prefixed_control_widget_id, set_semantic_widget_id, +}; +use crate::config::{Action, action_short_label}; use crate::i18n::{tr, trf}; +use crate::input::Tool; use crate::toolbar_gtk::css::CAPTURE_TRANSPARENT_CLASS; +use crate::toolbar_gtk::widgets::{ + FeedbackSender, add_button_shortcut_hint, install_click_modifier_capture, send_event, + set_active_class, text_button, +}; +use crate::ui::toolbar::model::TopStripPlan; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot, model}; +use gtk4::prelude::*; pub(super) fn set_popover_capture_transparent( popover: >k4::Popover, @@ -61,7 +82,7 @@ fn set_popover_input_enabled(popover: >k4::Popover, enabled: bool) { match region { PopoverInputRegion::Keep => {} PopoverInputRegion::Empty => { - let empty = gtk4::cairo::Region::create(); + let empty = Region::create(); surface.set_input_region(Some(&empty)); } PopoverInputRegion::Restore => { @@ -404,7 +425,7 @@ impl TopBar { ); set_prefixed_control_widget_id(&button, "top.picker.", control); let accessible_label = control.accessible_label(snapshot); - button.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + button.update_property(&[Property::Label(&accessible_label)]); let badge = control.shortcut_badge(snapshot); add_button_shortcut_hint(&button, badge.as_deref(), use_icons); set_active_class(&button, control.active(snapshot)); @@ -422,10 +443,7 @@ impl TopBar { // using them must not close it (GTK popovers keep inside clicks). if model::top_fill_visible(snapshot) { let fill = gtk4::CheckButton::with_label(action_short_label(Action::ToggleFill)); - set_semantic_widget_id( - &fill, - crate::config::toolbar_item_ids::TOP_UTILITY_FILL.as_str(), - ); + set_semantic_widget_id(&fill, toolbar_item_ids::TOP_UTILITY_FILL.as_str()); fill.set_tooltip_text(Some(&model::action_tooltip(snapshot, Action::ToggleFill))); // set_active runs before connect_toggled, so every later // toggle is user input and forwards unconditionally. @@ -504,7 +522,7 @@ impl TopBar { ); set_prefixed_control_widget_id(&button, "top.overflow.", control); let accessible_label = control.accessible_label(snapshot); - button.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + button.update_property(&[Property::Label(&accessible_label)]); if button_size.0 > COMPACT_BUTTON * scale { let badge = control.shortcut_badge(snapshot); add_button_shortcut_hint(&button, badge.as_deref(), use_icons); diff --git a/src/toolbar_gtk/view/top_bar/stepper.rs b/src/toolbar_gtk/view/top_bar/stepper.rs index 2f783d38e..f8b2b9f17 100644 --- a/src/toolbar_gtk/view/top_bar/stepper.rs +++ b/src/toolbar_gtk/view/top_bar/stepper.rs @@ -3,8 +3,17 @@ //! for slot; serves the docked selection properties and the pen smoothing //! and Shape Pen detection steppers of `stroke_controls = "stepper"`. +use gtk4::accessible::Property; + use super::style_pill::pill_button; -use super::*; +use super::{ + STYLE_CAPTION_W, STYLE_ROW_H, STYLE_SEL_VALUE_W, STYLE_STEP_W, TopBar, set_semantic_widget_id, +}; +use crate::toolbar_gtk::widgets::send_event; +use crate::ui::toolbar::{ToolbarSnapshot, model}; +use gtk4::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; impl TopBar { /// Appends one stepper to `pill`, followed by `gap_px` of margin, and @@ -29,7 +38,7 @@ impl TopBar { // A row of "− 3 +" says nothing about what it steps: the caption // names it on screen, the label for assistive tech. let accessible_label = control.label(snapshot); - row.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + row.update_property(&[Property::Label(&accessible_label)]); row.set_valign(gtk4::Align::Center); if let Some(caption) = control.caption() { let caption_label = gtk4::Label::new(Some(caption)); @@ -61,9 +70,9 @@ impl TopBar { let minus = pill_button(steps[0].label, sz(STYLE_STEP_W), sz(STYLE_ROW_H)); set_semantic_widget_id(&minus, steps[0].id); minus.set_tooltip_text(Some(&steps[0].tooltip)); - minus.update_property(&[gtk4::accessible::Property::Label(&steps[0].tooltip)]); + minus.update_property(&[Property::Label(&steps[0].tooltip)]); row.append(&minus); - handles.push(minus.clone()); + handles.push(minus); let value = gtk4::Label::new(Some(&control.required_value_text(snapshot))); value.add_css_class("stepper-value"); set_semantic_widget_id(&value, &format!("{}.value", control.id())); @@ -72,9 +81,9 @@ impl TopBar { let plus = pill_button(steps[1].label, sz(STYLE_STEP_W), sz(STYLE_ROW_H)); set_semantic_widget_id(&plus, steps[1].id); plus.set_tooltip_text(Some(&steps[1].tooltip)); - plus.update_property(&[gtk4::accessible::Property::Label(&steps[1].tooltip)]); + plus.update_property(&[Property::Label(&steps[1].tooltip)]); row.append(&plus); - handles.push(plus.clone()); + handles.push(plus); // The halves carry the level they land on, which moves with the // level, so their clicks read the live step rather than the one diff --git a/src/toolbar_gtk/view/top_bar/strip.rs b/src/toolbar_gtk/view/top_bar/strip.rs index caf394af9..ff6209d61 100644 --- a/src/toolbar_gtk/view/top_bar/strip.rs +++ b/src/toolbar_gtk/view/top_bar/strip.rs @@ -3,7 +3,27 @@ //! Owns the ordered strip layout and width-degradation presentation while //! delegating individual controls to the control factory methods. -use super::*; +use std::ptr; + +use gtk4::accessible::Property; + +use crate::toolbar_icons; +use crate::toolbar_icons::MicroChipStyle; + +use super::{ + COMPACT_BUTTON, COMPACT_CHROME, COMPACT_GAP, COMPACT_ISLAND_GAP, DIVIDER_SPAN, GAP, + HANDLE_SIZE, ICON_BUTTON, ICON_SIZE, ISLAND_GAP, MICRO_SIZE, MINIMIZED_SIZE, PIN_BUTTON_GAP, + PIN_BUTTON_SIZE, TEXT_BUTTON_H, TEXT_BUTTON_W, TopBar, effective_scale, set_control_widget_id, + set_island_widget_id, set_semantic_widget_id, top_default_width, +}; +use crate::toolbar_gtk::icons::IconWidget; +use crate::toolbar_gtk::widgets::{send_event, sized_button}; +use crate::toolbar_icons::top_toolbar_icon_painter; +use crate::ui::toolbar::model::TopStripPlan; +use crate::ui::toolbar::{ToolbarSnapshot, model}; +use gtk4::prelude::*; +use std::cell::Cell; +use std::rc::Rc; /// Micro-chip ring state: `(ring color, spec-unit ring width)`. type MicroRing = ((f64, f64, f64, f64), f64); @@ -38,7 +58,7 @@ impl TopBar { set_control_widget_id(&restore, control); restore.add_css_class("restore-tab"); let accessible_label = control.accessible_label(snapshot); - restore.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + restore.update_property(&[Property::Label(&accessible_label)]); restore.set_tooltip_text(Some(&control.tooltip(snapshot))); // Glyph plus a short caption, so the tab reads as a button to bring // the tools back rather than a stray sliver. @@ -84,10 +104,10 @@ impl TopBar { set_control_widget_id(&chip, control); chip.add_css_class("swatch"); let accessible_label = control.accessible_label(snapshot); - chip.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + chip.update_property(&[Property::Label(&accessible_label)]); chip.set_tooltip_text(Some(&control.tooltip(snapshot))); - let painter = Rc::new(Cell::new(crate::toolbar_icons::top_toolbar_icon_painter( + let painter = Rc::new(Cell::new(toolbar_icons::top_toolbar_icon_painter( model::TopToolbarIcon::Tool(model::semantic_icon_for_tool(snapshot.active_tool)), ))); let ring: Rc> = Rc::new(Cell::new(( @@ -115,13 +135,13 @@ impl TopBar { let chip_size = (width.min(height) as f64).max(1.0); let x = (width as f64 - chip_size) / 2.0; let y = (height as f64 - chip_size) / 2.0; - crate::toolbar_icons::draw_micro_chip( + toolbar_icons::draw_micro_chip( ctx, x, y, chip_size, draw_painter.get(), - &crate::toolbar_icons::MicroChipStyle { + &MicroChipStyle { ring_color, // Ring width is in spec units; scale with the chip. ring_width: ring_width * (chip_size / MICRO_SIZE), @@ -162,11 +182,11 @@ impl TopBar { // Live state: tool glyph, ring color, and ring width track the // snapshot without rebuilding the chip. self.updaters.borrow_mut().push(Box::new(move |snapshot| { - let next_painter = crate::toolbar_icons::top_toolbar_icon_painter( + let next_painter = toolbar_icons::top_toolbar_icon_painter( model::TopToolbarIcon::Tool(model::semantic_icon_for_tool(snapshot.active_tool)), ); let mut dirty = false; - if !std::ptr::fn_addr_eq(painter.get(), next_painter) { + if !ptr::fn_addr_eq(painter.get(), next_painter) { painter.set(next_painter); dirty = true; } @@ -299,9 +319,7 @@ impl TopBar { grip.area.add_css_class("drag-handle"); let accessible_label = control.accessible_label(snapshot); grip.area - .update_property(&[gtk4::accessible::Property::Label( - &accessible_label, - )]); + .update_property(&[Property::Label(&accessible_label)]); grip.area.set_tooltip_text(Some(&control.tooltip(snapshot))); grip.area.set_valign(gtk4::Align::Center); grip.area.set_cursor_from_name(Some("grab")); @@ -451,7 +469,7 @@ impl TopBar { // engine snaps under reduced motion). Continuous opacity, driven // per-update, so open popovers and hover state survive. `apply` // handles the fully hidden frame and input passthrough. - let fade_outer = outer.clone(); + let fade_outer = outer; self.updaters.borrow_mut().push(Box::new(move |snapshot| { fade_outer.set_opacity(snapshot.top_fade.clamp(0.0, 1.0)); })); @@ -463,7 +481,7 @@ impl TopBar { let ring = gtk4::CheckButton::with_label(&control.label(snapshot)); set_control_widget_id(&ring, control); let accessible_label = control.accessible_label(snapshot); - ring.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + ring.update_property(&[Property::Label(&accessible_label)]); ring.add_css_class("mini"); // The root is transparent behind the pills, so the contextual // ring row carries its own small pill background. It uses the diff --git a/src/toolbar_gtk/view/top_bar/style_pill.rs b/src/toolbar_gtk/view/top_bar/style_pill.rs index 06cecbf39..d3041874a 100644 --- a/src/toolbar_gtk/view/top_bar/style_pill.rs +++ b/src/toolbar_gtk/view/top_bar/style_pill.rs @@ -7,7 +7,26 @@ //! values (slider positions, swatch selection, numerals, segment actives, //! reset tooltips) run through stored updaters. -use super::*; +use gtk4::accessible::Property; +use gtk4::glib::Propagation; + +use crate::toolbar_icons; + +use super::{ + CHIP_SIZE, COMPACT_GAP, GAP, STYLE_FONT_PICK_W, STYLE_PILL_GAP, STYLE_RESET_W, STYLE_ROW_H, + STYLE_SEGMENT_LEAD, STYLE_SEL_VALUE_W, STYLE_SLIDER_W, STYLE_TAB_H, STYLE_VALUE_W, SWATCH_GAP, + SWATCH_SIZE, TopBar, effective_scale, set_semantic_widget_id, +}; +use crate::toolbar_gtk::GtkToolbarFeedback; +use crate::toolbar_gtk::widgets::{ + SliderRow, SwatchButton, install_quick_color_recolor, send_event, set_active_class, + sized_button, +}; +use crate::ui::toolbar::model::TopStripPlan; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot, model}; +use gtk4::prelude::*; +use std::cell::Cell; +use std::rc::Rc; /// Pill button on the shared `sized_button` chassis: non-focusable and /// releasing window keyboard focus on click, like every other top-bar @@ -116,7 +135,7 @@ impl TopBar { set_semantic_widget_id(&chip.button, control.id().as_ref()); let accessible_label = control.label(snapshot); chip.button - .update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + .update_property(&[Property::Label(&accessible_label)]); let sender = self.feedback.clone(); let event = control.click_event(snapshot); chip.button.connect_clicked(move |_| { @@ -140,7 +159,7 @@ impl TopBar { let accessible_label = control.label(snapshot); swatch .button - .update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + .update_property(&[Property::Label(&accessible_label)]); let sender = self.feedback.clone(); let event = control.click_event(snapshot); swatch.button.connect_clicked(move |_| { @@ -201,7 +220,7 @@ impl TopBar { let accessible_label = control.label(snapshot); slider .root - .update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + .update_property(&[Property::Label(&accessible_label)]); slider .root .set_tooltip_text(control.tooltip(snapshot).as_deref()); @@ -257,7 +276,7 @@ impl TopBar { if let Some(event) = slider.nudge_event(steps).filter(|_| steps != 0) { send_event(&scroll_sender, event); } - gtk4::glib::Propagation::Stop + Propagation::Stop }); button.add_controller(scroll); } @@ -269,7 +288,7 @@ impl TopBar { button.set_tooltip_text(Some(&tooltip)); } let accessible_label = control.label(snapshot); - button.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + button.update_property(&[Property::Label(&accessible_label)]); append_gap(&pill, button.upcast_ref(), gap); self.updaters.borrow_mut().push(Box::new(move |snapshot| { button.set_label(&control.required_value_text(snapshot)); @@ -384,7 +403,7 @@ impl TopBar { button.set_tooltip_text(Some(&tooltip)); } let accessible_label = control.label(snapshot); - button.update_property(&[gtk4::accessible::Property::Label(&accessible_label)]); + button.update_property(&[Property::Label(&accessible_label)]); let sender = self.feedback.clone(); let event = control.click_event(snapshot); button.connect_clicked(move |_| { @@ -481,7 +500,7 @@ impl OpacitySwatch { let draw_paint = paint.clone(); area.set_draw_func(move |_, ctx, width, height| { let paint = draw_paint.get(); - crate::toolbar_icons::draw_opacity_swatch( + toolbar_icons::draw_opacity_swatch( ctx, (0.0, 0.0, f64::from(width), f64::from(height)), paint.rgb, diff --git a/src/toolbar_gtk/view/top_bar/tests.rs b/src/toolbar_gtk/view/top_bar/tests.rs index ef518d085..e028c5daa 100644 --- a/src/toolbar_gtk/view/top_bar/tests.rs +++ b/src/toolbar_gtk/view/top_bar/tests.rs @@ -1,5 +1,8 @@ //! GTK top-strip unit tests. +use std::env; +use std::process::Command; + mod builtin_contract; mod builtin_style_nodes; mod capture_render; @@ -20,8 +23,8 @@ fn actual_gtk_widgets_match_the_shared_contract_without_presenting_a_window() { const CHILD_ENV: &str = "WAYSCRIBER_GTK_WIDGET_CONTRACT_CHILD"; const TEST_NAME: &str = "toolbar_gtk::view::top_bar::tests::actual_gtk_widgets_match_the_shared_contract_without_presenting_a_window"; - if std::env::var_os(CHILD_ENV).is_none() { - let status = std::process::Command::new(std::env::current_exe().expect("test binary")) + if env::var_os(CHILD_ENV).is_none() { + let status = Command::new(env::current_exe().expect("test binary")) .arg(TEST_NAME) .arg("--exact") .arg("--test-threads=1") @@ -35,7 +38,7 @@ fn actual_gtk_widgets_match_the_shared_contract_without_presenting_a_window() { if let Err(error) = gtk4::init() { assert!( - std::env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none(), + env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none(), "Required GTK widget coverage could not initialize: {error}" ); eprintln!("skipping GTK widget contract test: {error}"); diff --git a/src/toolbar_gtk/view/top_bar/tests/builtin_contract.rs b/src/toolbar_gtk/view/top_bar/tests/builtin_contract.rs index 0ce26af65..613d51e4a 100644 --- a/src/toolbar_gtk/view/top_bar/tests/builtin_contract.rs +++ b/src/toolbar_gtk/view/top_bar/tests/builtin_contract.rs @@ -1,5 +1,14 @@ //! Built-in toolbar order and semantic contract tests. +use std::ptr; + +use crate::backend::wayland; +use crate::backend::wayland::TopToolbarWidgetKind; +use crate::config::TopDisplayMode; +use crate::toolbar_icons; +use crate::ui::toolbar::ToolbarEvent; +use crate::ui_text::UiTextEngine; + use super::super::{ Tool, ToolbarLayoutMode, ToolbarSnapshot, TopStripPlan, model, plan_top_strip, top_toolbar_size, }; @@ -27,8 +36,8 @@ fn record_lane(record: &SemanticAdapterRecord) -> SemanticLane { } fn assert_builtin_node( - node: &crate::backend::wayland::TopToolbarWidgetKind, - interaction: Option<&crate::ui::toolbar::ToolbarEvent>, + node: &TopToolbarWidgetKind, + interaction: Option<&ToolbarEvent>, tooltip: Option<&str>, shortcut_badge: Option<&str>, expected: &SemanticAdapterRecord, @@ -50,10 +59,7 @@ fn assert_builtin_node( assert_builtin_node_kind(node, expected); } -fn assert_builtin_node_kind( - node: &crate::backend::wayland::TopToolbarWidgetKind, - expected: &SemanticControlRecord, -) { +fn assert_builtin_node_kind(node: &TopToolbarWidgetKind, expected: &SemanticControlRecord) { use crate::backend::wayland::TopToolbarWidgetKind as W; match node { @@ -63,9 +69,9 @@ fn assert_builtin_node_kind( style, } => { let icon = expected.icon.expect("semantic icon for icon button"); - assert!(std::ptr::fn_addr_eq( + assert!(ptr::fn_addr_eq( glyph.0, - crate::toolbar_icons::top_toolbar_icon_painter(icon) + toolbar_icons::top_toolbar_icon_painter(icon) )); assert_builtin_button_style(style.active, style.disabled, style.destructive, expected); } @@ -86,9 +92,9 @@ fn assert_builtin_node_kind( // has no glyph and shows its 1-based number label. Some(icon) => { let glyph = glyph.as_ref().expect("filled preset slot has a glyph"); - assert!(std::ptr::fn_addr_eq( + assert!(ptr::fn_addr_eq( glyph.0, - crate::toolbar_icons::top_toolbar_icon_painter(icon) + toolbar_icons::top_toolbar_icon_painter(icon) )); } None => { @@ -99,17 +105,17 @@ fn assert_builtin_node_kind( } W::MicroChip { glyph, .. } => { let icon = expected.icon.expect("semantic icon for micro chip"); - assert!(std::ptr::fn_addr_eq( + assert!(ptr::fn_addr_eq( glyph.0, - crate::toolbar_icons::top_toolbar_icon_painter(icon) + toolbar_icons::top_toolbar_icon_painter(icon) )); assert_eq!(expected.role, model::TopToolbarControlRole::Restore); } W::RestoreTab { glyph, label } => { let icon = expected.icon.expect("semantic icon for the restore tab"); - assert!(std::ptr::fn_addr_eq( + assert!(ptr::fn_addr_eq( glyph.0, - crate::toolbar_icons::top_toolbar_icon_painter(icon) + toolbar_icons::top_toolbar_icon_painter(icon) )); assert_eq!(label.text, expected.label); assert_eq!(expected.role, model::TopToolbarControlRole::Restore); @@ -142,9 +148,9 @@ fn builtin_semantic_records( snapshot: &ToolbarSnapshot, expected: &[SemanticAdapterRecord], ) -> Vec { - let (width, height) = top_toolbar_size(&crate::ui_text::UiTextEngine::default(), snapshot); - let tree = crate::backend::wayland::build_top_toolbar_view( - &crate::ui_text::UiTextEngine::default(), + let (width, height) = top_toolbar_size(&UiTextEngine::default(), snapshot); + let tree = wayland::build_top_toolbar_view( + &UiTextEngine::default(), snapshot, width as f64, height as f64, @@ -198,7 +204,7 @@ fn shared_spec_matches_builtin_order_and_full_semantics_without_starting_a_gui() let mut minimized = regular.clone(); minimized.top_minimized = true; let mut micro = regular.clone(); - micro.top_display_mode = crate::config::TopDisplayMode::Micro; + micro.top_display_mode = TopDisplayMode::Micro; let mut narrow = regular.clone(); narrow.top_viewport_max = Some(520.0); narrow.top_overflow_open = true; @@ -220,7 +226,7 @@ fn shared_spec_matches_builtin_order_and_full_semantics_without_starting_a_gui() ("shapes", shapes), ("highlighted", highlighted), ] { - let plan = plan_top_strip(&crate::ui_text::UiTextEngine::default(), &snapshot); + let plan = plan_top_strip(&UiTextEngine::default(), &snapshot); let spec = super::super::strip::top_toolbar_spec(&snapshot, &plan); let expected = expected_semantic_records(&snapshot, &spec, &plan); for record in &expected { @@ -356,7 +362,7 @@ fn style_pill_spec_matches_builtin_tree_across_morph_states() { let mut minimized = regular.clone(); minimized.top_minimized = true; let mut micro = regular.clone(); - micro.top_display_mode = crate::config::TopDisplayMode::Micro; + micro.top_display_mode = TopDisplayMode::Micro; for (name, snapshot) in [ ("regular", regular.clone()), @@ -387,11 +393,11 @@ fn style_pill_spec_matches_builtin_tree_across_morph_states() { } fn assert_builtin_style_pill_scenario(name: &str, snapshot: &ToolbarSnapshot) { - let plan = plan_top_strip(&crate::ui_text::UiTextEngine::default(), snapshot); + let plan = plan_top_strip(&UiTextEngine::default(), snapshot); let expected = expected_style_pill_nodes(snapshot, &plan); - let (width, height) = top_toolbar_size(&crate::ui_text::UiTextEngine::default(), snapshot); - let tree = crate::backend::wayland::build_top_toolbar_view( - &crate::ui_text::UiTextEngine::default(), + let (width, height) = top_toolbar_size(&UiTextEngine::default(), snapshot); + let tree = wayland::build_top_toolbar_view( + &UiTextEngine::default(), snapshot, width as f64, height as f64, diff --git a/src/toolbar_gtk/view/top_bar/tests/builtin_style_nodes.rs b/src/toolbar_gtk/view/top_bar/tests/builtin_style_nodes.rs index c29684ef3..e9d225102 100644 --- a/src/toolbar_gtk/view/top_bar/tests/builtin_style_nodes.rs +++ b/src/toolbar_gtk/view/top_bar/tests/builtin_style_nodes.rs @@ -1,5 +1,7 @@ //! Built-in style-pill node assertions. +use crate::backend::wayland::TopToolbarWidgetKind; + use super::super::{ToolbarEvent, ToolbarSnapshot, model}; use super::builtin_contract::StylePillNodeExpectation; use super::expectations::slider_opacity_paint; @@ -8,7 +10,7 @@ use super::expectations::slider_opacity_paint; pub(super) fn assert_builtin_style_pill_node( name: &str, snapshot: &ToolbarSnapshot, - kind: &crate::backend::wayland::TopToolbarWidgetKind, + kind: &TopToolbarWidgetKind, interaction_event: Option<&ToolbarEvent>, interaction_tooltip: Option<&str>, has_interaction: bool, @@ -65,7 +67,7 @@ pub(super) fn assert_builtin_style_pill_node( let zero = setting.zero_segment(snapshot, &control.id()); assert_eq!( kind, - &crate::backend::wayland::TopToolbarWidgetKind::MeterDot { + &TopToolbarWidgetKind::MeterDot { active: setting.level(snapshot) == 0, enabled: control.enabled(snapshot), } @@ -115,7 +117,7 @@ pub(super) fn assert_builtin_style_pill_node( fn assert_builtin_style_pill_control( name: &str, snapshot: &ToolbarSnapshot, - kind: &crate::backend::wayland::TopToolbarWidgetKind, + kind: &TopToolbarWidgetKind, interaction_event: Option<&ToolbarEvent>, interaction_tooltip: Option<&str>, has_interaction: bool, @@ -153,7 +155,7 @@ fn assert_builtin_style_pill_control( fn assert_builtin_style_pill_control_kind( name: &str, snapshot: &ToolbarSnapshot, - kind: &crate::backend::wayland::TopToolbarWidgetKind, + kind: &TopToolbarWidgetKind, interaction_event: Option<&ToolbarEvent>, has_interaction: bool, id: &str, @@ -248,7 +250,7 @@ fn assert_builtin_style_pill_control_kind( fn assert_builtin_style_pill_readout( name: &str, snapshot: &ToolbarSnapshot, - kind: &crate::backend::wayland::TopToolbarWidgetKind, + kind: &TopToolbarWidgetKind, has_interaction: bool, id: &str, control: model::StylePillControl, @@ -297,7 +299,7 @@ fn assert_builtin_style_pill_slider( fn assert_builtin_style_pill_step_half( name: &str, snapshot: &ToolbarSnapshot, - kind: &crate::backend::wayland::TopToolbarWidgetKind, + kind: &TopToolbarWidgetKind, interaction_event: Option<&ToolbarEvent>, interaction_tooltip: Option<&str>, id: &str, @@ -331,7 +333,7 @@ fn assert_builtin_style_pill_step_half( fn assert_builtin_style_pill_step_value( name: &str, snapshot: &ToolbarSnapshot, - kind: &crate::backend::wayland::TopToolbarWidgetKind, + kind: &TopToolbarWidgetKind, has_interaction: bool, id: &str, control: model::StylePillControl, @@ -353,7 +355,7 @@ fn assert_builtin_style_pill_step_value( fn assert_builtin_style_pill_meter_bar( name: &str, snapshot: &ToolbarSnapshot, - kind: &crate::backend::wayland::TopToolbarWidgetKind, + kind: &TopToolbarWidgetKind, interaction_event: Option<&ToolbarEvent>, interaction_tooltip: Option<&str>, id: &str, @@ -389,7 +391,7 @@ fn assert_builtin_style_pill_meter_bar( fn assert_builtin_style_pill_caption( name: &str, - kind: &crate::backend::wayland::TopToolbarWidgetKind, + kind: &TopToolbarWidgetKind, has_interaction: bool, id: &str, control: model::StylePillControl, @@ -414,7 +416,7 @@ fn assert_builtin_style_pill_caption( fn assert_builtin_style_pill_segment_half( name: &str, snapshot: &ToolbarSnapshot, - kind: &crate::backend::wayland::TopToolbarWidgetKind, + kind: &TopToolbarWidgetKind, interaction_event: Option<&ToolbarEvent>, interaction_tooltip: Option<&str>, id: &str, diff --git a/src/toolbar_gtk/view/top_bar/tests/capture_render.rs b/src/toolbar_gtk/view/top_bar/tests/capture_render.rs index e11631835..22e0a775c 100644 --- a/src/toolbar_gtk/view/top_bar/tests/capture_render.rs +++ b/src/toolbar_gtk/view/top_bar/tests/capture_render.rs @@ -6,6 +6,15 @@ mod popups; mod subprocess; mod support; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::mpsc; +use std::{env, process}; + +use gtk4::gdk::Display; + +use crate::toolbar_gtk::css; + use super::super::{FeedbackSender, TopBar}; use crate::toolbar_gtk::view::install_capture_css_provider; use gtk4::prelude::*; @@ -14,13 +23,13 @@ use support::*; #[test] fn capture_roots_override_user_css_and_restore_rendered_pixels() { - if std::env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none() - || std::env::var("WAYLAND_DISPLAY").as_deref() != Ok("wayscriber-widget-tests") + if env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none() + || env::var("WAYLAND_DISPLAY").as_deref() != Ok("wayscriber-widget-tests") { return; } - if std::env::var_os(CHILD_ENV).is_none() { + if env::var_os(CHILD_ENV).is_none() { let name = concat!( module_path!(), "::capture_roots_override_user_css_and_restore_rendered_pixels" @@ -42,9 +51,9 @@ fn capture_roots_override_user_css_and_restore_rendered_pixels() { gtk4::Settings::default() .unwrap() .set_gtk_xft_dpi(96 * 1024); - let display = gtk4::gdk::Display::default().unwrap(); + let display = Display::default().unwrap(); let application = gtk4::CssProvider::new(); - application.load_from_string(&crate::toolbar_gtk::css::stylesheet(1.0)); + application.load_from_string(&css::stylesheet(1.0)); gtk4::style_context_add_provider_for_display( &display, &application, @@ -52,7 +61,7 @@ fn capture_roots_override_user_css_and_restore_rendered_pixels() { ); let capture = install_capture_css_provider(1.0); let user = gtk4::CssProvider::new(); - let errors = std::rc::Rc::new(std::cell::RefCell::new(Vec::new())); + let errors = Rc::new(RefCell::new(Vec::new())); let observed = errors.clone(); user.connect_parsing_error(move |_, _, error| observed.borrow_mut().push(error.to_string())); gtk4::style_context_add_provider_for_display( @@ -61,12 +70,12 @@ fn capture_roots_override_user_css_and_restore_rendered_pixels() { gtk4::STYLE_PROVIDER_PRIORITY_USER, ); - let case = std::env::var(CHILD_ENV).unwrap(); + let case = env::var(CHILD_ENV).unwrap(); eprintln!( "capture case: {case}, renderer: {}", - std::env::var("GSK_RENDERER").unwrap() + env::var("GSK_RENDERER").unwrap() ); - let renderer = std::env::var("GSK_RENDERER").unwrap(); + let renderer = env::var("GSK_RENDERER").unwrap(); let mut outcome = Outcome::Passed; match case.as_str() { "renderer-probe" => { @@ -101,12 +110,12 @@ fn capture_roots_override_user_css_and_restore_rendered_pixels() { subprocess::complete(&renderer, &case, &outcome); if outcome == Outcome::VulkanUnavailable { - std::process::exit(77); + process::exit(77); } } fn roots(user: >k4::CssProvider, case: &str) { - let (tx, _) = std::sync::mpsc::channel(); + let (tx, _) = mpsc::channel(); let top = TopBar::new_for_test(FeedbackSender::new(tx)); top.root.set_size_request(160, 64); top.window.set_decorated(false); @@ -134,18 +143,22 @@ fn roots(user: >k4::CssProvider, case: &str) { } else { "" }; - user.load_from_string( - &(painted_css(selector, "0 2px 6px #58c0e8") - + &format!("{selector} {{ {transition} {image} {metrics} }}")), - ); + user.load_from_string(&format!( + "{}{selector} {{ {transition} {image} {metrics} }}", + painted_css(selector, "0 2px 6px #58c0e8") + )); if case == "animation" { - user.load_from_string(&(painted_css("window", "none") + r#" + let mut css = painted_css("window", "none"); + css.push_str( + r#" @keyframes capture-fixture { from { background-color: #23252e; } to { background-color: #e86b18; } } window { background-image: none; animation: capture-fixture 400ms linear infinite alternate; } - "#)); + "#, + ); + user.load_from_string(&css); } top.window.present(); wait_for_frame(&top.window); @@ -173,7 +186,7 @@ fn roots(user: >k4::CssProvider, case: &str) { top.capture_surface.set_transparent(true); top.capture_surface .widget() - .remove_css_class(crate::toolbar_gtk::css::CAPTURE_TRANSPARENT_CLASS); + .remove_css_class(css::CAPTURE_TRANSPARENT_CLASS); wait_for_frame(&top.window); assert_fixture(&pixels(&top.capture_surface.proof)); top.capture_surface.set_transparent(false); diff --git a/src/toolbar_gtk/view/top_bar/tests/capture_render/lifecycle.rs b/src/toolbar_gtk/view/top_bar/tests/capture_render/lifecycle.rs index 9d7443e67..ea61f33f7 100644 --- a/src/toolbar_gtk/view/top_bar/tests/capture_render/lifecycle.rs +++ b/src/toolbar_gtk/view/top_bar/tests/capture_render/lifecycle.rs @@ -1,11 +1,18 @@ -use super::{FeedbackSender, TopBar, support::*}; +use std::sync::mpsc; + +use gtk4::prelude::GtkWindowExt; + +use crate::input::state::test_support; + +use super::support::*; +use super::{FeedbackSender, TopBar}; use crate::toolbar_gtk::GtkToolbarUpdate; use crate::ui::toolbar::{ToolbarBindingHints, ToolbarSnapshot}; use gtk4::prelude::*; pub(super) fn apply_paths(user: >k4::CssProvider) { user.load_from_string(&painted_css("window", "0 2px 6px #58c0e8")); - let state = crate::input::state::test_support::make_test_input_state(); + let state = test_support::make_test_input_state(); let mut update = GtkToolbarUpdate { snapshot: ToolbarSnapshot::from_input_with_bindings( &state, @@ -23,7 +30,7 @@ pub(super) fn apply_paths(user: >k4::CssProvider) { capture_suppressed: false, capture_suppression_generation: None, }; - let (tx, _) = std::sync::mpsc::channel(); + let (tx, _) = mpsc::channel(); let mut top = TopBar::new_for_test(FeedbackSender::new(tx)); top.window.set_decorated(false); @@ -72,8 +79,8 @@ fn visible_capture_and_restore( // Weston may assign initial keyboard focus to the first slider after the // map callback. Capture releases that focus; compare an unfocused normal // toolbar so its Cairo focus ring is not part of the styling fixture. - gtk4::prelude::GtkWindowExt::set_focus(&top.window, gtk4::Widget::NONE); - assert!(gtk4::prelude::GtkWindowExt::focus(&top.window).is_none()); + GtkWindowExt::set_focus(&top.window, gtk4::Widget::NONE); + assert!(GtkWindowExt::focus(&top.window).is_none()); assert_renderer(&top.window); let normal = settled_pixels(&top.window); diff --git a/src/toolbar_gtk/view/top_bar/tests/capture_render/popups.rs b/src/toolbar_gtk/view/top_bar/tests/capture_render/popups.rs index c8ca01393..8780b0071 100644 --- a/src/toolbar_gtk/view/top_bar/tests/capture_render/popups.rs +++ b/src/toolbar_gtk/view/top_bar/tests/capture_render/popups.rs @@ -1,8 +1,11 @@ +use gtk4::gio::Menu; + +use crate::ui::theme::css::GtkStylesheetValues; + use super::support::*; +use crate::toolbar_gtk::view::capture_suppression::TooltipCapture; use crate::toolbar_gtk::view::top_bar::popovers::set_popover_capture_transparent; -use crate::toolbar_gtk::view::{ - CaptureProofTarget, CaptureSurfaceContent, capture_suppression::TooltipCapture, -}; +use crate::toolbar_gtk::view::{CaptureProofTarget, CaptureSurfaceContent}; use gtk4::prelude::*; fn anchor() -> (gtk4::Window, gtk4::Button) { @@ -21,7 +24,7 @@ fn anchor() -> (gtk4::Window, gtk4::Button) { } pub(super) fn popup(user: >k4::CssProvider, case: &str) { - let values = crate::ui::theme::css::GtkStylesheetValues::new(1.0); + let values = GtkStylesheetValues::new(1.0); let shadow = match case { "popup-none" => "none".to_string(), "popup-small" => "0 1px 2px #58c0e8".to_string(), @@ -49,10 +52,10 @@ pub(super) fn popup(user: >k4::CssProvider, case: &str) { // Shadow overrides are deliberately ignored in normal and capture modes. // Assert the policy before capture too, including smaller/absent shadows: // changing extents at capture time would withdraw this native surface. - user.load_from_string( - &(painted_css("popover, popover > contents, popover > arrow", "none") - + &format!("popover > contents {{ box-shadow: {shadow}; }}")), - ); + user.load_from_string(&format!( + "{}popover > contents {{ box-shadow: {shadow}; }}", + painted_css("popover, popover > contents, popover > arrow", "none") + )); wait_for_frame(&popup); assert_eq!( native_size(&popup), @@ -178,8 +181,10 @@ fn menu_css(case: &str) -> String { "menu" | "menu-decorated" => { // Preserve the adversarial fixtures that paint every GTK-owned // intermediary and explicitly zero the scrollbar's theme metrics. - let mut css = painted_css("popover, popover *", "none") - + "popover scrollbar, popover scrollbar * { min-width: 0; min-height: 0; padding: 0; margin: 0; }"; + let mut css = painted_css("popover, popover *", "none"); + css.push_str( + "popover scrollbar, popover scrollbar * { min-width: 0; min-height: 0; padding: 0; margin: 0; }", + ); if case == "menu-decorated" { css += "popover scrolledwindow { padding: 4px; margin: 2px; }"; } @@ -190,7 +195,7 @@ fn menu_css(case: &str) -> String { } fn menu_widget(anchor: >k4::Button, decorated: bool, long: bool) -> gtk4::PopoverMenu { - let model = gtk4::gio::Menu::new(); + let model = Menu::new(); if long { for item in 0..60 { model.append(Some(&format!("Copy item {item}")), Some("clipboard.copy")); diff --git a/src/toolbar_gtk/view/top_bar/tests/capture_render/subprocess.rs b/src/toolbar_gtk/view/top_bar/tests/capture_render/subprocess.rs index e20b65422..44c204a46 100644 --- a/src/toolbar_gtk/view/top_bar/tests/capture_render/subprocess.rs +++ b/src/toolbar_gtk/view/top_bar/tests/capture_render/subprocess.rs @@ -1,4 +1,5 @@ -use std::process::{Command, Output}; +use std::env; +use std::process::{Command, ExitStatus, Output}; pub(super) const CHILD_ENV: &str = "WAYSCRIBER_GTK_CAPTURE_RENDER_CHILD"; const COMPLETION_PREFIX: &str = "WAYSCRIBER_CAPTURE_RENDER_COMPLETED "; @@ -22,7 +23,7 @@ fn completion_marker(renderer: &str, case: &str, outcome: &Outcome) -> String { } fn child(name: &str, renderer: &str, case: &str) -> Command { - let mut command = Command::new(std::env::current_exe().expect("test binary")); + let mut command = Command::new(env::current_exe().expect("test binary")); command .args([name, "--exact", "--test-threads=1", "--nocapture"]) .env(CHILD_ENV, case) @@ -125,7 +126,7 @@ fn completion_requires_matching_case_renderer_outcome_and_exit_status() { use std::os::unix::process::ExitStatusExt; let mut output = Output { - status: std::process::ExitStatus::from_raw(0), + status: ExitStatus::from_raw(0), stdout: Vec::new(), stderr: Vec::new(), }; @@ -136,10 +137,10 @@ fn completion_requires_matching_case_renderer_outcome_and_exit_status() { output.stdout = output.stderr.clone(); assert!(validate(&output, "cairo", "window").is_err()); output.stdout.clear(); - output.status = std::process::ExitStatus::from_raw(1 << 8); + output.status = ExitStatus::from_raw(1 << 8); assert!(validate(&output, "cairo", "window").is_err()); - output.status = std::process::ExitStatus::from_raw(77 << 8); + output.status = ExitStatus::from_raw(77 << 8); output.stderr = completion_marker("vulkan", "renderer-probe", &Outcome::VulkanUnavailable).into_bytes(); assert_eq!( diff --git a/src/toolbar_gtk/view/top_bar/tests/capture_render/support.rs b/src/toolbar_gtk/view/top_bar/tests/capture_render/support.rs index ff1bbe21c..65ab7bdb4 100644 --- a/src/toolbar_gtk/view/top_bar/tests/capture_render/support.rs +++ b/src/toolbar_gtk/view/top_bar/tests/capture_render/support.rs @@ -1,12 +1,19 @@ -use crate::toolbar_gtk::view::{ - CaptureProofTarget, capture_suppression::wait_for_presented_transparency, - widget_native_is_mapped, -}; +use std::cell::Cell; +use std::rc::Rc; +use std::{env, thread}; + +use gtk4::gdk::FrameClockPhase; +use gtk4::glib::MainContext; +use gtk4::graphene::Rect; +use gtk4::gsk::{CairoRenderer, GLRenderer}; + +use crate::toolbar_gtk::view::capture_suppression::wait_for_presented_transparency; +use crate::toolbar_gtk::view::{CaptureProofTarget, widget_native_is_mapped}; use gtk4::prelude::*; use std::time::{Duration, Instant}; pub(super) fn prove(generation: u64, targets: Vec) { - gtk4::glib::MainContext::default() + MainContext::default() .block_on(wait_for_presented_transparency(generation, targets)) .expect("transparent native buffer presentation"); } @@ -19,9 +26,9 @@ pub(super) fn native_size(widget: &impl IsA) -> (i32, i32) { pub(super) fn assert_renderer(window: >k4::Window) { let renderer = window.renderer().unwrap(); - match std::env::var("GSK_RENDERER").unwrap().as_str() { - "cairo" => assert!(renderer.is::()), - "gl" => assert!(renderer.is::()), + match env::var("GSK_RENDERER").unwrap().as_str() { + "cairo" => assert!(renderer.is::()), + "gl" => assert!(renderer.is::()), "vulkan" => assert_eq!(renderer.type_().name(), "GskVulkanRenderer"), _ => unreachable!(), } @@ -127,7 +134,7 @@ pub(super) fn pixels(widget: &impl IsA) -> Vec<[u8; 4]> { }; // WidgetPaintable clips to the allocation. The margin checks transparent // render bounds only; native popup geometry is asserted separately. - let viewport = gtk4::graphene::Rect::new( + let viewport = Rect::new( -16.0, -16.0, widget.width() as f32 + 32.0, @@ -149,7 +156,7 @@ pub(super) fn pixels(widget: &impl IsA) -> Vec<[u8; 4]> { } pub(super) fn wait_for_frame(widget: &impl IsA) { - let context = gtk4::glib::MainContext::default(); + let context = MainContext::default(); let deadline = Instant::now() + Duration::from_secs(3); let clock = loop { while context.pending() { @@ -163,19 +170,19 @@ pub(super) fn wait_for_frame(widget: &impl IsA) { break clock; } assert!(Instant::now() < deadline, "GTK widget did not map"); - std::thread::sleep(Duration::from_millis(2)); + thread::sleep(Duration::from_millis(2)); }; - let painted = std::rc::Rc::new(std::cell::Cell::new(false)); + let painted = Rc::new(Cell::new(false)); let observed = painted.clone(); let handler = clock.connect_after_paint(move |_| observed.set(true)); widget.as_ref().queue_draw(); - clock.request_phase(gtk4::gdk::FrameClockPhase::PAINT); + clock.request_phase(FrameClockPhase::PAINT); while !painted.get() { while context.pending() { context.iteration(false); } assert!(Instant::now() < deadline, "GTK did not paint"); - std::thread::sleep(Duration::from_millis(2)); + thread::sleep(Duration::from_millis(2)); } clock.disconnect(handler); } diff --git a/src/toolbar_gtk/view/top_bar/tests/expectations.rs b/src/toolbar_gtk/view/top_bar/tests/expectations.rs index 3e1100a6e..358ee4ed8 100644 --- a/src/toolbar_gtk/view/top_bar/tests/expectations.rs +++ b/src/toolbar_gtk/view/top_bar/tests/expectations.rs @@ -1,5 +1,7 @@ //! Shared semantic expectations and snapshot scenarios. +use crate::input::{SelectionPropertyEntry, SelectionPropertyKind}; + use super::super::{Tool, ToolbarEvent, ToolbarSnapshot, TopStripPlan, model}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -132,10 +134,10 @@ pub(super) fn style_pill_tool_snapshot(base: &ToolbarSnapshot, tool: Tool) -> To fn selection_property_entry( label: &str, value: &str, - kind: crate::input::SelectionPropertyKind, + kind: SelectionPropertyKind, disabled: bool, -) -> crate::input::SelectionPropertyEntry { - crate::input::SelectionPropertyEntry { +) -> SelectionPropertyEntry { + SelectionPropertyEntry { label: label.to_string(), value: value.to_string(), kind, diff --git a/src/toolbar_gtk/view/top_bar/tests/gtk_contract.rs b/src/toolbar_gtk/view/top_bar/tests/gtk_contract.rs index f3c30f135..b1ecbcac9 100644 --- a/src/toolbar_gtk/view/top_bar/tests/gtk_contract.rs +++ b/src/toolbar_gtk/view/top_bar/tests/gtk_contract.rs @@ -1,5 +1,16 @@ //! GTK scenario semantics, islands, widths, and preset assertions. +use std::collections::BTreeMap; +use std::sync::mpsc; + +use gtk4::gdk::Display; + +use crate::config::{TopDisplayMode, toolbar_item_ids}; +use crate::draw::{Color, FontDescriptor}; +use crate::toolbar_gtk::css; +use crate::ui::toolbar::PresetSlotSnapshot; +use crate::ui_text::UiTextEngine; + use super::super::{ FeedbackSender, Tool, ToolbarLayoutMode, ToolbarSnapshot, TopBar, TopStripPlan, model, plan_top_strip, @@ -140,7 +151,7 @@ pub(super) fn assert_gtk_control_widget(widget: >k4::Widget, expected: &Semant if expected.role == model::TopToolbarControlRole::Destructive { assert!(surface.has_css_class("destructive"), "{}", expected.id); } - if expected.id == crate::config::toolbar_item_ids::TOP_CHROME_PIN.as_str() { + if expected.id == toolbar_item_ids::TOP_CHROME_PIN.as_str() { assert_eq!( surface.has_css_class("pinned"), expected.active, @@ -158,7 +169,7 @@ pub(super) fn assert_gtk_control_widget(widget: >k4::Widget, expected: &Semant ); } - if let Ok(button) = surface.clone().downcast::() + if let Ok(button) = surface.downcast::() && let Some(label) = button.label() { assert_eq!(label, expected.label, "{} text label", expected.id); @@ -187,8 +198,8 @@ fn font_button_natural_width( pub(super) fn install_gtk_contract_metrics() { let css_provider = gtk4::CssProvider::new(); - css_provider.load_from_string(&crate::toolbar_gtk::css::stylesheet(1.0)); - if let Some(display) = gtk4::gdk::Display::default() { + css_provider.load_from_string(&css::stylesheet(1.0)); + if let Some(display) = Display::default() { gtk4::style_context_add_provider_for_display( &display, &css_provider, @@ -216,7 +227,7 @@ pub(super) fn gtk_widget_contract_scenarios() -> ( let mut minimized = regular.clone(); minimized.top_minimized = true; let mut micro = regular.clone(); - micro.top_display_mode = crate::config::TopDisplayMode::Micro; + micro.top_display_mode = TopDisplayMode::Micro; let mut text = regular.clone(); text.use_icons = false; let mut highlighted = regular.clone(); @@ -248,7 +259,7 @@ pub(super) fn gtk_widget_contract_scenarios() -> ( text_mode.text_active = true; scenarios.push(("text-mode", text_mode.clone())); let mut long_font = text_mode; - long_font.font = crate::draw::FontDescriptor::new( + long_font.font = FontDescriptor::new( "Noto Sans Mono CJK JP ExtraCondensed Black".to_string(), "normal".to_string(), "normal".to_string(), @@ -261,8 +272,8 @@ pub(super) fn gtk_widget_contract_scenarios() -> ( pub(super) fn assert_gtk_widget_scenarios( scenarios: Vec<(&'static str, ToolbarSnapshot)>, -) -> std::collections::BTreeMap<&'static str, i32> { - let mut widths = std::collections::BTreeMap::new(); +) -> BTreeMap<&'static str, i32> { + let mut widths = BTreeMap::new(); for (name, snapshot) in scenarios { assert_gtk_widget_scenario(name, &snapshot, &mut widths); } @@ -270,7 +281,7 @@ pub(super) fn assert_gtk_widget_scenarios( } fn build_contract_top(snapshot: &ToolbarSnapshot, plan: &TopStripPlan) -> TopBar { - let (tx, _rx) = std::sync::mpsc::channel(); + let (tx, _rx) = mpsc::channel(); let mut top = TopBar::new_for_test(FeedbackSender::new(tx)); if snapshot.top_minimized { top.build_minimized(snapshot, plan); @@ -288,9 +299,9 @@ fn build_contract_top(snapshot: &ToolbarSnapshot, plan: &TopStripPlan) -> TopBar fn assert_gtk_widget_scenario( name: &'static str, snapshot: &ToolbarSnapshot, - widths: &mut std::collections::BTreeMap<&'static str, i32>, + widths: &mut BTreeMap<&'static str, i32>, ) { - let plan = plan_top_strip(&crate::ui_text::UiTextEngine::default(), snapshot); + let plan = plan_top_strip(&UiTextEngine::default(), snapshot); let spec = super::super::strip::top_toolbar_spec(snapshot, &plan); let expected = expected_semantic_records(snapshot, &spec, &plan); let style_controls = style_pill_controls(snapshot, &plan); @@ -336,7 +347,7 @@ fn assert_scenario_widget_semantics( expected: &[SemanticAdapterRecord], style_controls: &[(String, model::StylePillControl)], widgets: Vec, - widths: &mut std::collections::BTreeMap<&'static str, i32>, + widths: &mut BTreeMap<&'static str, i32>, ) { for widget in widgets { let id = widget.widget_name(); @@ -366,7 +377,7 @@ fn assert_scenario_widget_semantics( } } -pub(super) fn assert_font_button_width_stable(widths: &std::collections::BTreeMap<&str, i32>) { +pub(super) fn assert_font_button_width_stable(widths: &BTreeMap<&str, i32>) { let short = widths .get("text-mode") .copied() @@ -386,10 +397,10 @@ pub(super) fn assert_font_button_width_stable(widths: &std::collections::BTreeMa pub(super) fn assert_preset_slot_faces(regular: &ToolbarSnapshot) { let mut snapshot = regular.clone(); snapshot.presets = vec![None; 5]; - snapshot.presets[0] = Some(crate::ui::toolbar::PresetSlotSnapshot { + snapshot.presets[0] = Some(PresetSlotSnapshot { name: None, tool: Tool::Pen, - color: crate::draw::Color::new(1.0, 0.0, 0.0, 1.0), + color: Color::new(1.0, 0.0, 0.0, 1.0), size: 4.0, eraser_kind: None, eraser_mode: None, @@ -437,7 +448,7 @@ pub(super) fn assert_preset_slot_faces(regular: &ToolbarSnapshot) { pub(super) fn assert_compact_gtk_widget_contract(regular: &ToolbarSnapshot) { let mut compact_plan = TopStripPlan::unconstrained(); compact_plan.compact = true; - let (tx, _rx) = std::sync::mpsc::channel(); + let (tx, _rx) = mpsc::channel(); let mut compact_top = TopBar::new_for_test(FeedbackSender::new(tx)); compact_top.build_strip(regular, &compact_plan); let compact_ids = collect_semantic_widgets(compact_top.root.upcast_ref()) diff --git a/src/toolbar_gtk/view/top_bar/tests/interactions.rs b/src/toolbar_gtk/view/top_bar/tests/interactions.rs index 5194d87d2..c6661f737 100644 --- a/src/toolbar_gtk/view/top_bar/tests/interactions.rs +++ b/src/toolbar_gtk/view/top_bar/tests/interactions.rs @@ -1,5 +1,14 @@ //! Toolbar feedback and style-pill interaction tests. +use std::sync::mpsc; +use std::sync::mpsc::Receiver; + +use crate::config::QuickColorPalette; +use crate::input::EraserMode; +use crate::toolbar_gtk::css; +use crate::ui::toolbar::PrecisionEntryTarget; +use crate::ui_text::UiTextEngine; + use super::super::{ FeedbackSender, GtkToolbarFeedback, ICON_BUTTON, ICON_SIZE, PIN_BUTTON_SIZE, Tool, ToolbarEvent, ToolbarSnapshot, TopBar, model, plan_top_strip, @@ -12,7 +21,7 @@ use gtk4::prelude::*; use std::time::Duration; pub(super) fn assert_gtk_toggle_events(regular: &ToolbarSnapshot, highlighted: &ToolbarSnapshot) { - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let mut top = TopBar::new_for_test(FeedbackSender::new(tx)); let shape = top.shapes_picker_button( regular, @@ -116,7 +125,7 @@ fn assert_test_popover_capture_surfaces(top: &TopBar) { true, false, ); - assert!(popover.has_css_class(crate::toolbar_gtk::css::CAPTURE_TRANSPARENT_CLASS)); + assert!(popover.has_css_class(css::CAPTURE_TRANSPARENT_CLASS)); assert!(!popover.can_target()); assert_eq!(capture_surface.content_opacity(), Some(0.0)); assert!(capture_surface.proof_visible()); @@ -126,7 +135,7 @@ fn assert_test_popover_capture_surfaces(top: &TopBar) { false, true, ); - assert!(!popover.has_css_class(crate::toolbar_gtk::css::CAPTURE_TRANSPARENT_CLASS)); + assert!(!popover.has_css_class(css::CAPTURE_TRANSPARENT_CLASS)); assert!(popover.can_target()); assert_eq!(capture_surface.content_opacity(), Some(1.0)); assert!(!capture_surface.proof_visible()); @@ -134,7 +143,7 @@ fn assert_test_popover_capture_surfaces(top: &TopBar) { } fn assert_gtk_button_events( - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, cases: [(>k4::Button, ToolbarEvent); 4], ) { for (button, event) in cases { @@ -152,11 +161,11 @@ fn assert_gtk_button_events( fn assert_highlight_ring_event( top: &mut TopBar, highlighted: &ToolbarSnapshot, - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, ) { top.build_strip( highlighted, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), highlighted), + &plan_top_strip(&UiTextEngine::default(), highlighted), ); let ring = collect_semantic_widgets(top.root.upcast_ref()) .into_iter() @@ -181,7 +190,7 @@ fn pill_widget(top: &TopBar, id: &str) -> gtk4::Widget { } pub(super) fn assert_style_pill_interactions(regular: &ToolbarSnapshot) { - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let mut top = TopBar::new_for_test(FeedbackSender::new(tx)); assert_eraser_pill_interactions(&mut top, regular, &rx); assert_pen_pill_interactions(&mut top, regular, &rx); @@ -192,7 +201,7 @@ pub(super) fn assert_style_pill_interactions(regular: &ToolbarSnapshot) { fn assert_inline_stroke_level_clicks( top: &mut TopBar, regular: &ToolbarSnapshot, - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, ) { use crate::config::ToolbarStrokeControls::{Meter, Stepper}; @@ -218,7 +227,7 @@ fn assert_inline_stroke_level_clicks( snapshot.shape_recognition_sensitivity = 3; top.build_strip( &snapshot, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &snapshot), + &plan_top_strip(&UiTextEngine::default(), &snapshot), ); for control in controls { let setting = control.stroke_setting().unwrap(); @@ -251,13 +260,10 @@ fn assert_inline_stroke_level_clicks( fn assert_eraser_pill_interactions( top: &mut TopBar, regular: &ToolbarSnapshot, - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, ) { let eraser = style_pill_tool_snapshot(regular, Tool::Eraser); - top.build_strip( - &eraser, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &eraser), - ); + top.build_strip(&eraser, &plan_top_strip(&UiTextEngine::default(), &eraser)); let segment_row = pill_widget(top, "top.style.eraser-mode"); let mut halves = Vec::new(); let mut child = segment_row.first_child(); @@ -270,10 +276,7 @@ fn assert_eraser_pill_interactions( ); } assert_eq!(halves.len(), 2); - for (half, mode) in halves.iter().zip([ - crate::input::EraserMode::Brush, - crate::input::EraserMode::Stroke, - ]) { + for (half, mode) in halves.iter().zip([EraserMode::Brush, EraserMode::Stroke]) { half.emit_clicked(); assert_eq!( rx.recv_timeout(Duration::from_secs(1)) @@ -292,9 +295,7 @@ fn assert_eraser_pill_interactions( rx.recv_timeout(Duration::from_secs(1)) .expect("GTK numeral event"), GtkToolbarFeedback::Event { - event: ToolbarEvent::OpenPrecisionEntry( - crate::ui::toolbar::PrecisionEntryTarget::Thickness - ), + event: ToolbarEvent::OpenPrecisionEntry(PrecisionEntryTarget::Thickness), rebind_requested: false, } ); @@ -304,13 +305,10 @@ fn assert_eraser_pill_interactions( fn assert_pen_pill_interactions( top: &mut TopBar, regular: &ToolbarSnapshot, - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, ) { let pen = style_pill_tool_snapshot(regular, Tool::Pen); - top.build_strip( - &pen, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &pen), - ); + top.build_strip(&pen, &plan_top_strip(&UiTextEngine::default(), &pen)); pill_widget(top, "top.style.color-chip") .downcast::() .expect("chip button") @@ -333,7 +331,7 @@ fn assert_pen_pill_interactions( GtkToolbarFeedback::Event { event: ToolbarEvent::SetQuickColor { color: pen.quick_colors.rendered_entries()[1].color, - action: crate::config::QuickColorPalette::action_for_index(1), + action: QuickColorPalette::action_for_index(1), index: 1, }, rebind_requested: false, @@ -374,13 +372,10 @@ fn assert_pen_pill_interactions( fn assert_shape_pill_interaction( top: &mut TopBar, regular: &ToolbarSnapshot, - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, ) { let shape = style_pill_tool_snapshot(regular, Tool::Rect); - top.build_strip( - &shape, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &shape), - ); + top.build_strip(&shape, &plan_top_strip(&UiTextEngine::default(), &shape)); pill_widget(top, "top.style.fill") .downcast::() .expect("fill check button") diff --git a/src/toolbar_gtk/view/top_bar/tests/pane_popovers.rs b/src/toolbar_gtk/view/top_bar/tests/pane_popovers.rs index f4424021c..f5eb423e2 100644 --- a/src/toolbar_gtk/view/top_bar/tests/pane_popovers.rs +++ b/src/toolbar_gtk/view/top_bar/tests/pane_popovers.rs @@ -1,12 +1,19 @@ //! Session, settings, and canvas pane assertions. +use std::path::PathBuf; +use std::sync::mpsc; +use std::sync::mpsc::Receiver; + +use crate::ui::theme::toolbar; +use crate::ui::toolbar::SessionRecentSnapshot; +use crate::ui_text::UiTextEngine; + use super::super::{ FeedbackSender, GtkToolbarFeedback, ToolbarEvent, ToolbarLayoutMode, ToolbarSnapshot, TopBar, Updater, model, plan_top_strip, }; use super::widget_support::{collect_descendants, detach_test_popovers, find_widget_named}; -use crate::ui::toolbar::RuntimeUiPersistenceMode; -use crate::ui::toolbar::RuntimeUiPersistenceSnapshot; +use crate::ui::toolbar::{RuntimeUiPersistenceMode, RuntimeUiPersistenceSnapshot}; use gtk4::prelude::*; use std::time::Duration; @@ -15,20 +22,19 @@ pub(super) fn assert_menu_popover_contracts(regular: &ToolbarSnapshot) { let mut session_snapshot = regular.clone(); session_snapshot.session_popover_open = true; session_snapshot.active_session_name = Some("lecture.wayscriber-session".to_string()); - session_snapshot.active_session_path = - Some(std::path::PathBuf::from("/tmp/lecture.wayscriber-session")); - session_snapshot.recent_sessions = vec![crate::ui::toolbar::SessionRecentSnapshot { + session_snapshot.active_session_path = Some(PathBuf::from("/tmp/lecture.wayscriber-session")); + session_snapshot.recent_sessions = vec![SessionRecentSnapshot { display_name: "recent-0.wayscriber-session".to_string(), - path: std::path::PathBuf::from("/tmp/recent-0.wayscriber-session"), + path: PathBuf::from("/tmp/recent-0.wayscriber-session"), }]; session_snapshot.home_session_name = Some("home.wayscriber-session".to_string()); session_snapshot.at_home_session = false; - let (tx, menu_rx) = std::sync::mpsc::channel(); + let (tx, menu_rx) = mpsc::channel(); let mut menu_top = TopBar::new_for_test(FeedbackSender::new(tx)); // Building the strip creates the two overflow-anchored native popovers. menu_top.build_strip( &session_snapshot, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &session_snapshot), + &plan_top_strip(&UiTextEngine::default(), &session_snapshot), ); assert!(menu_top.session.mounted.is_some(), "session popover exists"); assert!( @@ -46,7 +52,7 @@ pub(super) fn assert_menu_popover_contracts(regular: &ToolbarSnapshot) { fn assert_session_popover_contract( top: &TopBar, snapshot: &ToolbarSnapshot, - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, ) { let model = model::ToolbarSessionModel::for_popover(snapshot).expect("session model"); let content = top.build_session_popover_content(snapshot, 1.0); @@ -102,7 +108,7 @@ fn assert_session_popover_contract( fn assert_settings_popover_contract( top: &TopBar, regular: &ToolbarSnapshot, - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, ) { let mut snapshot = regular.clone(); snapshot.layout_mode = ToolbarLayoutMode::Advanced; @@ -133,7 +139,7 @@ fn assert_settings_toggle_contract( model: &model::ToolbarSettingsModel, panel: >k4::Widget, updaters: &[Updater], - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, ) { let mut checks: Vec = Vec::new(); collect_descendants(panel, &mut checks); @@ -173,7 +179,7 @@ fn assert_settings_button_contract( snapshot: &ToolbarSnapshot, model: &model::ToolbarSettingsModel, panel: >k4::Widget, - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, ) { let mut buttons: Vec = Vec::new(); collect_descendants(panel, &mut buttons); @@ -233,7 +239,7 @@ fn assert_settings_button_contract( fn assert_canvas_popover_contract( top: &TopBar, regular: &ToolbarSnapshot, - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, ) { assert!(top.canvas.mounted.is_some(), "canvas popover exists"); assert_canvas_command_sections(top, regular, rx); @@ -244,7 +250,7 @@ fn assert_canvas_popover_contract( fn assert_canvas_command_sections( top: &TopBar, regular: &ToolbarSnapshot, - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, ) { let mut snapshot = regular.clone(); snapshot.canvas_popover_open = true; @@ -257,10 +263,7 @@ fn assert_canvas_command_sections( let (content, _) = top.build_canvas_popover_content(&snapshot, 1.0); let panel = find_widget_named(&content, "top.menu.canvas.panel").expect("canvas popover panel box"); - assert_eq!( - panel.width_request(), - crate::ui::theme::toolbar::CANVAS_MENU_CONTENT_W as i32 - ); + assert_eq!(panel.width_request(), toolbar::CANVAS_MENU_CONTENT_W as i32); assert_eq!(panel.margin_start(), 10); assert_eq!(panel.margin_end(), 10); let mut buttons: Vec = Vec::new(); diff --git a/src/toolbar_gtk/view/top_bar/tests/pen_feel.rs b/src/toolbar_gtk/view/top_bar/tests/pen_feel.rs index e41fdba73..c3cd1faad 100644 --- a/src/toolbar_gtk/view/top_bar/tests/pen_feel.rs +++ b/src/toolbar_gtk/view/top_bar/tests/pen_feel.rs @@ -1,5 +1,15 @@ //! Pen feel panel and delayed-snapshot meter-scroll assertions. +use std::sync::mpsc; +use std::sync::mpsc::Receiver; + +use gtk4::gdk::{Key, ModifierType}; + +use crate::config::ToolbarStrokeControls; +use crate::draw; +use crate::input::InputState; +use crate::ui_text::UiTextEngine; + use super::super::{ FeedbackSender, GtkToolbarFeedback, STYLE_METER_W, STYLE_ROW_H, Tool, ToolbarEvent, ToolbarSnapshot, TopBar, model, plan_top_strip, @@ -22,7 +32,7 @@ pub(super) fn assert_meter_scroll_survives_delayed_snapshots() { state.set_pen_smoothing(1); state.set_shape_recognition_sensitivity(1); let snapshot = ToolbarSnapshot::from_input(&state); - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let row = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); let refresh = super::super::meter::append_meter_bars( &FeedbackSender::new(tx), @@ -45,7 +55,7 @@ pub(super) fn assert_meter_scroll_survives_delayed_snapshots() { .ok() }) .expect("meter scroll controller"); - let scroll = |state: &mut crate::input::InputState, dy: f64, expected: u8| { + let scroll = |state: &mut InputState, dy: f64, expected: u8| { assert!(wheel.emit_by_name::("scroll", &[&0.0f64, &dy])); let GtkToolbarFeedback::Event { event, .. } = rx .recv_timeout(Duration::from_secs(1)) @@ -85,15 +95,15 @@ fn named>(root: &impl IsA, id: &str) -> W { /// level events), the live preview, and the level's name and hint, which /// the panel's updaters keep current while it stays open. pub(super) fn assert_pen_feel_contract(regular: &ToolbarSnapshot) { - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let mut top = TopBar::new_for_test(FeedbackSender::new(tx)); let mut shape_pen = style_pill_tool_snapshot(regular, Tool::LiveShape); - shape_pen.stroke_controls = crate::config::ToolbarStrokeControls::Panel; + shape_pen.stroke_controls = ToolbarStrokeControls::Panel; shape_pen.pen_smoothing = 3; shape_pen.shape_recognition_sensitivity = 3; top.build_strip( &shape_pen, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &shape_pen), + &plan_top_strip(&UiTextEngine::default(), &shape_pen), ); assert!(top.feel.mounted.is_some(), "the chip carries its panel"); assert_pen_feel_popover_relays_keys(&top, &rx); @@ -129,7 +139,7 @@ pub(super) fn assert_pen_feel_contract(regular: &ToolbarSnapshot) { // Levels change while the panel stays open; nothing rebuilds. let mut changed = open.clone(); - changed.pen_smoothing = crate::draw::MAX_PEN_SMOOTHING; + changed.pen_smoothing = draw::MAX_PEN_SMOOTHING; changed.shape_recognition_sensitivity = 0; for updater in &updaters { updater(&changed); @@ -145,7 +155,7 @@ pub(super) fn assert_pen_feel_contract(regular: &ToolbarSnapshot) { ] { assert_eq!(named::(&content, id).text(), text, "{id}"); } - for bar in 1..=crate::draw::MAX_PEN_SMOOTHING { + for bar in 1..=draw::MAX_PEN_SMOOTHING { let id = format!("top.feel.smoothing.level-{bar}"); assert!(named::(&content, &id).has_css_class("filled")); } @@ -164,10 +174,7 @@ pub(super) fn assert_pen_feel_contract(regular: &ToolbarSnapshot) { /// Keys that reach the open panel go to the overlay like the other menus': /// Escape closes it, and a shortcut closes it and runs. Switching input on /// every snapshot leaves the popover targetable. -fn assert_pen_feel_popover_relays_keys( - top: &TopBar, - rx: &std::sync::mpsc::Receiver, -) { +fn assert_pen_feel_popover_relays_keys(top: &TopBar, rx: &Receiver) { use crate::toolbar_gtk::widgets::key_relay_controller; use gtk4::glib::translate::IntoGlib; @@ -178,11 +185,9 @@ fn assert_pen_feel_popover_relays_keys( assert!(resources.popover.can_target(), "the panel takes input"); let relay = key_relay_controller(&resources.popover).expect("the panel relays keys"); - for keyval in [gtk4::gdk::Key::Escape, gtk4::gdk::Key::s] { - let handled = relay.emit_by_name::( - "key-pressed", - &[&keyval, &0u32, >k4::gdk::ModifierType::empty()], - ); + for keyval in [Key::Escape, Key::s] { + let handled = + relay.emit_by_name::("key-pressed", &[&keyval, &0u32, &ModifierType::empty()]); assert!(handled, "{keyval:?} leaves the panel for the overlay"); assert_eq!( rx.recv_timeout(Duration::from_secs(1)) @@ -201,7 +206,7 @@ fn assert_pen_feel_popover_relays_keys( fn assert_pen_feel_section( content: >k4::Box, section: &model::PenFeelSection, - rx: &std::sync::mpsc::Receiver, + rx: &Receiver, ) { let key = section.setting.panel_key(); assert_eq!( diff --git a/src/toolbar_gtk/view/top_bar/tests/pen_feel_render.rs b/src/toolbar_gtk/view/top_bar/tests/pen_feel_render.rs index a5c65760b..01916db1d 100644 --- a/src/toolbar_gtk/view/top_bar/tests/pen_feel_render.rs +++ b/src/toolbar_gtk/view/top_bar/tests/pen_feel_render.rs @@ -1,5 +1,17 @@ //! Actual GTK snapshots on the repository's isolated headless compositor. +use std::cell::Cell; +use std::process::Command; +use std::rc::Rc; +use std::sync::mpsc; +use std::{env, thread}; + +use gtk4::gdk::Display; +use gtk4::glib::MainContext; +use gtk4::graphene::Point; + +use crate::toolbar_gtk::css; + use super::super::{FeedbackSender, ToolbarSnapshot, TopBar}; use super::gtk_contract::install_gtk_contract_metrics; use super::widget_support::find_widget_named; @@ -11,16 +23,16 @@ use std::time::{Duration, Instant}; fn gtk_pen_feel_headless_render_at_fractional_scale() { // A window is allowed only on the dedicated test compositor, never on // the user's display. Ordinary library runs skip this render check. - if std::env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none() - || std::env::var("WAYLAND_DISPLAY").as_deref() != Ok("wayscriber-widget-tests") + if env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none() + || env::var("WAYLAND_DISPLAY").as_deref() != Ok("wayscriber-widget-tests") { return; } const CHILD_ENV: &str = "WAYSCRIBER_GTK_PEN_RENDER_CHILD"; const TEST_NAME: &str = "toolbar_gtk::view::top_bar::tests::pen_feel_render::gtk_pen_feel_headless_render_at_fractional_scale"; - if std::env::var_os(CHILD_ENV).is_none() { - let status = std::process::Command::new(std::env::current_exe().expect("test binary")) + if env::var_os(CHILD_ENV).is_none() { + let status = Command::new(env::current_exe().expect("test binary")) .args([TEST_NAME, "--exact", "--test-threads=1", "--nocapture"]) .env(CHILD_ENV, "1") .status() @@ -34,13 +46,13 @@ fn gtk_pen_feel_headless_render_at_fractional_scale() { let mut state = make_test_input_state(); state.set_pen_smoothing(3); let snapshot = ToolbarSnapshot::from_input(&state); - let (tx, _) = std::sync::mpsc::channel(); + let (tx, _) = mpsc::channel(); let top = TopBar::new_for_test(FeedbackSender::new(tx)); for scale in [1.0, 5.0 / 3.0] { let provider = gtk4::CssProvider::new(); - provider.load_from_string(&crate::toolbar_gtk::css::stylesheet(scale)); - let display = gtk4::gdk::Display::default().unwrap(); + provider.load_from_string(&css::stylesheet(scale)); + let display = Display::default().unwrap(); gtk4::style_context_add_provider_for_display( &display, &provider, @@ -55,7 +67,7 @@ fn gtk_pen_feel_headless_render_at_fractional_scale() { let dot = find_widget_named(content.upcast_ref(), "top.feel.smoothing.level-0") .expect("zero dot"); let origin = dot - .compute_point(&content, >k4::graphene::Point::new(0.0, 0.0)) + .compute_point(&content, &Point::new(0.0, 0.0)) .expect("dot in panel"); let controllers = dot.observe_controllers(); let motion = (0..controllers.n_items()) @@ -122,7 +134,7 @@ fn gtk_pen_feel_headless_render_at_fractional_scale() { } fn wait_for_frame(widget: &impl IsA) { - let context = gtk4::glib::MainContext::default(); + let context = MainContext::default(); let deadline = Instant::now() + Duration::from_secs(3); let clock = loop { while context.pending() { @@ -132,9 +144,9 @@ fn wait_for_frame(widget: &impl IsA) { break clock; } assert!(Instant::now() < deadline, "GTK widget has no frame clock"); - std::thread::sleep(Duration::from_millis(2)); + thread::sleep(Duration::from_millis(2)); }; - let painted = std::rc::Rc::new(std::cell::Cell::new(false)); + let painted = Rc::new(Cell::new(false)); let observed = painted.clone(); let handler = clock.connect_after_paint(move |_| observed.set(true)); widget.as_ref().queue_draw(); @@ -146,7 +158,7 @@ fn wait_for_frame(widget: &impl IsA) { Instant::now() < deadline, "GTK did not paint the requested frame" ); - std::thread::sleep(Duration::from_millis(2)); + thread::sleep(Duration::from_millis(2)); } clock.disconnect(handler); } diff --git a/src/toolbar_gtk/view/top_bar/tests/structure.rs b/src/toolbar_gtk/view/top_bar/tests/structure.rs index 284536cfd..53c1962b4 100644 --- a/src/toolbar_gtk/view/top_bar/tests/structure.rs +++ b/src/toolbar_gtk/view/top_bar/tests/structure.rs @@ -1,18 +1,19 @@ //! Structure, content-key, and width-plan tests. +use crate::config::ToolbarStrokeControls; +use crate::ui_text::UiTextEngine; + use super::super::{ Action, CanvasMenuContentKey, SessionMenuContentKey, SettingsMenuContentKey, StructureKey, Tool, ToolbarLayoutMode, ToolbarSnapshot, TopStripPlan, model, plan_top_strip, top_default_width, top_toolbar_size, }; use super::expectations::style_pill_tool_snapshot; -use crate::config::Shortcut; -use crate::config::ToolbarSectionFlag; -use crate::config::toolbar_item_ids as ids; +use crate::config::{Shortcut, ToolbarSectionFlag, toolbar_item_ids as ids}; use crate::input::state::test_support::make_test_input_state; -use crate::ui::toolbar::RuntimeUiPersistenceMode; -use crate::ui::toolbar::RuntimeUiPersistenceSnapshot; -use crate::ui::toolbar::ToolbarBindingHints; +use crate::ui::toolbar::{ + RuntimeUiPersistenceMode, RuntimeUiPersistenceSnapshot, ToolbarBindingHints, +}; use std::collections::HashMap; #[test] @@ -22,7 +23,7 @@ fn top_structure_rebuilds_when_current_shortcuts_change() { &state, ToolbarBindingHints::from_input_state(&state), ); - let initial_plan = plan_top_strip(&crate::ui_text::UiTextEngine::default(), &initial); + let initial_plan = plan_top_strip(&UiTextEngine::default(), &initial); let initial_key = StructureKey::of(&initial, &initial_plan); state.set_action_bindings(HashMap::from([( @@ -33,7 +34,7 @@ fn top_structure_rebuilds_when_current_shortcuts_change() { &state, ToolbarBindingHints::from_input_state(&state), ); - let changed_plan = plan_top_strip(&crate::ui_text::UiTextEngine::default(), &changed); + let changed_plan = plan_top_strip(&UiTextEngine::default(), &changed); let changed_key = StructureKey::of(&changed, &changed_plan); assert!(initial_key != changed_key); @@ -204,10 +205,7 @@ fn top_structure_ignores_popover_only_section_visibility_changes() { &state, ToolbarBindingHints::from_input_state(&state), ); - let base_key = StructureKey::of( - &base, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &base), - ); + let base_key = StructureKey::of(&base, &plan_top_strip(&UiTextEngine::default(), &base)); for flag in [ ToolbarSectionFlag::Actions, @@ -243,7 +241,7 @@ fn top_structure_ignores_popover_only_section_visibility_changes() { changed.resolved_toolbar_items.shown.remove(&flag.item_id()); let changed_key = StructureKey::of( &changed, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &changed), + &plan_top_strip(&UiTextEngine::default(), &changed), ); assert!( base_key == changed_key, @@ -270,13 +268,11 @@ fn top_structure_still_tracks_top_item_visibility() { .remove(&ids::TOP_TOOL_PEN); assert!( - StructureKey::of( - &base, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &base) - ) != StructureKey::of( - &changed, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &changed) - ), + StructureKey::of(&base, &plan_top_strip(&UiTextEngine::default(), &base)) + != StructureKey::of( + &changed, + &plan_top_strip(&UiTextEngine::default(), &changed) + ), "top-item visibility must still rebuild the top bar" ); } @@ -375,12 +371,12 @@ fn simple_layout_requests_its_smaller_natural_width() { ToolbarBindingHints::from_input_state(&state), ); - let regular_width = top_default_width(&crate::ui_text::UiTextEngine::default(), ®ular); - let simple_width = top_default_width(&crate::ui_text::UiTextEngine::default(), &simple); + let regular_width = top_default_width(&UiTextEngine::default(), ®ular); + let simple_width = top_default_width(&UiTextEngine::default(), &simple); assert!(simple_width < regular_width); assert_eq!( simple_width, - top_toolbar_size(&crate::ui_text::UiTextEngine::default(), &simple).0 as i32 + top_toolbar_size(&UiTextEngine::default(), &simple).0 as i32 ); } @@ -393,14 +389,14 @@ fn degraded_layout_requests_the_selected_plan_width() { ); snapshot.top_viewport_max = Some(700.0); - let plan = plan_top_strip(&crate::ui_text::UiTextEngine::default(), &snapshot); + let plan = plan_top_strip(&UiTextEngine::default(), &snapshot); let degraded = plan.compact || plan.drop_presets || !plan.dropped_tools.is_empty() || !plan.dropped_utilities.is_empty() || plan.swatch_count < 8; assert!(degraded, "the 700px budget must degrade the plan: {plan:?}"); - assert!(top_default_width(&crate::ui_text::UiTextEngine::default(), &snapshot) <= 700); + assert!(top_default_width(&UiTextEngine::default(), &snapshot) <= 700); } /// Colors left the strip for the pill (M7-C1); the presets island is the new @@ -444,14 +440,8 @@ fn top_structure_rebuilds_when_the_style_pill_morphs() { let pen = style_pill_tool_snapshot(®ular, Tool::Pen); let eraser = style_pill_tool_snapshot(®ular, Tool::Eraser); - let pen_key = StructureKey::of( - &pen, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &pen), - ); - let eraser_key = StructureKey::of( - &eraser, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &eraser), - ); + let pen_key = StructureKey::of(&pen, &plan_top_strip(&UiTextEngine::default(), &pen)); + let eraser_key = StructureKey::of(&eraser, &plan_top_strip(&UiTextEngine::default(), &eraser)); assert!( pen_key != eraser_key, "a pill morph change must rebuild the GTK bar structure" @@ -463,7 +453,7 @@ fn top_structure_rebuilds_when_the_style_pill_morphs() { thicker.thickness += 3.0; let thicker_key = StructureKey::of( &thicker, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &thicker), + &plan_top_strip(&UiTextEngine::default(), &thicker), ); assert!(pen_key == thicker_key, "value churn must not rebuild"); @@ -472,7 +462,7 @@ fn top_structure_rebuilds_when_the_style_pill_morphs() { let key_of = |snapshot: &ToolbarSnapshot| { StructureKey::of( snapshot, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), snapshot), + &plan_top_strip(&UiTextEngine::default(), snapshot), ) }; let styled = |style| { @@ -480,7 +470,7 @@ fn top_structure_rebuilds_when_the_style_pill_morphs() { snapshot.stroke_controls = style; key_of(&snapshot) }; - let [panel, meter, stepper] = crate::config::ToolbarStrokeControls::ALL.map(styled); + let [panel, meter, stepper] = ToolbarStrokeControls::ALL.map(styled); assert!(panel != meter && meter != stepper && panel != stepper); let mut open = pen.clone(); open.pen_feel_open = true; diff --git a/src/toolbar_gtk/view/top_bar/tests/toolbar_popovers.rs b/src/toolbar_gtk/view/top_bar/tests/toolbar_popovers.rs index 54f291e8b..e7b7886b6 100644 --- a/src/toolbar_gtk/view/top_bar/tests/toolbar_popovers.rs +++ b/src/toolbar_gtk/view/top_bar/tests/toolbar_popovers.rs @@ -1,5 +1,12 @@ //! Shapes, overflow, layout, arrow-style, and key-relay assertions. +use std::sync::mpsc; + +use gtk4::gdk::{Key, ModifierType}; + +use crate::draw::ArrowStyle; +use crate::ui_text::UiTextEngine; + use super::super::{ FeedbackSender, GtkToolbarFeedback, ICON_BUTTON, ICON_SIZE, Tool, ToolbarEvent, ToolbarSnapshot, TopBar, TopStripPlan, model, plan_top_strip, @@ -23,7 +30,7 @@ fn assert_shapes_popover_contract(regular: &ToolbarSnapshot) { let mut shapes = regular.clone(); shapes.shape_picker_open = true; shapes.active_tool = Tool::RegularPolygon; - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let top = TopBar::new_for_test(FeedbackSender::new(tx)); let content = top.build_shapes_popover_content(&shapes, (ICON_BUTTON, ICON_BUTTON), ICON_SIZE, true, 1.0); @@ -102,7 +109,7 @@ fn assert_shapes_popover_contract(regular: &ToolbarSnapshot) { } fn assert_overflow_popover_contract(regular: &ToolbarSnapshot) { - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let top = TopBar::new_for_test(FeedbackSender::new(tx)); let mut plan = TopStripPlan::unconstrained(); plan.dropped_tools = vec![Tool::Line, Tool::Arrow]; @@ -152,12 +159,9 @@ fn assert_overflow_popover_contract(regular: &ToolbarSnapshot) { /// The layout button opens a preset menu built from the shared entries; a /// row applies its preset. No click cycles presets any more. pub(super) fn assert_layout_menu_contract(regular: &ToolbarSnapshot) { - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let mut top = TopBar::new_for_test(FeedbackSender::new(tx)); - top.build_strip( - regular, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), regular), - ); + top.build_strip(regular, &plan_top_strip(&UiTextEngine::default(), regular)); assert!( top.layout.mounted.is_some(), "the layout menu popover exists" @@ -203,12 +207,9 @@ pub(super) fn assert_layout_menu_contract(regular: &ToolbarSnapshot) { /// style. The chip no longer cycles. pub(super) fn assert_arrow_style_menu_contract(regular: &ToolbarSnapshot) { let arrow = style_pill_tool_snapshot(regular, Tool::Arrow); - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let mut top = TopBar::new_for_test(FeedbackSender::new(tx)); - top.build_strip( - &arrow, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), &arrow), - ); + top.build_strip(&arrow, &plan_top_strip(&UiTextEngine::default(), &arrow)); assert!( top.arrow_style.mounted.is_some(), "the arrow style menu popover exists" @@ -228,7 +229,7 @@ pub(super) fn assert_arrow_style_menu_contract(regular: &ToolbarSnapshot) { ); let mut open = arrow.clone(); - open.arrow_style = crate::draw::ArrowStyle::Double; + open.arrow_style = ArrowStyle::Double; open.arrow_style_menu_open = true; let content = top.build_arrow_style_menu_content(&open, 1.0); for entry in model::arrow_style_menu_entries(open.arrow_style) { @@ -268,12 +269,9 @@ pub(super) fn assert_key_relay_contract(regular: &ToolbarSnapshot) { use crate::toolbar_gtk::widgets::key_relay_controller; use gtk4::glib::translate::IntoGlib; - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let mut top = TopBar::new_for_test(FeedbackSender::new(tx)); - top.build_strip( - regular, - &plan_top_strip(&crate::ui_text::UiTextEngine::default(), regular), - ); + top.build_strip(regular, &plan_top_strip(&UiTextEngine::default(), regular)); let window_relay = key_relay_controller(&top.window).expect("toolbar window relays keys"); for (name, resources) in top.popover_resources() { @@ -284,12 +282,8 @@ pub(super) fn assert_key_relay_contract(regular: &ToolbarSnapshot) { } for (keyval, state, expected_shift) in [ - ( - gtk4::gdk::Key::Escape, - gtk4::gdk::ModifierType::empty(), - false, - ), - (gtk4::gdk::Key::S, gtk4::gdk::ModifierType::SHIFT_MASK, true), + (Key::Escape, ModifierType::empty(), false), + (Key::S, ModifierType::SHIFT_MASK, true), ] { let handled = window_relay.emit_by_name::("key-pressed", &[&keyval, &0u32, &state]); assert!(handled, "{keyval:?} is consumed by the relay"); @@ -306,14 +300,8 @@ pub(super) fn assert_key_relay_contract(regular: &ToolbarSnapshot) { ); } - let handled = window_relay.emit_by_name::( - "key-pressed", - &[ - >k4::gdk::Key::Tab, - &0u32, - >k4::gdk::ModifierType::empty(), - ], - ); + let handled = window_relay + .emit_by_name::("key-pressed", &[&Key::Tab, &0u32, &ModifierType::empty()]); assert!(!handled, "Tab stays with GTK focus navigation"); assert!(rx.try_recv().is_err(), "Tab is not relayed"); diff --git a/src/toolbar_gtk/view/top_bar/tests/widget_support.rs b/src/toolbar_gtk/view/top_bar/tests/widget_support.rs index 3d6c85883..d65758c8b 100644 --- a/src/toolbar_gtk/view/top_bar/tests/widget_support.rs +++ b/src/toolbar_gtk/view/top_bar/tests/widget_support.rs @@ -1,9 +1,11 @@ //! GTK widget traversal, accessibility, and lifecycle helpers. +use gtk4::ffi; +use gtk4::glib::ffi as glib_ffi; + use super::super::TopBar; use gtk4::prelude::*; -use std::ffi::CStr; -use std::ffi::CString; +use std::ffi::{CStr, CString}; pub(super) fn collect_semantic_widgets(root: >k4::Widget) -> Vec { fn visit(widget: >k4::Widget, widgets: &mut Vec) { @@ -55,20 +57,27 @@ pub(super) fn assert_accessible_label(widget: >k4::Widget, expected: &str, id: let expected = CString::new(expected).expect("accessible label contains no NUL"); // GTK returns a newly allocated diagnostic string on mismatch and null // when the live accessible property has the requested value. + // SAFETY: every GtkWidget implements GtkAccessible, so the borrowed + // widget pointer is a valid accessible for the call. The LABEL property + // takes one NUL-terminated string vararg, and `expected` outlives the call. let mismatch = unsafe { - gtk4::ffi::gtk_test_accessible_check_property( + ffi::gtk_test_accessible_check_property( widget.as_ptr().cast(), - gtk4::ffi::GTK_ACCESSIBLE_PROPERTY_LABEL, + ffi::GTK_ACCESSIBLE_PROPERTY_LABEL, expected.as_ptr(), ) }; if mismatch.is_null() { return; } + // SAFETY: `mismatch` is the non-null, NUL-terminated string GTK just + // allocated; it is freed only after `message` copies it. let message = unsafe { CStr::from_ptr(mismatch) } .to_string_lossy() .into_owned(); - unsafe { gtk4::glib::ffi::g_free(mismatch.cast()) }; + // SAFETY: GTK transferred ownership of the string, which is freed once + // here and not used afterwards. + unsafe { glib_ffi::g_free(mismatch.cast()) }; panic!("{id} accessible label: {message}"); } diff --git a/src/toolbar_gtk/widgets.rs b/src/toolbar_gtk/widgets.rs index 6f6beeb08..e8768b14f 100644 --- a/src/toolbar_gtk/widgets.rs +++ b/src/toolbar_gtk/widgets.rs @@ -2,8 +2,11 @@ use std::cell::Cell; use std::rc::Rc; +#[cfg(test)] +use std::sync::mpsc::Sender; use gtk4::prelude::*; +use gtk4::{gdk, glib}; use gtk4_layer_shell::{KeyboardMode, LayerShell}; use super::GtkToolbarFeedback; @@ -17,11 +20,15 @@ use crate::ui::theme::toolbar::COLOR_PANEL_BACKGROUND; use crate::ui::toolbar::ToolbarEvent; mod key_relay; mod slider; +use gtk4::gdk::ModifierType; +use gtk4::prelude::GtkWindowExt; pub(super) use key_relay::install_key_relay; #[cfg(test)] pub(super) use key_relay::key_relay_controller; pub(super) use slider::SliderRow; +use crate::config::keybindings; +use crate::ui; pub(super) use crate::ui::theme::toolbar::COLOR_SWATCH_HAIRLINE; /// Sender the view hands to every control closure. Clones share the configured /// rebind chord and modifier state captured from the actual GTK click. @@ -48,7 +55,7 @@ impl FeedbackSink for FeedbackPublisher { } #[cfg(test)] -impl FeedbackSink for std::sync::mpsc::Sender { +impl FeedbackSink for Sender { fn publish(&self, feedback: GtkToolbarFeedback) -> bool { self.send(feedback).is_ok() } @@ -76,14 +83,14 @@ impl FeedbackSender { } } - fn capture_click_modifiers(&self, state: gtk4::gdk::ModifierType) { + fn capture_click_modifiers(&self, state: ModifierType) { self.click_in_progress.set(true); self.click_shift - .set(state.contains(gtk4::gdk::ModifierType::SHIFT_MASK)); + .set(state.contains(ModifierType::SHIFT_MASK)); if self.rebind_modifier.get().matches( - state.contains(gtk4::gdk::ModifierType::CONTROL_MASK), - state.contains(gtk4::gdk::ModifierType::SHIFT_MASK), - state.contains(gtk4::gdk::ModifierType::ALT_MASK), + state.contains(ModifierType::CONTROL_MASK), + state.contains(ModifierType::SHIFT_MASK), + state.contains(ModifierType::ALT_MASK), ) { self.click_rebind_requested.set(true); } @@ -102,22 +109,19 @@ impl FeedbackSender { } } -fn gdk_pointer_modifiers(state: gtk4::gdk::ModifierType) -> (bool, bool, bool, bool) { +fn gdk_pointer_modifiers(state: ModifierType) -> (bool, bool, bool, bool) { ( - state.contains(gtk4::gdk::ModifierType::CONTROL_MASK), - state.contains(gtk4::gdk::ModifierType::SHIFT_MASK), - state.contains(gtk4::gdk::ModifierType::ALT_MASK), - state.contains(gtk4::gdk::ModifierType::SUPER_MASK) - || state.contains(gtk4::gdk::ModifierType::META_MASK) - || state.contains(gtk4::gdk::ModifierType::HYPER_MASK), + state.contains(ModifierType::CONTROL_MASK), + state.contains(ModifierType::SHIFT_MASK), + state.contains(ModifierType::ALT_MASK), + state.contains(ModifierType::SUPER_MASK) + || state.contains(ModifierType::META_MASK) + || state.contains(ModifierType::HYPER_MASK), ) } -fn pointer_shortcut_feedback( - button: u32, - state: gtk4::gdk::ModifierType, -) -> Option { - crate::config::keybindings::gdk::pointer_button(button)?; +fn pointer_shortcut_feedback(button: u32, state: ModifierType) -> Option { + keybindings::gdk::pointer_button(button)?; let (ctrl, shift, alt, logo) = gdk_pointer_modifiers(state); Some(GtkToolbarFeedback::PointerShortcut { button, @@ -168,7 +172,7 @@ pub(super) fn install_quick_color_recolor( feedback: &FeedbackSender, ) { let gesture = gtk4::GestureClick::new(); - gesture.set_button(gtk4::gdk::BUTTON_SECONDARY); + gesture.set_button(gdk::BUTTON_SECONDARY); let feedback = feedback.clone(); gesture.connect_pressed(move |gesture, _, _, _| { gesture.set_state(gtk4::EventSequenceState::Claimed); @@ -184,7 +188,7 @@ pub(super) fn secondary_click_gesture(widget: >k4::Widget) -> Option().ok()) - .find(|gesture| gesture.button() == gtk4::gdk::BUTTON_SECONDARY) + .find(|gesture| gesture.button() == gdk::BUTTON_SECONDARY) } /// Drive a widget's secondary-click gesture the way `emit_clicked` drives a @@ -218,7 +222,7 @@ pub(super) fn install_click_modifier_capture( let release_feedback = feedback.clone(); click.connect_released(move |_, _, _, _| { let release_feedback = release_feedback.clone(); - gtk4::glib::idle_add_local_once(move || release_feedback.finish_pointer_click()); + glib::idle_add_local_once(move || release_feedback.finish_pointer_click()); }); widget.as_ref().add_controller(click); } @@ -239,12 +243,12 @@ pub(super) fn sized_button(width: f64, height: f64) -> gtk4::Button { fn release_window_keyboard_focus(widget: &impl IsA) { if let Some(window) = widget.root().and_downcast::() { window.set_keyboard_mode(KeyboardMode::None); - gtk4::prelude::GtkWindowExt::set_focus(&window, None::<>k4::Widget>); + GtkWindowExt::set_focus(&window, None::<>k4::Widget>); // Interactivity is double-buffered. Re-arm OnDemand after the // focus-dropping commit so a later click can focus an entry without // making this surface retain the current keyboard focus. let weak = window.downgrade(); - gtk4::glib::idle_add_local_once(move || { + glib::idle_add_local_once(move || { if let Some(window) = weak.upgrade() { window.set_keyboard_mode(KeyboardMode::OnDemand); } @@ -273,7 +277,7 @@ fn focus_change_releases_keyboard(focus: >k4::Widget) -> bool { /// may leave a previously focused entry as the logical focus widget. pub(super) fn install_shortcut_focus_policy(window: >k4::Window, feedback: &FeedbackSender) { window.connect_focus_widget_notify(|window| { - let Some(focus) = gtk4::prelude::GtkWindowExt::focus(window) else { + let Some(focus) = GtkWindowExt::focus(window) else { return; }; if focus_change_releases_keyboard(&focus) { @@ -296,7 +300,7 @@ pub(super) fn install_shortcut_focus_policy(window: >k4::Window, feedback: &Fe let release_feedback = feedback.clone(); click.connect_released(move |_, _, _, _| { let release_feedback = release_feedback.clone(); - gtk4::glib::idle_add_local_once(move || release_feedback.finish_pointer_click()); + glib::idle_add_local_once(move || release_feedback.finish_pointer_click()); }); let weak = window.downgrade(); click.connect_released(move |_, _, x, y| { @@ -479,7 +483,7 @@ impl SwatchButton { let swatch_path = |ctx: &cairo::Context| { rounded_rect_path(ctx, 4.0, 4.0, size - 8.0, size - 8.0, 4.0) }; - crate::ui::checkerboard_behind(ctx, a, swatch_path); + ui::checkerboard_behind(ctx, a, swatch_path); ctx.set_source_rgba(r, g, b, a); swatch_path(ctx); let _ = ctx.fill(); diff --git a/src/toolbar_gtk/widgets/key_relay.rs b/src/toolbar_gtk/widgets/key_relay.rs index 6e85dc094..a2b8534a8 100644 --- a/src/toolbar_gtk/widgets/key_relay.rs +++ b/src/toolbar_gtk/widgets/key_relay.rs @@ -7,6 +7,8 @@ //! presses to the backend, which routes them exactly like its own: Escape //! closes the open menu, and any other shortcut closes it and then runs. +use gtk4::gdk::{Key, KeyEvent, ModifierType}; +use gtk4::glib::Propagation; use gtk4::glib::translate::IntoGlib; use gtk4::prelude::*; @@ -36,7 +38,7 @@ pub(super) enum FocusedKeys { /// activatable control, so Ctrl+PgUp switches boards even while a slider has /// focus; the hex entry keeps its editing chords. pub(super) fn key_stays_local( - keyval: gtk4::gdk::Key, + keyval: Key, is_modifier: bool, chord: bool, focus: FocusedKeys, @@ -64,7 +66,7 @@ pub(super) fn key_stays_local( } /// Whether Ctrl, Alt, or Super is held, which makes a press a shortcut. -fn is_chord(state: gtk4::gdk::ModifierType) -> bool { +fn is_chord(state: ModifierType) -> bool { let (ctrl, _shift, alt, logo) = gdk_pointer_modifiers(state); ctrl || alt || logo @@ -73,14 +75,11 @@ fn is_chord(state: gtk4::gdk::ModifierType) -> bool { /// Whether this press is an Escape the focused widget spends on its own /// dismissal. The overlay still has to hear about it, so a second Escape /// right behind it does not exit. -pub(super) fn escape_dismisses_locally(keyval: gtk4::gdk::Key, focus: FocusedKeys) -> bool { - focus == FocusedKeys::Slider && keyval == gtk4::gdk::Key::Escape +pub(super) fn escape_dismisses_locally(keyval: Key, focus: FocusedKeys) -> bool { + focus == FocusedKeys::Slider && keyval == Key::Escape } -pub(super) fn forwarded_key_feedback( - keyval: gtk4::gdk::Key, - state: gtk4::gdk::ModifierType, -) -> GtkToolbarFeedback { +pub(super) fn forwarded_key_feedback(keyval: Key, state: ModifierType) -> GtkToolbarFeedback { let (ctrl, shift, alt, logo) = gdk_pointer_modifiers(state); GtkToolbarFeedback::Key { keyval: keyval.into_glib(), @@ -120,7 +119,7 @@ pub(in crate::toolbar_gtk) fn install_key_relay( key.connect_key_pressed(move |controller, keyval, _, state| { let is_modifier = controller .current_event() - .and_then(|event| event.downcast::().ok()) + .and_then(|event| event.downcast::().ok()) .is_some_and(|event| event.is_modifier()); let focus = controller .widget() @@ -132,11 +131,11 @@ pub(in crate::toolbar_gtk) fn install_key_relay( if escape_dismisses_locally(keyval, focus) { let _ = feedback.send(GtkToolbarFeedback::EscapeDismissed); } - return gtk4::glib::Propagation::Proceed; + return Propagation::Proceed; } let _ = feedback.send(forwarded_key_feedback(keyval, state)); - gtk4::glib::Propagation::Stop + Propagation::Stop }); widget.as_ref().add_controller(key); } diff --git a/src/toolbar_gtk/widgets/slider.rs b/src/toolbar_gtk/widgets/slider.rs index 0226fd18b..e152193ec 100644 --- a/src/toolbar_gtk/widgets/slider.rs +++ b/src/toolbar_gtk/widgets/slider.rs @@ -1,9 +1,28 @@ //! Slider interaction retains the Cairo geometry and the active-drag update barrier. +#[cfg(test)] +use crate::toolbar_gtk::GtkToolbarFeedback; +use crate::toolbar_gtk::css; +use crate::toolbar_icons; + +use gtk4::accessible::Property; +use gtk4::gdk::Key; +#[cfg(test)] +use gtk4::gdk::ModifierType; +use gtk4::glib::Propagation; +#[cfg(test)] +use gtk4::prelude::GtkWindowExt; + +use std::cell::RefCell; +use std::f64::consts; +#[cfg(test)] +use std::sync::mpsc; + use super::rounded_rect_path; use crate::ui::theme::{ACCENT_RGB, Rgba, rgba, set_color}; use crate::ui::toolbar::model::{OpacityPaint, ToolbarSliderSpec}; use gtk4::prelude::*; -use std::{cell::Cell, rc::Rc}; +use std::cell::Cell; +use std::rc::Rc; const COLOR_TRACK_FILL: Rgba = rgba(ACCENT_RGB, 0.55); @@ -26,7 +45,7 @@ struct SliderState { /// solid in the stroke color instead of filling with the accent. opacity_paint: Cell>, /// Told when Escape made this slider give up the toolbar's keyboard focus. - keyboard_released: std::cell::RefCell>>, + keyboard_released: RefCell>>, } impl SliderRow { @@ -49,7 +68,7 @@ impl SliderRow { value: Cell::new(initial), dragging: Cell::new(false), opacity_paint: Cell::new(None), - keyboard_released: std::cell::RefCell::new(None), + keyboard_released: RefCell::new(None), }); let area = gtk4::DrawingArea::builder() @@ -58,10 +77,10 @@ impl SliderRow { .focus_on_click(false) .build(); area.update_property(&[ - gtk4::accessible::Property::Label(name), - gtk4::accessible::Property::ValueMin(spec.min), - gtk4::accessible::Property::ValueMax(spec.max), - gtk4::accessible::Property::Orientation(gtk4::Orientation::Horizontal), + Property::Label(name), + Property::ValueMin(spec.min), + Property::ValueMax(spec.max), + Property::Orientation(gtk4::Orientation::Horizontal), ]); update_accessible_value(&area, initial, format); area.set_content_height((16.0 * scale).round() as i32); @@ -76,13 +95,13 @@ impl SliderRow { let radius = track_h / 2.0; let t = draw_state.spec.t_from_value(draw_state.value.get()); if widget.has_focus() { - set_color(ctx, crate::toolbar_gtk::css::ACCENT); + set_color(ctx, css::ACCENT); ctx.set_line_width(1.5); rounded_rect_path(ctx, 1.0, 1.0, w - 2.0, h - 2.0, 3.0); let _ = ctx.stroke(); } if let Some(paint) = draw_state.opacity_paint.get() { - crate::toolbar_icons::draw_opacity_track( + toolbar_icons::draw_opacity_track( ctx, (0.0, 0.0, w, h), paint.rgb, @@ -91,13 +110,13 @@ impl SliderRow { } else { // Track rounded_rect_path(ctx, 0.0, track_y, w, track_h, radius); - set_color(ctx, crate::toolbar_gtk::css::TRACK_BACKGROUND); + set_color(ctx, css::TRACK_BACKGROUND); let _ = ctx.fill(); // Filled portion (accent at reduced alpha) rounded_rect_path(ctx, 0.0, track_y, (w * t).max(track_h), track_h, radius); set_color(ctx, COLOR_TRACK_FILL); let _ = ctx.fill(); - crate::toolbar_icons::draw_slider_ticks( + toolbar_icons::draw_slider_ticks( ctx, (0.0, 0.0, w, h), draw_state.spec.tick_positions(), @@ -106,8 +125,8 @@ impl SliderRow { // Knob let knob_r = (h / 2.0).min(7.0); let knob_x = knob_r + t * (w - knob_r * 2.0); - ctx.arc(knob_x, h / 2.0, knob_r, 0.0, std::f64::consts::PI * 2.0); - set_color(ctx, crate::toolbar_gtk::css::TRACK_KNOB); + ctx.arc(knob_x, h / 2.0, knob_r, 0.0, consts::PI * 2.0); + set_color(ctx, css::TRACK_KNOB); let _ = ctx.fill(); }); @@ -135,7 +154,7 @@ impl SliderRow { }); let update_state = state.clone(); let update_area = area.clone(); - let update_start = start_value.clone(); + let update_start = start_value; let update_label = value_label.clone(); let change = Rc::new(on_change); let update_change = change.clone(); @@ -166,7 +185,7 @@ impl SliderRow { let key_label = value_label.clone(); let key_change = change.clone(); key.connect_key_pressed(move |_, key, _, _| { - if key == gtk4::gdk::Key::Escape { + if key == Key::Escape { if !key_state.dragging.get() { if let Some(popover) = key_area .ancestor(gtk4::Popover::static_type()) @@ -182,10 +201,10 @@ impl SliderRow { } } } - return gtk4::glib::Propagation::Stop; + return Propagation::Stop; } let Some(value) = keyboard_value(key_state.spec, key_state.value.get(), key) else { - return gtk4::glib::Propagation::Proceed; + return Propagation::Proceed; }; if !key_state.dragging.get() { key_state.value.set(value); @@ -194,7 +213,7 @@ impl SliderRow { key_area.queue_draw(); key_change(value); } - gtk4::glib::Propagation::Stop + Propagation::Stop }); area.add_controller(key); area.connect_has_focus_notify(|area| area.queue_draw()); @@ -207,13 +226,13 @@ impl SliderRow { let scroll_state = state.clone(); let scroll_area = area.clone(); let scroll_label = value_label.clone(); - let scroll_change = change.clone(); + let scroll_change = change; scroll.connect_scroll(move |_, _, dy| { if scroll_state.dragging.get() { - return gtk4::glib::Propagation::Stop; + return Propagation::Stop; } let Some(value) = wheel_value(scroll_state.spec, scroll_state.value.get(), dy) else { - return gtk4::glib::Propagation::Stop; + return Propagation::Stop; }; scroll_state.value.set(value); @@ -221,7 +240,7 @@ impl SliderRow { update_accessible_value(&scroll_area, value, format); scroll_area.queue_draw(); scroll_change(value); - gtk4::glib::Propagation::Stop + Propagation::Stop }); area.add_controller(scroll); @@ -281,8 +300,8 @@ impl SliderRow { fn update_accessible_value(area: >k4::DrawingArea, value: f64, format: fn(f64) -> String) { area.update_property(&[ - gtk4::accessible::Property::ValueNow(value), - gtk4::accessible::Property::ValueText(&format(value)), + Property::ValueNow(value), + Property::ValueText(&format(value)), ]); } @@ -300,7 +319,7 @@ enum SliderStep { /// The keys a focused slider owns, keypad variants included. The key relay /// reads this same map, so the keys it leaves to a slider are exactly the /// keys the slider acts on. -fn slider_step_for_key(key: gtk4::gdk::Key) -> Option { +fn slider_step_for_key(key: Key) -> Option { use gtk4::gdk::Key; Some(match key { @@ -314,7 +333,7 @@ fn slider_step_for_key(key: gtk4::gdk::Key) -> Option { }) } -pub(super) fn is_slider_navigation_key(key: gtk4::gdk::Key) -> bool { +pub(super) fn is_slider_navigation_key(key: Key) -> bool { slider_step_for_key(key).is_some() } @@ -329,7 +348,7 @@ fn wheel_value(spec: ToolbarSliderSpec, value: f64, dy: f64) -> Option { Some(spec.step_value(value, notches)) } -fn keyboard_value(spec: ToolbarSliderSpec, value: f64, key: gtk4::gdk::Key) -> Option { +fn keyboard_value(spec: ToolbarSliderSpec, value: f64, key: Key) -> Option { Some(match slider_step_for_key(key)? { SliderStep::Down => spec.step_value(value, -1.0), SliderStep::Up => spec.step_value(value, 1.0), @@ -343,7 +362,7 @@ fn keyboard_value(spec: ToolbarSliderSpec, value: f64, key: gtk4::gdk::Key) -> O /// Called by the isolated GTK widget test after initialization. #[cfg(test)] pub(super) fn assert_widget_contract() { - let changes = Rc::new(std::cell::RefCell::new(Vec::new())); + let changes = Rc::new(RefCell::new(Vec::new())); let observed = changes.clone(); let slider = SliderRow::new( 1.0, @@ -372,14 +391,7 @@ pub(super) fn assert_widget_contract() { .filter_map(|i| controllers.item(i)) .find_map(|item| item.downcast::().ok()) .unwrap(); - key.emit_by_name::( - "key-pressed", - &[ - >k4::gdk::Key::Right, - &0u32, - >k4::gdk::ModifierType::empty(), - ], - ); + key.emit_by_name::("key-pressed", &[&Key::Right, &0u32, &ModifierType::empty()]); // An off-grid 4.5 steps up to the adjacent whole value, 5, not 6. assert_eq!(slider.state.value.get(), 5.0); assert_eq!(changes.borrow().as_slice(), &[5.0]); @@ -400,15 +412,11 @@ pub(super) fn assert_widget_contract() { fn assert_escape_contract(slider: &SliderRow, key: >k4::EventControllerKey) { let window = gtk4::Window::new(); window.set_child(Some(&slider.root)); - gtk4::prelude::GtkWindowExt::set_focus(&window, Some(&slider.area)); + GtkWindowExt::set_focus(&window, Some(&slider.area)); let escape = || { key.emit_by_name::( "key-pressed", - &[ - >k4::gdk::Key::Escape, - &0u32, - >k4::gdk::ModifierType::empty(), - ], + &[&Key::Escape, &0u32, &ModifierType::empty()], ) }; // The release callback reports only a release that happened: a drag @@ -418,13 +426,13 @@ fn assert_escape_contract(slider: &SliderRow, key: >k4::EventControllerKey) { slider.on_keyboard_released(move || counted.set(counted.get() + 1)); slider.state.dragging.set(true); assert!(escape()); - assert!(gtk4::prelude::GtkWindowExt::focus(&window).is_some()); + assert!(GtkWindowExt::focus(&window).is_some()); assert_eq!(releases.get(), 0, "a drag keeps the keyboard"); slider.set_value(10.0); assert_eq!(slider.state.value.get(), 5.0); slider.state.dragging.set(false); assert!(escape()); - assert!(gtk4::prelude::GtkWindowExt::focus(&window).is_none()); + assert!(GtkWindowExt::focus(&window).is_none()); assert_eq!(releases.get(), 1, "the release is reported once"); assert_eq!(slider.state.value.get(), 5.0); window.set_child(None::<>k4::Widget>); @@ -449,7 +457,7 @@ fn assert_escape_contract(slider: &SliderRow, key: >k4::EventControllerKey) { /// press to the slider and only tells the overlay, which arms its guard. #[cfg(test)] pub(super) fn assert_focused_slider_escape_stays_local() { - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let slider = SliderRow::new( 1.0, "Thickness", @@ -462,21 +470,17 @@ pub(super) fn assert_focused_slider_escape_stays_local() { window.set_child(Some(&slider.root)); super::install_key_relay(&window, &super::FeedbackSender::new(tx)); let relay = super::key_relay_controller(&window).expect("the window relays keys"); - gtk4::prelude::GtkWindowExt::set_focus(&window, Some(&slider.area)); + GtkWindowExt::set_focus(&window, Some(&slider.area)); let handled = relay.emit_by_name::( "key-pressed", - &[ - >k4::gdk::Key::Escape, - &0u32, - >k4::gdk::ModifierType::empty(), - ], + &[&Key::Escape, &0u32, &ModifierType::empty()], ); assert!(!handled, "Escape goes on to the focused slider"); assert_eq!( rx.try_recv(), - Ok(crate::toolbar_gtk::GtkToolbarFeedback::EscapeDismissed), + Ok(GtkToolbarFeedback::EscapeDismissed), "the relay arms the overlay's Escape guard; the slider reports any release" ); assert!( diff --git a/src/toolbar_gtk/widgets/tests.rs b/src/toolbar_gtk/widgets/tests.rs index 05a3759d6..1bed766f9 100644 --- a/src/toolbar_gtk/widgets/tests.rs +++ b/src/toolbar_gtk/widgets/tests.rs @@ -1,5 +1,11 @@ //! Shared-widget unit tests. +use std::env; +use std::process::Command; +use std::sync::mpsc; + +use crate::config::PointerButton; + use super::*; /// Focus moving to a control inside a popover must not drop the layer @@ -14,8 +20,8 @@ fn popover_internal_focus_keeps_the_keyboard_grab() { const TEST_NAME: &str = "toolbar_gtk::widgets::tests::popover_internal_focus_keeps_the_keyboard_grab"; - if std::env::var_os(CHILD_ENV).is_none() { - let status = std::process::Command::new(std::env::current_exe().expect("test binary")) + if env::var_os(CHILD_ENV).is_none() { + let status = Command::new(env::current_exe().expect("test binary")) .arg(TEST_NAME) .arg("--exact") .arg("--test-threads=1") @@ -30,7 +36,7 @@ fn popover_internal_focus_keeps_the_keyboard_grab() { if let Err(error) = gtk4::init() { assert!( - std::env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none(), + env::var_os("WAYSCRIBER_REQUIRE_GTK_TESTS").is_none(), "Required GTK widget coverage could not initialize: {error}" ); eprintln!("skipping GTK focus-policy test: {error}"); @@ -78,12 +84,10 @@ fn popover_internal_focus_keeps_the_keyboard_grab() { #[test] fn gtk_feedback_carries_rebind_state_once() { - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let sender = FeedbackSender::new(tx); sender.set_rebind_state(ToolbarRebindModifier::CtrlShift, true); - sender.capture_click_modifiers( - gtk4::gdk::ModifierType::CONTROL_MASK | gtk4::gdk::ModifierType::SHIFT_MASK, - ); + sender.capture_click_modifiers(ModifierType::CONTROL_MASK | ModifierType::SHIFT_MASK); send_event(&sender, ToolbarEvent::Undo); send_event(&sender, ToolbarEvent::Redo); @@ -105,10 +109,10 @@ fn gtk_feedback_carries_rebind_state_once() { #[test] fn backend_modifier_latch_survives_focus_reset_during_click() { - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let sender = FeedbackSender::new(tx); sender.set_rebind_state(ToolbarRebindModifier::CtrlShift, true); - sender.capture_click_modifiers(gtk4::gdk::ModifierType::empty()); + sender.capture_click_modifiers(ModifierType::empty()); sender.set_rebind_state(ToolbarRebindModifier::CtrlShift, false); send_event(&sender, ToolbarEvent::Undo); @@ -124,7 +128,7 @@ fn backend_modifier_latch_survives_focus_reset_during_click() { #[test] fn auxiliary_mouse_buttons_forward_as_pointer_shortcuts() { - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = mpsc::channel(); let sender = FeedbackSender::new(tx); let _ = sender.send(GtkToolbarFeedback::PointerShortcut { button: 8, @@ -144,16 +148,16 @@ fn auxiliary_mouse_buttons_forward_as_pointer_shortcuts() { } ); assert_eq!( - crate::config::keybindings::gdk::pointer_button(8), - Some(crate::config::PointerButton::Back) + keybindings::gdk::pointer_button(8), + Some(PointerButton::Back) ); - assert_eq!(crate::config::keybindings::gdk::pointer_button(1), None); + assert_eq!(keybindings::gdk::pointer_button(1), None); } #[test] fn gtk_pointer_shortcuts_carry_click_modifiers() { assert_eq!( - super::pointer_shortcut_feedback(8, gtk4::gdk::ModifierType::CONTROL_MASK), + super::pointer_shortcut_feedback(8, ModifierType::CONTROL_MASK), Some(GtkToolbarFeedback::PointerShortcut { button: 8, ctrl: true, @@ -163,10 +167,7 @@ fn gtk_pointer_shortcuts_carry_click_modifiers() { }) ); assert_eq!( - super::pointer_shortcut_feedback( - 9, - gtk4::gdk::ModifierType::SHIFT_MASK | gtk4::gdk::ModifierType::SUPER_MASK, - ), + super::pointer_shortcut_feedback(9, ModifierType::SHIFT_MASK | ModifierType::SUPER_MASK,), Some(GtkToolbarFeedback::PointerShortcut { button: 9, ctrl: false, diff --git a/src/toolbar_icons/actions.rs b/src/toolbar_icons/actions.rs index 83348f593..405442e0e 100644 --- a/src/toolbar_icons/actions.rs +++ b/src/toolbar_icons/actions.rs @@ -1,3 +1,5 @@ +use std::f64::consts; + use cairo::Context; use std::f64::consts::{FRAC_1_SQRT_2, PI}; @@ -478,7 +480,7 @@ pub fn draw_icon_pencil(ctx: &Context, x: f64, y: f64, size: f64) { // Additive fills only (the gaps merely reveal the row background), so it is // safe on any surface and reads clearly at the ~16px palette size, where the // old thin-outline parallelogram with no point just looked like a bar. - let inv_sqrt2 = std::f64::consts::FRAC_1_SQRT_2; + let inv_sqrt2 = consts::FRAC_1_SQRT_2; let ax = x + s * 0.76; // axis origin = center of the eraser end let ay = y + s * 0.24; let (ux, uy) = (-inv_sqrt2, inv_sqrt2); // down the pencil (toward the nib) diff --git a/src/toolbar_icons/arrow_style_preview.rs b/src/toolbar_icons/arrow_style_preview.rs index 4434f9698..2446b58bd 100644 --- a/src/toolbar_icons/arrow_style_preview.rs +++ b/src/toolbar_icons/arrow_style_preview.rs @@ -7,6 +7,7 @@ use crate::draw::ArrowStyle; use crate::ui::theme::{Rgba, set_color}; +use crate::util; /// Sample space, authored large so the outline's whole-unit endpoints stay /// far below a preview pixel once scaled down. @@ -57,7 +58,7 @@ fn sample_outline(style: ArrowStyle) -> Option> { } else { (SAMPLE_H / 2.0) as i32 }; - crate::util::calculate_arrow_outline_styled( + util::calculate_arrow_outline_styled( SAMPLE_W as i32 - SAMPLE_INSET, baseline, SAMPLE_INSET, @@ -66,7 +67,7 @@ fn sample_outline(style: ArrowStyle) -> Option> { SAMPLE_HEAD_LENGTH, SAMPLE_HEAD_ANGLE, style, - style.effective_bend(crate::util::DEFAULT_ARROW_BEND), + style.effective_bend(util::DEFAULT_ARROW_BEND), ) } diff --git a/src/toolbar_icons/meter_dot.rs b/src/toolbar_icons/meter_dot.rs index 190cf8598..52bc96fb3 100644 --- a/src/toolbar_icons/meter_dot.rs +++ b/src/toolbar_icons/meter_dot.rs @@ -1,5 +1,7 @@ //! Shared zero-level meter dot, used by Cairo and GTK toolbars. +use std::f64::consts; + use crate::ui::theme::set_color; use crate::ui::theme::toolbar::{COLOR_ACCENT, COLOR_METER_TRACK_HOVER}; @@ -16,13 +18,13 @@ pub(crate) fn draw_meter_dot( let _ = ctx.save(); ctx.new_path(); set_color(ctx, faded(COLOR_ACCENT)); - ctx.arc(x + 6.0, y + h / 2.0, 5.0, 0.0, std::f64::consts::TAU); + ctx.arc(x + 6.0, y + h / 2.0, 5.0, 0.0, consts::TAU); let _ = ctx.fill(); if hover && !active && enabled { set_color(ctx, COLOR_METER_TRACK_HOVER); ctx.set_line_width(1.5); - ctx.arc(x + 6.0, y + h / 2.0, 7.5, 0.0, std::f64::consts::TAU); + ctx.arc(x + 6.0, y + h / 2.0, 7.5, 0.0, consts::TAU); let _ = ctx.stroke(); } diff --git a/src/toolbar_icons/mod.rs b/src/toolbar_icons/mod.rs index e5e3496cf..b94b9c951 100644 --- a/src/toolbar_icons/mod.rs +++ b/src/toolbar_icons/mod.rs @@ -29,12 +29,14 @@ pub(crate) use opacity_preview::{draw_opacity_swatch, draw_opacity_track}; pub(crate) use slider_ticks::draw_slider_ticks; pub(crate) use smoothing_preview::draw_smoothing_preview; +use crate::config::action_meta::ActionIcon; +use crate::ui::toolbar::model::TopToolbarIcon; + pub(crate) type ToolbarIconPainter = fn(&cairo::Context, f64, f64, f64); /// Resolve action metadata to the shared Cairo glyph used by UI surfaces. -pub fn action_icon_painter( - icon: crate::config::action_meta::ActionIcon, -) -> fn(&cairo::Context, f64, f64, f64) { +#[must_use] +pub fn action_icon_painter(icon: ActionIcon) -> fn(&cairo::Context, f64, f64, f64) { use crate::config::action_meta::ActionIcon; match icon { @@ -127,9 +129,7 @@ pub(crate) fn draw_micro_chip( glyph(ctx, cx - icon_size / 2.0, cy - icon_size / 2.0, icon_size); } -pub(crate) fn top_toolbar_icon_painter( - icon: crate::ui::toolbar::model::TopToolbarIcon, -) -> ToolbarIconPainter { +pub(crate) fn top_toolbar_icon_painter(icon: TopToolbarIcon) -> ToolbarIconPainter { use crate::ui::toolbar::model::{SemanticToolIcon as T, TopToolbarIcon as I}; match icon { @@ -180,6 +180,11 @@ pub(crate) fn top_toolbar_icon_painter( #[cfg(test)] mod painter_tests { + use std::ptr; + + use crate::config; + use crate::config::Action; + use super::*; use cairo::{Context, Format, ImageSurface}; @@ -267,19 +272,16 @@ mod painter_tests { use crate::toolbar_icons::top_toolbar_icon_painter; use crate::ui::toolbar::model::{TopToolbarIcon, semantic_icon_for_tool}; - for meta in crate::config::action_meta_iter().filter(|meta| meta.icon.is_some()) { + let actions_with_icons = config::action_meta_iter().filter(|meta| meta.icon.is_some()); + for meta in actions_with_icons { let action = meta.action; - let icon = crate::config::action_meta(action) + let icon = config::action_meta(action) .and_then(|meta| meta.icon) .map(action_icon_painter) .unwrap_or_else(|| panic!("missing icon for {:?}", action)); let expected = match action { - crate::config::Action::EnterTextMode => { - top_toolbar_icon_painter(TopToolbarIcon::Text) - } - crate::config::Action::EnterStickyNoteMode => { - top_toolbar_icon_painter(TopToolbarIcon::StickyNote) - } + Action::EnterTextMode => top_toolbar_icon_painter(TopToolbarIcon::Text), + Action::EnterStickyNoteMode => top_toolbar_icon_painter(TopToolbarIcon::StickyNote), _ => { let tool = Tool::from_select_action(action) .unwrap_or_else(|| panic!("{:?} should select a tool", action)); @@ -287,7 +289,7 @@ mod painter_tests { } }; assert!( - std::ptr::fn_addr_eq(icon, expected), + ptr::fn_addr_eq(icon, expected), "icon painter for {:?} drifted from the shared semantic painter", action ); diff --git a/src/toolbar_icons/opacity_preview.rs b/src/toolbar_icons/opacity_preview.rs index 04a527e0a..5b2823379 100644 --- a/src/toolbar_icons/opacity_preview.rs +++ b/src/toolbar_icons/opacity_preview.rs @@ -94,6 +94,8 @@ pub(crate) fn draw_opacity_track( #[cfg(test)] mod tests { + use crate::ui::toolbar::ToolbarSnapshot; + use super::*; use crate::input::state::test_support::make_test_input_state; use crate::ui::toolbar::ToolbarBindingHints; @@ -102,10 +104,8 @@ mod tests { #[test] fn the_opacity_track_matches_stroke_alpha_through_the_minimum_plateau() { let state = make_test_input_state(); - let mut snapshot = crate::ui::toolbar::ToolbarSnapshot::from_input_with_bindings( - &state, - ToolbarBindingHints::default(), - ); + let mut snapshot = + ToolbarSnapshot::from_input_with_bindings(&state, ToolbarBindingHints::default()); snapshot.color.r = 0.0; snapshot.color.g = 0.0; snapshot.color.b = 0.0; diff --git a/src/toolbar_icons/smoothing_preview.rs b/src/toolbar_icons/smoothing_preview.rs index 97ac2a91a..437010c1f 100644 --- a/src/toolbar_icons/smoothing_preview.rs +++ b/src/toolbar_icons/smoothing_preview.rs @@ -10,6 +10,7 @@ use std::f64::consts::TAU; +use crate::draw::shape; use crate::ui::theme::set_color; use crate::ui::theme::toolbar::{ COLOR_ACCENT, COLOR_SMOOTHING_PREVIEW_BG, COLOR_SMOOTHING_PREVIEW_RAW, @@ -64,7 +65,7 @@ pub(crate) fn draw_smoothing_preview(ctx: &cairo::Context, rect: (f64, f64, f64, y + (h - SAMPLE_H * scale) / 2.0, ); let raw = smoothing_preview_sample(); - let smoothed = crate::draw::shape::smooth_path(&raw, level); + let smoothed = shape::smooth_path(&raw, level); let _ = ctx.save(); set_color(ctx, COLOR_SMOOTHING_PREVIEW_BG); @@ -106,6 +107,8 @@ fn rounded_rect(ctx: &cairo::Context, rect: (f64, f64, f64, f64), radius: f64) { #[cfg(test)] mod tests { + use crate::draw; + use super::*; /// Pixels where the as-drawn stroke shows on its own: bright and neutral, @@ -150,7 +153,7 @@ mod tests { fn mean_deviation(level: u8) -> f64 { let raw = smoothing_preview_sample(); - let smoothed = crate::draw::shape::smooth_path(&raw, level); + let smoothed = shape::smooth_path(&raw, level); let total: f64 = raw .iter() .zip(&smoothed) @@ -164,10 +167,10 @@ mod tests { let raw = smoothing_preview_sample(); assert_eq!(raw.len(), SAMPLE_POINTS); - assert_eq!(crate::draw::shape::smooth_path(&raw, 0), raw); + assert_eq!(shape::smooth_path(&raw, 0), raw); assert_eq!(mean_deviation(0), 0.0); // Each level takes visibly more of the tremor out than the last. - for level in 1..=crate::draw::MAX_PEN_SMOOTHING { + for level in 1..=draw::MAX_PEN_SMOOTHING { assert!( mean_deviation(level) > mean_deviation(level - 1), "level {level} smooths more than level {}", @@ -175,7 +178,7 @@ mod tests { ); } // Past two preview pixels on average at the top level. - assert!(mean_deviation(crate::draw::MAX_PEN_SMOOTHING) > 2.0 * SAMPLE_SCALE); + assert!(mean_deviation(draw::MAX_PEN_SMOOTHING) > 2.0 * SAMPLE_SCALE); } #[test] @@ -191,7 +194,7 @@ mod tests { #[test] fn the_drawn_stroke_shows_only_where_smoothing_moved_it() { assert_eq!(raw_only_pixels(0), 0, "Off draws one stroke"); - let separated = raw_only_pixels(crate::draw::MAX_PEN_SMOOTHING); + let separated = raw_only_pixels(draw::MAX_PEN_SMOOTHING); assert!( separated > 40, "only {separated} raw pixels show at the maximum" diff --git a/src/toolbar_icons/svg.rs b/src/toolbar_icons/svg.rs index 8712f1d45..dc342ebce 100644 --- a/src/toolbar_icons/svg.rs +++ b/src/toolbar_icons/svg.rs @@ -5,6 +5,8 @@ //! Every public renderer accepts the same `(ctx, x, y, size)` contract used by //! Wayscriber's existing Cairo toolbar icon entry points. +use std::f64::consts; + use cairo::Context; use std::f64::consts::PI; @@ -485,10 +487,10 @@ fn draw_spotlight(ctx: &Context) { rounded_rect(ctx, 3.0, 4.0, 18.0, 16.0, 3.0); stroke(ctx); ctx.new_sub_path(); - ctx.arc(12.0, 12.0, 4.6, 0.0, std::f64::consts::TAU); + ctx.arc(12.0, 12.0, 4.6, 0.0, consts::TAU); stroke(ctx); ctx.new_sub_path(); - ctx.arc(12.0, 12.0, 1.8, 0.0, std::f64::consts::TAU); + ctx.arc(12.0, 12.0, 1.8, 0.0, consts::TAU); fill(ctx); } diff --git a/src/tray_action.rs b/src/tray_action.rs index ec561d7ef..cf4246727 100644 --- a/src/tray_action.rs +++ b/src/tray_action.rs @@ -1,5 +1,8 @@ +use std::process; + use crate::daemon::protocol_v2::DaemonControlProtocolMode; use crate::durable_io::{AtomicWriteOptions, OverwriteMode, PermissionPolicy, SymlinkPolicy}; +use crate::{durable_io, paths}; use anyhow::{Context, Result}; use log::warn; use serde::{Deserialize, Serialize}; @@ -72,18 +75,18 @@ fn queued_action_path(dir: &Path) -> PathBuf { dir.join(format!( "{:032x}-{:08x}-{:08x}.action", action_queue_stamp(), - std::process::id(), + process::id(), sequence )) } pub(crate) fn queue_action(action: TrayAction) -> Result { - let dir = crate::paths::tray_action_dir(); + let dir = paths::tray_action_dir(); fs::create_dir_all(&dir) .with_context(|| format!("failed to create runtime directory {}", dir.display()))?; let path = queued_action_path(&dir); - crate::durable_io::write_text_atomic( + durable_io::write_text_atomic( &path, action.as_str(), AtomicWriteOptions { @@ -126,7 +129,7 @@ pub(crate) fn take_pending_legacy_actions(mode: DaemonControlProtocolMode) -> Ve } pub(crate) fn take_pending_actions() -> Vec { - let dir = crate::paths::tray_action_dir(); + let dir = paths::tray_action_dir(); let mut paths = match fs::read_dir(&dir) { Ok(entries) => entries .filter_map(|entry| entry.ok().map(|entry| entry.path())) @@ -168,7 +171,7 @@ pub(crate) fn take_pending_actions() -> Vec { } } - let legacy_path = crate::paths::tray_action_file(); + let legacy_path = paths::tray_action_file(); match fs::read_to_string(&legacy_path) { Ok(content) => { if let Err(err) = fs::remove_file(&legacy_path) { @@ -195,6 +198,8 @@ pub(crate) fn take_pending_actions() -> Vec { #[cfg(test)] mod tests { + use crate::{test_env, test_temp}; + use super::{TrayAction, queue_action, take_pending_actions, take_pending_legacy_actions}; use crate::daemon::protocol_v2::DaemonControlProtocolMode; use crate::env_vars::XDG_RUNTIME_DIR_ENV; @@ -202,8 +207,8 @@ mod tests { #[test] fn a_v2_overlay_leaves_the_legacy_action_queue_unread() { - let tmp = crate::test_temp::tempdir().unwrap(); - crate::test_env::with_env_var(XDG_RUNTIME_DIR_ENV, Some(tmp.path().as_os_str()), || { + let tmp = test_temp::tempdir().unwrap(); + test_env::with_env_var(XDG_RUNTIME_DIR_ENV, Some(tmp.path().as_os_str()), || { queue_action(TrayAction::CaptureFull).unwrap(); assert!(take_pending_legacy_actions(DaemonControlProtocolMode::PublishedV2).is_empty()); @@ -238,9 +243,10 @@ mod tests { #[test] fn queued_tray_actions_round_trip_in_order() { - let _guard = crate::test_env::lock(); - let tmp = crate::test_temp::tempdir().unwrap(); + let _guard = test_env::lock(); + let tmp = test_temp::tempdir().unwrap(); let prev = env::var_os(XDG_RUNTIME_DIR_ENV); + // SAFETY: serialized by the test environment mutex held in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, tmp.path()); } @@ -255,10 +261,12 @@ mod tests { assert!(take_pending_actions().is_empty()); if let Some(prev) = prev { + // SAFETY: this test still holds the environment mutex in `_guard`. unsafe { env::set_var(XDG_RUNTIME_DIR_ENV, prev); } } else { + // SAFETY: this test still holds the environment mutex in `_guard`. unsafe { env::remove_var(XDG_RUNTIME_DIR_ENV); } diff --git a/src/ui.rs b/src/ui.rs index a58c1db0f..1974d8b5f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -48,10 +48,10 @@ pub(crate) use context_menu::{ }; pub(crate) use eyedropper_loupe::{compute_eyedropper_loupe_layout, render_eyedropper_loupe}; pub use font_picker::render_font_picker; -#[allow(unused_imports)] -pub use help_overlay::HelpOverlayBindings; pub(crate) use help_overlay::{HelpContentCache, render_help_overlay_result_with_content}; #[allow(unused_imports)] +pub use help_overlay::{HelpContentOptions, HelpOverlayBindings}; +#[allow(unused_imports)] pub use help_overlay::{ HelpHitMap, HelpOverlayRegion, HelpRenderResult, render_help_overlay, render_help_overlay_result, @@ -96,7 +96,6 @@ pub(crate) use region_capture_picker::{ }; pub(crate) use region_resize_handles::RegionResizeHandles; pub(crate) use spotlight_control::render_spotlight_magnification_control; -pub(crate) use status::StatusHudTooltip; pub use status::{ StatusHudLayout, StatusHudSegmentKind, ZoomChipButtonKind, ZoomChipLayout, ZoomChipPress, compute_status_hud_layout, compute_zoom_chip_layout, render_editing_badge, render_frozen_badge, @@ -105,10 +104,11 @@ pub use status::{ zoom_chip_geometry, }; pub(crate) use status::{ - compute_status_hud_layout_with_resources, compute_zoom_chip_layout_with_engine, - render_editing_badge_with_engine, render_frozen_badge_with_engine, - render_page_badge_with_engine, render_pan_badge_with_engine, render_status_bar_with_resources, - render_zoom_badge_with_engine, render_zoom_chip_with_resources, + StatusHudTooltip, compute_status_hud_layout_with_resources, + compute_zoom_chip_layout_with_engine, render_editing_badge_with_engine, + render_frozen_badge_with_engine, render_page_badge_with_engine, render_pan_badge_with_engine, + render_status_bar_with_resources, render_zoom_badge_with_engine, + render_zoom_chip_with_resources, }; pub use toasts::{ blocked_feedback_rects, preset_toast_geometry, render_blocked_feedback, render_preset_toast, diff --git a/src/ui/anim.rs b/src/ui/anim.rs index 629e3d0ca..7a50f283b 100644 --- a/src/ui/anim.rs +++ b/src/ui/anim.rs @@ -9,6 +9,9 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; +#[cfg(test)] +use crate::test_env::{Lease, LeaseGuard}; + static MOTION_ENABLED: AtomicBool = AtomicBool::new(true); /// Enable/disable non-essential chrome animation process-wide. Wired from the @@ -23,6 +26,7 @@ pub fn motion_enabled() -> bool { } /// Cubic ease-out easing curve (pure math; not gated). +#[must_use] pub fn ease_out_cubic(t: f64) -> f64 { let t = t.clamp(0.0, 1.0); 1.0 - (1.0 - t).powi(3) @@ -42,6 +46,7 @@ pub struct Envelope { } impl Envelope { + #[must_use] pub fn new(value: f64) -> Self { Self { current: value, @@ -53,11 +58,13 @@ impl Envelope { } /// Current value without advancing time. + #[must_use] pub fn value(&self) -> f64 { self.current } /// Whether the envelope has reached its target (no transition running). + #[must_use] pub fn settled(&self) -> bool { self.started.is_none() } @@ -110,6 +117,7 @@ impl Envelope { /// Fully opaque for `hold_ratio` of the lifetime, then a linear fade to zero /// at `progress == 1.0`. Used by preset toasts. +#[must_use] pub fn hold_then_fade_out(progress: f64, hold_ratio: f64) -> f64 { if !motion_enabled() { return if progress >= 1.0 { 0.0 } else { 1.0 }; @@ -125,6 +133,7 @@ pub fn hold_then_fade_out(progress: f64, hold_ratio: f64) -> f64 { /// Opaque until the final `fade_secs` of `duration_secs`, then a linear fade /// to zero. Used by UI toasts (fixed short end fade keeps long-lived toasts /// crisp for nearly their full lifetime). +#[must_use] pub fn end_fade(elapsed_secs: f64, duration_secs: f64, fade_secs: f64) -> f64 { let fade_duration = duration_secs.min(fade_secs); if fade_duration <= 0.0 { @@ -148,6 +157,7 @@ pub fn end_fade(elapsed_secs: f64, duration_secs: f64, fade_secs: f64) -> f64 { /// Attack/hold/release flash: ramp to `peak` by `attack_end`, hold until /// `hold_end`, then release to zero at `progress == 1.0`. Used by the /// blocked-action edge flash. +#[must_use] pub fn flash(progress: f64, attack_end: f64, hold_end: f64, peak: f64) -> f64 { if !motion_enabled() { return if progress >= 1.0 { 0.0 } else { peak }; @@ -165,15 +175,15 @@ pub fn flash(progress: f64, attack_end: f64, hold_end: f64, peak: f64) -> f64 { /// flag; motion is restored to enabled when the guard drops. #[cfg(test)] pub(crate) struct MotionOverrideGuard { - _lock: std::sync::MutexGuard<'static, ()>, + _lease: LeaseGuard<'static>, } #[cfg(test)] pub(crate) fn override_motion_for_test(enabled: bool) -> MotionOverrideGuard { - static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); - let lock = LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + static MOTION_LEASE: Lease = Lease::new(); + let lease = MOTION_LEASE.acquire(); set_motion_enabled(enabled); - MotionOverrideGuard { _lock: lock } + MotionOverrideGuard { _lease: lease } } #[cfg(test)] diff --git a/src/ui/arrow_bend_handle.rs b/src/ui/arrow_bend_handle.rs index 4e89c9795..ad3213e74 100644 --- a/src/ui/arrow_bend_handle.rs +++ b/src/ui/arrow_bend_handle.rs @@ -1,5 +1,7 @@ //! The bend handle drawn on a selected curved arrow. +use std::f64::consts; + use crate::util::Rect; /// Border width of the handle, matching the selection box's handles so the two @@ -21,7 +23,7 @@ pub(crate) fn render_arrow_bend_handle(ctx: &cairo::Context, rect: Rect) { let _ = ctx.save(); ctx.new_path(); - ctx.arc(cx, cy, radius, 0.0, std::f64::consts::TAU); + ctx.arc(cx, cy, radius, 0.0, consts::TAU); ctx.set_source_rgba(1.0, 1.0, 1.0, 0.95); let _ = ctx.fill_preserve(); ctx.set_source_rgba(0.2, 0.45, 1.0, 0.95); diff --git a/src/ui/board_picker.rs b/src/ui/board_picker.rs index 673e97ce6..9acc00a78 100644 --- a/src/ui/board_picker.rs +++ b/src/ui/board_picker.rs @@ -1,5 +1,7 @@ +use crate::draw::{Frame, RenderCaches, RenderCtx, TextMeasurer}; use crate::input::InputState; use crate::ui::primitives::{draw_rounded_rect, text_extents_for_with_engine}; +use crate::ui::theme::popup; use crate::ui_text::{UiTextEngine, UiTextStyle}; use super::constants::{ @@ -27,8 +29,8 @@ pub fn render_board_picker( screen_height: u32, ) { let engine = UiTextEngine::default(); - let measurer = crate::draw::TextMeasurer::default(); - let mut caches = crate::draw::RenderCaches::default(); + let measurer = TextMeasurer::default(); + let mut caches = RenderCaches::default(); render_board_picker_with_halo( &engine, &measurer, @@ -44,9 +46,9 @@ pub fn render_board_picker( #[allow(clippy::too_many_arguments)] pub(crate) fn render_board_picker_with_halo( engine: &UiTextEngine, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, cache: &mut ThumbnailCache, - render: &mut crate::draw::RenderCtx<'_, '_>, + render: &mut RenderCtx<'_, '_>, input_state: &InputState, screen_width: u32, screen_height: u32, @@ -57,13 +59,13 @@ pub(crate) fn render_board_picker_with_halo( return; } - cache.retain_revisions(input_state.boards.board_states().iter().flat_map(|board| { - board - .pages - .pages() + cache.retain_revisions( + input_state + .boards + .board_states() .iter() - .map(crate::draw::Frame::content_revision) - })); + .flat_map(|board| board.pages.pages().iter().map(Frame::content_revision)), + ); let layout = match input_state.board_picker_layout() { Some(layout) => layout, None => return, @@ -90,9 +92,9 @@ pub(crate) fn render_board_picker_with_halo( layout.height, RADIUS_PANEL, ); - constants::set_color(ctx, crate::ui::theme::popup::bg_board_picker()); + constants::set_color(ctx, popup::bg_board_picker()); let _ = ctx.fill_preserve(); - constants::set_color(ctx, crate::ui::theme::popup::border_board_picker()); + constants::set_color(ctx, popup::border_board_picker()); ctx.set_line_width(1.0); let _ = ctx.stroke(); diff --git a/src/ui/board_picker/appearance.rs b/src/ui/board_picker/appearance.rs index d73ed7cfe..33baa43bb 100644 --- a/src/ui/board_picker/appearance.rs +++ b/src/ui/board_picker/appearance.rs @@ -1,4 +1,7 @@ +use std::f64::consts; + use crate::domain::BoardGridKind; +use crate::draw::BoardPaper; use crate::i18n::{tr, trf}; use crate::input::InputState; use crate::input::state::AppearanceField; @@ -167,7 +170,7 @@ pub(super) fn render( draw_size_row( engine, ctx, - SizeRowPaint { + &SizeRowPaint { label_x: 4.0, label_right: row.label_right, label: match edit.kind { @@ -193,7 +196,7 @@ pub(super) fn render( // The pattern stays at board size so the preview shows the real spacing. ctx.scale(1.0 / frame.scale, 1.0 / frame.scale); let (color, grid) = edit.preview(); - if let Ok(paper) = crate::draw::BoardPaper::for_context(color, grid, ctx) { + if let Ok(paper) = BoardPaper::for_context(color, grid, ctx) { let _ = paper.paint(ctx); } let _ = ctx.restore(); @@ -302,14 +305,14 @@ fn draw_frame(ctx: &cairo::Context, (left, top, frame_width, frame_height): (f64 let _ = ctx.stroke(); } -struct SizeRowPaint<'a> { +struct SizeRowPaint<'text> { label_x: f64, label_right: f64, - label: &'a str, + label: &'text str, /// Rail `(start_x, end_x, center_y)`, thumb x, and thumb radius. slider: Option<((f64, f64, f64), f64, f64)>, field: (f64, f64, f64, f64), - text: &'a str, + text: &'text str, focused: bool, /// Typing replaces the size, shown as a selection. armed: bool, @@ -321,7 +324,7 @@ struct SizeRowPaint<'a> { fn draw_size_row( engine: &UiTextEngine, ctx: &cairo::Context, - row: SizeRowPaint<'_>, + row: &SizeRowPaint<'_>, style: UiTextStyle<'_>, ) { let (field_x, field_y, field_width, field_height) = row.field; @@ -357,7 +360,7 @@ fn draw_size_row( } ctx.new_sub_path(); - ctx.arc(thumb_x, center_y, radius, 0.0, std::f64::consts::TAU); + ctx.arc(thumb_x, center_y, radius, 0.0, consts::TAU); constants::set_color(ctx, TEXT_PRIMARY); let _ = ctx.fill_preserve(); constants::set_color( diff --git a/src/ui/board_picker/helpers.rs b/src/ui/board_picker/helpers.rs index 2b28906b4..ec4defefe 100644 --- a/src/ui/board_picker/helpers.rs +++ b/src/ui/board_picker/helpers.rs @@ -3,6 +3,7 @@ use std::f64::consts::PI; use crate::config::Action; use crate::draw::{BLACK, BLUE, Color, GREEN, ORANGE, PINK, RED, WHITE, YELLOW}; use crate::input::InputState; +use crate::label_format; use crate::ui::constants::{self, ICON_DRAG_HANDLE, ICON_SUBMENU_ARROW}; use crate::ui::theme::Rgba; @@ -55,7 +56,7 @@ pub(super) fn board_slot_hint(state: &InputState, index: usize) -> Option, + measurer: &TextMeasurer, + render: &mut RenderCtx<'_, '_>, cache: &mut ThumbnailCache, input_state: &InputState, layout: &BoardPickerLayout, diff --git a/src/ui/board_picker/page_panel/thumbnail/cache.rs b/src/ui/board_picker/page_panel/thumbnail/cache.rs index 1ac4bd217..07f0bdcf3 100644 --- a/src/ui/board_picker/page_panel/thumbnail/cache.rs +++ b/src/ui/board_picker/page_panel/thumbnail/cache.rs @@ -1,5 +1,9 @@ //! Bounded raster reuse keyed by content and the exact card pixels beneath it. //! Retaining the backdrop preserves transparent paint and eraser semantics. +use crate::domain::BoardGrid; +use crate::draw; +use crate::draw::Shape; + use super::content::render_page_content; use super::types::PageContentArgs; use crate::draw::{RenderCtx, TextMeasurer}; @@ -16,7 +20,7 @@ struct ThumbnailKey { content: u64, view: (i32, i32), background: Option<[u64; 3]>, - grid: crate::domain::BoardGrid, + grid: BoardGrid, backdrop: Vec, geometry: [u64; 4], matrix: [u64; 6], @@ -111,8 +115,8 @@ fn try_render_cached( && args.frame.shapes.iter().any(|drawn| { matches!( &drawn.shape, - crate::draw::Shape::Spotlight { magnification, .. } - if crate::draw::spotlight_magnification_is_active(*magnification) + Shape::Spotlight { magnification, .. } + if draw::spotlight_magnification_is_active(*magnification) ) }) { diff --git a/src/ui/board_picker/page_panel/thumbnail/cache/spotlight_tests.rs b/src/ui/board_picker/page_panel/thumbnail/cache/spotlight_tests.rs index 0e8454e04..70b78e99e 100644 --- a/src/ui/board_picker/page_panel/thumbnail/cache/spotlight_tests.rs +++ b/src/ui/board_picker/page_panel/thumbnail/cache/spotlight_tests.rs @@ -1,3 +1,5 @@ +use crate::draw::RenderCaches; + use super::*; use crate::draw::{Color, Frame, Shape}; @@ -33,7 +35,7 @@ fn pixels( } let engine = UiTextEngine::default(); let measurer = TextMeasurer::default(); - let mut draw = crate::draw::RenderCaches::default(); + let mut draw = RenderCaches::default(); let mut render = RenderCtx::new(&ctx, &mut draw); let args = PageContentArgs { render: &mut render, diff --git a/src/ui/board_picker/page_panel/thumbnail/cache/tests.rs b/src/ui/board_picker/page_panel/thumbnail/cache/tests.rs index 9b23a8ddc..e9f1a6501 100644 --- a/src/ui/board_picker/page_panel/thumbnail/cache/tests.rs +++ b/src/ui/board_picker/page_panel/thumbnail/cache/tests.rs @@ -1,3 +1,8 @@ +use std::time::Instant; +use std::{hint, iter}; + +use crate::draw::{EraserBrush, EraserKind, RenderCaches}; + use super::*; use crate::draw::frame::UndoAction; use crate::draw::{Color, FontDescriptor, Frame, Shape}; @@ -51,7 +56,7 @@ fn pixels( ctx.scale(density, density); let engine = UiTextEngine::default(); let measurer = TextMeasurer::default(); - let mut draw = crate::draw::RenderCaches::default(); + let mut draw = RenderCaches::default(); let mut render = RenderCtx::new(&ctx, &mut draw); let args = PageContentArgs { render: &mut render, @@ -111,9 +116,9 @@ fn cached_pixels_follow_edits_rollback_history_and_render_inputs() { assert_parity(&frame, &solid, &mut cache); frame.add_shape(Shape::EraserStroke { points: vec![(50, 100), (180, 140)], - brush: crate::draw::EraserBrush { + brush: EraserBrush { size: 20.0, - kind: crate::draw::EraserKind::Circle, + kind: EraserKind::Circle, }, }); assert_parity(&frame, &transparent, &mut cache); @@ -166,7 +171,7 @@ fn cache_prunes_removed_pages_and_stays_within_entry_and_byte_budgets() { } assert_eq!(cache.entries.len(), MAX_ENTRIES); assert!(cache.bytes <= MAX_BYTES); - cache.retain_revisions(std::iter::empty()); + cache.retain_revisions(iter::empty()); assert!(cache.entries.is_empty()); assert_eq!(cache.bytes, 0); } @@ -211,9 +216,9 @@ fn measure_cached_thumbnail_hover() { let mut cache = ThumbnailCache::default(); pixels(&frame, &background, 1.0, true, Some(&mut cache)); for cached in [false, true] { - let start = std::time::Instant::now(); + let start = Instant::now(); for _ in 0..100 { - std::hint::black_box(pixels( + hint::black_box(pixels( &frame, &background, 1.0, diff --git a/src/ui/board_picker/page_panel/thumbnail/cards.rs b/src/ui/board_picker/page_panel/thumbnail/cards.rs index 37adcf728..943c2efb5 100644 --- a/src/ui/board_picker/page_panel/thumbnail/cards.rs +++ b/src/ui/board_picker/page_panel/thumbnail/cards.rs @@ -1,3 +1,4 @@ +use crate::draw::TextMeasurer; use crate::i18n::{tr, trf}; use crate::input::state::{PAGE_DELETE_ICON_MARGIN, PAGE_DELETE_ICON_SIZE}; use crate::ui::constants::{ @@ -5,7 +6,7 @@ use crate::ui::constants::{ RADIUS_STD, SHADOW_DEEP, TEXT_WHITE, }; use crate::ui::primitives::{draw_rounded_rect, text_extents_for_with_engine}; -use crate::ui::theme::Rgba; +use crate::ui::theme::{Rgba, popup}; use crate::ui_text::{UiTextEngine, UiTextStyle}; // File-local colors with no matching theme token (kept from the pre-theme @@ -26,7 +27,7 @@ use super::types::{PREVIEW_SCALE, PageContentArgs, PagePreviewArgs, PageThumbnai pub(in crate::ui::board_picker::page_panel) fn render_page_thumbnail( engine: &UiTextEngine, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, cache: &mut ThumbnailCache, args: PageThumbnailArgs<'_, '_, '_>, ) { @@ -59,10 +60,10 @@ pub(in crate::ui::board_picker::page_panel) fn render_page_thumbnail( if is_drop_target { constants::set_color(ctx, BG_SELECTION); } else { - constants::set_color(ctx, crate::ui::theme::popup::bg_board_picker()); + constants::set_color(ctx, popup::bg_board_picker()); } let _ = ctx.fill_preserve(); - constants::set_color(ctx, crate::ui::theme::popup::border_board_picker()); + constants::set_color(ctx, popup::border_board_picker()); ctx.set_line_width(1.0); let _ = ctx.stroke(); @@ -135,26 +136,33 @@ pub(in crate::ui::board_picker::page_panel) fn render_page_thumbnail( y, width, height, - is_hovered, - rename_hovered, - duplicate_hovered, - delete_hovered, + ThumbnailHover { + card: is_hovered, + rename: rename_hovered, + duplicate: duplicate_hovered, + delete: delete_hovered, + }, ); draw_page_badge(engine, ctx, x, y, page_number, is_hovered); render_page_name_label(engine, ctx, x, y, width, height, page_name, is_hovered); } -#[allow(clippy::too_many_arguments)] +/// Which parts of a page thumbnail the pointer is over. +#[derive(Debug, Clone, Copy)] +struct ThumbnailHover { + card: bool, + rename: bool, + duplicate: bool, + delete: bool, +} + fn draw_thumbnail_actions( ctx: &cairo::Context, x: f64, y: f64, width: f64, height: f64, - is_hovered: bool, - rename_hovered: bool, - duplicate_hovered: bool, - delete_hovered: bool, + hover: ThumbnailHover, ) { let handle_size = (height * 0.22).clamp(8.0, 12.0); let handle_x = x + width - handle_size - 4.0; @@ -174,21 +182,21 @@ fn draw_thumbnail_actions( rename_x, icon_y, icon_size, - icon_alpha(is_hovered, rename_hovered), + icon_alpha(hover.card, hover.rename), ); draw_duplicate_icon( ctx, duplicate_x, icon_y, icon_size, - icon_alpha(is_hovered, duplicate_hovered), + icon_alpha(hover.card, hover.duplicate), ); draw_delete_icon( ctx, delete_x, icon_y, icon_size, - icon_alpha(is_hovered, delete_hovered), + icon_alpha(hover.card, hover.delete), ); } @@ -258,7 +266,7 @@ pub(in crate::ui::board_picker::page_panel) fn render_add_page_card( } let _ = ctx.fill_preserve(); - constants::set_color(ctx, crate::ui::theme::popup::border_board_picker()); + constants::set_color(ctx, popup::border_board_picker()); ctx.set_line_width(1.0); if !is_hovered { ctx.set_dash(&[4.0, 3.0], 0.0); @@ -312,7 +320,7 @@ pub(in crate::ui::board_picker::page_panel) fn render_add_page_card( pub(in crate::ui::board_picker::page_panel) fn render_page_preview( engine: &UiTextEngine, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, cache: &mut ThumbnailCache, args: PagePreviewArgs<'_, '_, '_>, ) { @@ -358,9 +366,9 @@ pub(in crate::ui::board_picker::page_panel) fn render_page_preview( let _ = ctx.fill(); draw_rounded_rect(ctx, preview_x, preview_y, preview_w, preview_h, RADIUS_MD); - constants::set_color(ctx, crate::ui::theme::popup::bg_board_picker()); + constants::set_color(ctx, popup::bg_board_picker()); let _ = ctx.fill_preserve(); - constants::set_color(ctx, crate::ui::theme::popup::border_board_picker()); + constants::set_color(ctx, popup::border_board_picker()); ctx.set_line_width(1.2); let _ = ctx.stroke(); diff --git a/src/ui/board_picker/page_panel/thumbnail/content.rs b/src/ui/board_picker/page_panel/thumbnail/content.rs index c7cad8f39..3364ecbfe 100644 --- a/src/ui/board_picker/page_panel/thumbnail/content.rs +++ b/src/ui/board_picker/page_panel/thumbnail/content.rs @@ -1,7 +1,9 @@ +use crate::domain::BoardGrid; +use crate::draw; use crate::draw::{ - EraserReplayContext, SpotlightMagnifierScratch, SpotlightMagnifierSource, SpotlightPass, - render_eraser_stroke, render_spotlight_magnification_pass, render_spotlight_pass, - spotlight_regions_for_frame, + BoardPaper, EraserReplayContext, Frame, RenderCtx, Shape, SpotlightMagnifierScratch, + SpotlightMagnifierSource, SpotlightPass, SpotlightRegion, TextMeasurer, render_eraser_stroke, + render_spotlight_magnification_pass, render_spotlight_pass, spotlight_regions_for_frame, }; use crate::i18n::tr; use crate::input::BoardBackground; @@ -27,7 +29,7 @@ const TRANSPARENT_CROSS: Rgba = (1.0, 1.0, 1.0, 0.08); pub(super) fn render_page_content( engine: &UiTextEngine, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, args: PageContentArgs<'_, '_, '_>, ) { let PageContentArgs { @@ -97,11 +99,11 @@ pub(super) fn render_page_content( #[allow(clippy::too_many_arguments)] fn render_frame_shapes( engine: &UiTextEngine, - measurer: &crate::draw::TextMeasurer, - render: &mut crate::draw::RenderCtx<'_, '_>, - frame: &crate::draw::Frame, + measurer: &TextMeasurer, + render: &mut RenderCtx<'_, '_>, + frame: &Frame, background: &BoardBackground, - grid: crate::domain::BoardGrid, + grid: BoardGrid, target_width: u32, target_height: u32, text_halo_enabled: bool, @@ -114,16 +116,14 @@ fn render_frame_shapes( }; ctx.translate(-f64::from(view_x), -f64::from(view_y)); let paper = match background { - BoardBackground::Solid(color) => { - crate::draw::BoardPaper::for_context(*color, grid, ctx).ok() - } + BoardBackground::Solid(color) => BoardPaper::for_context(*color, grid, ctx).ok(), BoardBackground::Transparent => None, }; if let Some(paper) = &paper { let _ = paper.paint(ctx); } let eraser_ctx = EraserReplayContext { - pattern: paper.as_ref().map(crate::draw::BoardPaper::pattern), + pattern: paper.as_ref().map(BoardPaper::pattern), surface: None, backdrop_cache_key: None, bg_color: match background { @@ -138,7 +138,7 @@ fn render_frame_shapes( for drawn in &frame.shapes { match &drawn.shape { - crate::draw::Shape::EraserStroke { points, brush } => { + Shape::EraserStroke { points, brush } => { render_eraser_stroke(ctx, points, brush, &eraser_ctx); } _ => { @@ -192,13 +192,13 @@ fn render_frame_shapes( fn render_unavailable_magnification_labels( engine: &UiTextEngine, ctx: &cairo::Context, - regions: &[crate::draw::SpotlightRegion], + regions: &[SpotlightRegion], ) { - for region in regions + let magnified_regions = regions .iter() - .filter(|region| crate::draw::spotlight_magnification_is_active(region.magnification)) - { - let label = crate::draw::format_spotlight_magnification(region.magnification); + .filter(|region| draw::spotlight_magnification_is_active(region.magnification)); + for region in magnified_regions { + let label = draw::format_spotlight_magnification(region.magnification); let font_size = (region.ry.abs() * 0.35).clamp(18.0, 56.0); let style = UiTextStyle { family: "Sans", @@ -287,6 +287,8 @@ pub(super) fn render_page_name_label( #[cfg(test)] mod tests { + use crate::draw::RenderCaches; + use super::*; use crate::draw::{Color, FontDescriptor, Frame, Shape}; @@ -322,12 +324,9 @@ mod tests { }); render_page_content( &UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), PageContentArgs { - render: &mut crate::draw::RenderCtx::new( - &ctx, - &mut crate::draw::RenderCaches::default(), - ), + render: &mut crate::draw::RenderCtx::new(&ctx, &mut RenderCaches::default()), frame: &frame, background, grid: Default::default(), @@ -364,12 +363,9 @@ mod tests { }); render_page_content( &UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), PageContentArgs { - render: &mut crate::draw::RenderCtx::new( - &ctx, - &mut crate::draw::RenderCaches::default(), - ), + render: &mut crate::draw::RenderCtx::new(&ctx, &mut RenderCaches::default()), frame: &frame, background: &BoardBackground::Solid(Color::new(1.0, 1.0, 1.0, 1.0)), grid: Default::default(), @@ -450,43 +446,42 @@ mod tests { x: 40, y: 36, text: "Image".into(), - color: crate::draw::BLACK, + color: draw::BLACK, size: 20.0, font_descriptor: Default::default(), background_enabled: false, wrap_width: None, }); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let engine = UiTextEngine::default(); - let paint = - |measurer: &crate::draw::TextMeasurer, caches: &mut RenderCaches, density: i32| { - let mut surface = - cairo::ImageSurface::create(cairo::Format::ARgb32, 124 * density, 64 * density) - .unwrap(); - surface.set_device_scale(density as f64, density as f64); - { - let ctx = cairo::Context::new(&surface).unwrap(); - render_page_content( - &engine, - measurer, - PageContentArgs { - render: &mut RenderCtx::new(&ctx, caches), - frame: &frame, - background: &BoardBackground::Solid(crate::draw::WHITE), - grid: Default::default(), - x: 0.0, - y: 0.0, - width: 124.0, - height: 64.0, - screen_width: 120, - screen_height: 60, - text_halo_enabled: false, - }, - ); - } - surface.flush(); - surface.data().unwrap().to_vec() - }; + let paint = |measurer: &TextMeasurer, caches: &mut RenderCaches, density: i32| { + let mut surface = + cairo::ImageSurface::create(cairo::Format::ARgb32, 124 * density, 64 * density) + .unwrap(); + surface.set_device_scale(density as f64, density as f64); + { + let ctx = cairo::Context::new(&surface).unwrap(); + render_page_content( + &engine, + measurer, + PageContentArgs { + render: &mut RenderCtx::new(&ctx, caches), + frame: &frame, + background: &BoardBackground::Solid(draw::WHITE), + grid: Default::default(), + x: 0.0, + y: 0.0, + width: 124.0, + height: 64.0, + screen_width: 120, + screen_height: 60, + text_halo_enabled: false, + }, + ); + } + surface.flush(); + surface.data().unwrap().to_vec() + }; let baseline = Arc::strong_count(&bytes); let mut caches = RenderCaches::default(); let first = paint(&measurer, &mut caches, 1); @@ -500,7 +495,7 @@ mod tests { for density in [1, 2, 1] { let actual = paint(&measurer, &mut caches, density); let fresh = paint( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), &mut RenderCaches::default(), density, ); @@ -533,6 +528,10 @@ mod tests { #[cfg(test)] mod measurement { + use std::time::Instant; + + use crate::draw::RenderCaches; + use super::*; use crate::draw::{Color, DrawnShape, Frame, Shape}; @@ -542,8 +541,8 @@ mod measurement { let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 240, 160).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); let engine = UiTextEngine::default(); - let measurer = crate::draw::TextMeasurer::default(); - let mut caches = crate::draw::RenderCaches::default(); + let measurer = TextMeasurer::default(); + let mut caches = RenderCaches::default(); let mut render = crate::draw::RenderCtx { cairo: &ctx, caches: &mut caches, @@ -574,7 +573,7 @@ mod measurement { ) }) .collect(); - let start = std::time::Instant::now(); + let start = Instant::now(); for _ in 0..100 { render_page_content( &engine, diff --git a/src/ui/board_picker/page_panel/thumbnail/types.rs b/src/ui/board_picker/page_panel/thumbnail/types.rs index d2c28ca34..74930cf53 100644 --- a/src/ui/board_picker/page_panel/thumbnail/types.rs +++ b/src/ui/board_picker/page_panel/thumbnail/types.rs @@ -1,12 +1,14 @@ +use crate::domain::BoardGrid; +use crate::draw::{Frame, RenderCtx}; use crate::input::BoardBackground; pub(in crate::ui::board_picker::page_panel) const PREVIEW_SCALE: f64 = 1.6; -pub(in crate::ui::board_picker::page_panel) struct PageThumbnailArgs<'a, 'c, 'r> { - pub(in crate::ui::board_picker::page_panel) render: &'a mut crate::draw::RenderCtx<'c, 'r>, - pub(in crate::ui::board_picker::page_panel) frame: &'a crate::draw::Frame, - pub(in crate::ui::board_picker::page_panel) background: &'a BoardBackground, - pub(in crate::ui::board_picker::page_panel) grid: crate::domain::BoardGrid, +pub(in crate::ui::board_picker::page_panel) struct PageThumbnailArgs<'page, 'cairo, 'caches> { + pub(in crate::ui::board_picker::page_panel) render: &'page mut RenderCtx<'cairo, 'caches>, + pub(in crate::ui::board_picker::page_panel) frame: &'page Frame, + pub(in crate::ui::board_picker::page_panel) background: &'page BoardBackground, + pub(in crate::ui::board_picker::page_panel) grid: BoardGrid, pub(in crate::ui::board_picker::page_panel) x: f64, pub(in crate::ui::board_picker::page_panel) y: f64, pub(in crate::ui::board_picker::page_panel) width: f64, @@ -15,7 +17,7 @@ pub(in crate::ui::board_picker::page_panel) struct PageThumbnailArgs<'a, 'c, 'r> pub(in crate::ui::board_picker::page_panel) screen_height: u32, pub(in crate::ui::board_picker::page_panel) text_halo_enabled: bool, pub(in crate::ui::board_picker::page_panel) page_number: usize, - pub(in crate::ui::board_picker::page_panel) page_name: Option<&'a str>, + pub(in crate::ui::board_picker::page_panel) page_name: Option<&'page str>, pub(in crate::ui::board_picker::page_panel) is_active: bool, pub(in crate::ui::board_picker::page_panel) is_drop_target: bool, pub(in crate::ui::board_picker::page_panel) is_search_match: bool, @@ -26,11 +28,11 @@ pub(in crate::ui::board_picker::page_panel) struct PageThumbnailArgs<'a, 'c, 'r> pub(in crate::ui::board_picker::page_panel) rename_hovered: bool, } -pub(in crate::ui::board_picker::page_panel) struct PagePreviewArgs<'a, 'c, 'r> { - pub(in crate::ui::board_picker::page_panel) render: &'a mut crate::draw::RenderCtx<'c, 'r>, - pub(in crate::ui::board_picker::page_panel) frame: &'a crate::draw::Frame, - pub(in crate::ui::board_picker::page_panel) background: &'a BoardBackground, - pub(in crate::ui::board_picker::page_panel) grid: crate::domain::BoardGrid, +pub(in crate::ui::board_picker::page_panel) struct PagePreviewArgs<'page, 'cairo, 'caches> { + pub(in crate::ui::board_picker::page_panel) render: &'page mut RenderCtx<'cairo, 'caches>, + pub(in crate::ui::board_picker::page_panel) frame: &'page Frame, + pub(in crate::ui::board_picker::page_panel) background: &'page BoardBackground, + pub(in crate::ui::board_picker::page_panel) grid: BoardGrid, pub(in crate::ui::board_picker::page_panel) thumb_x: f64, pub(in crate::ui::board_picker::page_panel) thumb_y: f64, pub(in crate::ui::board_picker::page_panel) thumb_w: f64, @@ -41,11 +43,11 @@ pub(in crate::ui::board_picker::page_panel) struct PagePreviewArgs<'a, 'c, 'r> { pub(in crate::ui::board_picker::page_panel) page_number: usize, } -pub(in crate::ui::board_picker::page_panel) struct PageContentArgs<'a, 'c, 'r> { - pub(in crate::ui::board_picker::page_panel) render: &'a mut crate::draw::RenderCtx<'c, 'r>, - pub(in crate::ui::board_picker::page_panel) frame: &'a crate::draw::Frame, - pub(in crate::ui::board_picker::page_panel) background: &'a BoardBackground, - pub(in crate::ui::board_picker::page_panel) grid: crate::domain::BoardGrid, +pub(in crate::ui::board_picker::page_panel) struct PageContentArgs<'page, 'cairo, 'caches> { + pub(in crate::ui::board_picker::page_panel) render: &'page mut RenderCtx<'cairo, 'caches>, + pub(in crate::ui::board_picker::page_panel) frame: &'page Frame, + pub(in crate::ui::board_picker::page_panel) background: &'page BoardBackground, + pub(in crate::ui::board_picker::page_panel) grid: BoardGrid, pub(in crate::ui::board_picker::page_panel) x: f64, pub(in crate::ui::board_picker::page_panel) y: f64, pub(in crate::ui::board_picker::page_panel) width: f64, diff --git a/src/ui/board_picker/palette.rs b/src/ui/board_picker/palette.rs index 4c94cb6e7..eb05ab463 100644 --- a/src/ui/board_picker/palette.rs +++ b/src/ui/board_picker/palette.rs @@ -1,3 +1,4 @@ +use crate::input::state::BoardPickerLayout; use crate::input::{BoardBackground, InputState}; use crate::ui::primitives::draw_rounded_rect; @@ -10,7 +11,7 @@ pub(super) const PALETTE_SWATCH_GAP: f64 = 6.0; pub(super) fn render_board_palette( ctx: &cairo::Context, input_state: &InputState, - layout: &crate::input::state::BoardPickerLayout, + layout: &BoardPickerLayout, ) { if layout.palette_rows == 0 || layout.palette_cols == 0 { return; diff --git a/src/ui/board_picker/rows.rs b/src/ui/board_picker/rows.rs index 3c56bee08..cdb42dfea 100644 --- a/src/ui/board_picker/rows.rs +++ b/src/ui/board_picker/rows.rs @@ -27,11 +27,11 @@ pub(super) fn render_board_rows( BoardRowsRenderer::new(engine, ctx, input_state, layout, board_count, max_count).render(); } -struct BoardRowsRenderer<'a> { - engine: &'a UiTextEngine, - ctx: &'a cairo::Context, - input: &'a InputState, - layout: &'a BoardPickerLayout, +struct BoardRowsRenderer<'render> { + engine: &'render UiTextEngine, + ctx: &'render cairo::Context, + input: &'render InputState, + layout: &'render BoardPickerLayout, board_count: usize, max_count: usize, rows_top: f64, @@ -43,17 +43,17 @@ struct BoardRowsRenderer<'a> { highlight_index: Option, selected_index: Option, active_board_index: usize, - edit_state: Option<(BoardPickerEditMode, usize, &'a str)>, + edit_state: Option<(BoardPickerEditMode, usize, &'render str)>, pinned_count: usize, body_style: UiTextStyle<'static>, } -impl<'a> BoardRowsRenderer<'a> { +impl<'render> BoardRowsRenderer<'render> { fn new( - engine: &'a UiTextEngine, - ctx: &'a cairo::Context, - input: &'a InputState, - layout: &'a BoardPickerLayout, + engine: &'render UiTextEngine, + ctx: &'render cairo::Context, + input: &'render InputState, + layout: &'render BoardPickerLayout, board_count: usize, max_count: usize, ) -> Self { diff --git a/src/ui/board_picker/tests.rs b/src/ui/board_picker/tests.rs index 5f49faeea..0290712c8 100644 --- a/src/ui/board_picker/tests.rs +++ b/src/ui/board_picker/tests.rs @@ -1,9 +1,16 @@ +use std::env; +use std::fs::File; +use std::path::Path; + +use crate::input::events::Key; +use crate::input::state::test_support; + use super::*; fn pixels( engine: &UiTextEngine, - measurer: &crate::draw::TextMeasurer, - caches: &mut crate::draw::RenderCaches, + measurer: &TextMeasurer, + caches: &mut RenderCaches, state: &InputState, size: (i32, i32), density: i32, @@ -36,10 +43,10 @@ fn pixels( fn retained_board_text_owner_matches_fresh_during_unicode_rename_and_small_layouts() { check_appearance_sheet_fits_each_surface(); let engine = UiTextEngine::default(); - let measurer = crate::draw::TextMeasurer::default(); - let mut caches = crate::draw::RenderCaches::default(); - let mut state = crate::input::state::test_support::make_test_input_state(); - state.open_board_picker_with_measurer(&crate::draw::TextMeasurer::default()); + let measurer = TextMeasurer::default(); + let mut caches = RenderCaches::default(); + let mut state = test_support::make_test_input_state(); + state.open_board_picker_with_measurer(&TextMeasurer::default()); for (width, height, density) in [(900, 700, 1), (420, 300, 2), (900, 700, 1)] { let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); @@ -71,8 +78,8 @@ fn retained_board_text_owner_matches_fresh_during_unicode_rename_and_small_layou ); let expected = pixels( &UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), - &mut crate::draw::RenderCaches::default(), + &TextMeasurer::default(), + &mut RenderCaches::default(), &state, (width, height), density, @@ -99,14 +106,14 @@ fn retained_board_text_owner_matches_fresh_during_unicode_rename_and_small_layou fn check_appearance_sheet_fits_each_surface() { let engine = UiTextEngine::default(); - let measurer = crate::draw::TextMeasurer::default(); - let mut state = crate::input::state::test_support::make_test_input_state(); + let measurer = TextMeasurer::default(); + let mut state = test_support::make_test_input_state(); state.switch_board_force("whiteboard"); state.open_board_picker_with_measurer(&measurer); state.board_picker_edit_color_selected_with_measurer(&measurer); - state.board_appearance_key(crate::input::events::Key::Tab); - state.board_appearance_key(crate::input::events::Key::Right); - state.board_appearance_key(crate::input::events::Key::Right); + state.board_appearance_key(Key::Tab); + state.board_appearance_key(Key::Right); + state.board_appearance_key(Key::Right); for (width, height) in [(1920, 1080), (1280, 720), (900, 700), (420, 300)] { let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); @@ -117,12 +124,12 @@ fn check_appearance_sheet_fits_each_surface() { let data = pixels( &engine, &measurer, - &mut crate::draw::RenderCaches::default(), + &mut RenderCaches::default(), &state, (width, height), 1, ); - if let Ok(folder) = std::env::var("WAYSCRIBER_GRID_UI_ARTIFACTS") { + if let Ok(folder) = env::var("WAYSCRIBER_GRID_UI_ARTIFACTS") { let surface = cairo::ImageSurface::create_for_data( data, cairo::Format::ARgb32, @@ -131,10 +138,9 @@ fn check_appearance_sheet_fits_each_surface() { width * 4, ) .unwrap(); - let mut file = std::fs::File::create( - std::path::Path::new(&folder).join(format!("paper-editor-{width}x{height}.png")), - ) - .unwrap(); + let mut file = + File::create(Path::new(&folder).join(format!("paper-editor-{width}x{height}.png"))) + .unwrap(); surface.write_to_png(&mut file).unwrap(); } } diff --git a/src/ui/color_picker_popup.rs b/src/ui/color_picker_popup.rs index 64517b986..ba796fb9f 100644 --- a/src/ui/color_picker_popup.rs +++ b/src/ui/color_picker_popup.rs @@ -3,25 +3,21 @@ //! Renders a modal popup with a large gradient color picker, //! hex input field, and OK/Cancel buttons. -use crate::draw::Color; use crate::i18n::tr; use crate::input::InputState; use crate::input::state::{ COLOR_PICKER_PREVIEW_SIZE, COLOR_PICKER_RECENT_SWATCH_COUNT as RECENT_SWATCH_COUNT, COLOR_PICKER_RECENT_SWATCH_SIZE as RECENT_SWATCH_SIZE, ColorPickerPopupLayout, }; +use crate::toolbar_icons; use crate::ui::primitives::{ - checkerboard_behind, draw_alpha_checkerboard, draw_rounded_rect, ellipsize_to_fit_with_engine, - text_extents_for_with_engine, + draw_rounded_rect, ellipsize_to_fit_with_engine, text_extents_for_with_engine, }; -use crate::ui::theme::Rgba; +use crate::ui::theme::{Rgba, popup}; +use crate::ui::tooltip; use crate::ui_text::{UiTextEngine, UiTextStyle}; -use super::constants::{ - self, ACCENT_BRIGHT, ACCENT_PRIMARY, BG_INPUT_SELECTION, INPUT_BG, INPUT_BORDER_FOCUSED, - INPUT_CARET, OVERLAY_DIM_MEDIUM, RADIUS_MD, RADIUS_PANEL, RADIUS_SM, RADIUS_STD, TEXT_HINT_DIM, - TEXT_PRIMARY, -}; +use super::constants::{self, OVERLAY_DIM_MEDIUM, RADIUS_PANEL, TEXT_HINT_DIM, TEXT_PRIMARY}; mod controls; @@ -118,8 +114,8 @@ pub(crate) fn color_picker_popup_visual_geometry_with_engine( ( x, y, - width + crate::ui::tooltip::TOOLTIP_PAINT_OUTSET, - height + crate::ui::tooltip::TOOLTIP_PAINT_OUTSET, + width + tooltip::TOOLTIP_PAINT_OUTSET, + height + tooltip::TOOLTIP_PAINT_OUTSET, ), ); } @@ -193,9 +189,9 @@ pub(crate) fn render_color_picker_popup_with_engine( layout.height, RADIUS_PANEL, ); - constants::set_color(ctx, crate::ui::theme::popup::bg_modal()); + constants::set_color(ctx, popup::bg_modal()); let _ = ctx.fill_preserve(); - constants::set_color(ctx, crate::ui::theme::popup::border_modal()); + constants::set_color(ctx, popup::border_modal()); ctx.set_line_width(1.0); let _ = ctx.stroke(); @@ -315,7 +311,7 @@ pub(crate) fn render_color_picker_popup_with_engine( layout.copy_btn_y, size, copy_hover, - crate::toolbar_icons::draw_icon_copy, + toolbar_icons::draw_icon_copy, 16.0, ); draw_action_button( @@ -324,7 +320,7 @@ pub(crate) fn render_color_picker_popup_with_engine( layout.paste_btn_y, size, paste_hover, - crate::toolbar_icons::draw_icon_paste, + toolbar_icons::draw_icon_paste, 16.0, ); if layout.eyedropper_enabled { @@ -334,7 +330,7 @@ pub(crate) fn render_color_picker_popup_with_engine( layout.eyedropper_btn_y, size, eyedropper_hover, - crate::toolbar_icons::draw_icon_eyedropper, + toolbar_icons::draw_icon_eyedropper, 18.0, ); } @@ -457,6 +453,8 @@ fn fit_title_to_panel( #[cfg(test)] mod tests { + use crate::input::state::ColorPickerPopupLayoutOptions; + use super::*; fn test_context() -> cairo::Context { @@ -492,11 +490,8 @@ mod tests { fn wide_titles_are_trimmed_to_the_panel_not_a_character_budget() { let engine = &UiTextEngine::default(); let ctx = test_context(); - let layout = ColorPickerPopupLayout::compute( - 1920, - 1080, - crate::input::state::ColorPickerPopupLayoutOptions::ALL, - ); + let layout = + ColorPickerPopupLayout::compute(1920, 1080, ColorPickerPopupLayoutOptions::ALL); let content_width = layout.width - TITLE_INSET * 2.0; // Wide glyphs: few characters, far more pixels than a Latin label of diff --git a/src/ui/color_picker_popup/controls.rs b/src/ui/color_picker_popup/controls.rs index cc1f3f76d..5dcc4a8dd 100644 --- a/src/ui/color_picker_popup/controls.rs +++ b/src/ui/color_picker_popup/controls.rs @@ -1,6 +1,28 @@ //! Popup color controls and their labels/tooltips. -use super::*; +use std::f64::consts; + +use crate::draw; +use crate::draw::color; +use crate::ui::theme::popup; +use crate::ui::tooltip; + +use super::super::constants; +use super::super::constants::{ + ACCENT_BRIGHT, ACCENT_PRIMARY, BG_INPUT_SELECTION, INPUT_BG, INPUT_BORDER_FOCUSED, INPUT_CARET, + RADIUS_MD, RADIUS_SM, RADIUS_STD, TEXT_PRIMARY, +}; +use super::{ + BUTTON_HOVER_GLOW, BUTTON_SECONDARY_BG, BUTTON_SECONDARY_BG_HOVER, BUTTON_SECONDARY_BORDER, + BUTTON_SECONDARY_BORDER_HOVER, EYEDROPPER_BG, GRADIENT_BORDER, HEX_INVALID_BORDER, + HEX_INVALID_GLOW, INDICATOR_OUTLINE, INDICATOR_RING, INPUT_BORDER_IDLE, RECENT_SWATCH_SIZE, + SWATCH_BORDER_ON_DARK, SWATCH_BORDER_ON_LIGHT, TOOLTIP_POINTER_OFFSET, TOOLTIP_SCREEN_MARGIN, +}; +use crate::draw::Color; +use crate::ui::primitives::{ + checkerboard_behind, draw_alpha_checkerboard, draw_rounded_rect, text_extents_for_with_engine, +}; +use crate::ui_text::{UiTextEngine, UiTextStyle}; /// Draw the HSV color gradient. /// Draw the saturation (x) by value (y) square for one hue. @@ -10,7 +32,7 @@ use super::*; /// axes are real: unlike the previous hue-by-value gradient, every point here /// maps to the colour actually produced by clicking it. pub(super) fn draw_sat_val_square(ctx: &cairo::Context, x: f64, y: f64, w: f64, h: f64, hue: f64) { - let full = crate::draw::color::hsv_to_rgb(hue, 1.0, 1.0); + let full = color::hsv_to_rgb(hue, 1.0, 1.0); let sat_grad = cairo::LinearGradient::new(x, y, x + w, y); sat_grad.add_color_stop_rgba(0.0, 1.0, 1.0, 1.0, 1.0); @@ -37,7 +59,7 @@ pub(super) fn draw_hue_bar(ctx: &cairo::Context, x: f64, y: f64, w: f64, h: f64) let hue_grad = cairo::LinearGradient::new(x, y, x + w, y); for step in 0..=6 { let t = f64::from(step) / 6.0; - let color = crate::draw::color::hsv_to_rgb(t, 1.0, 1.0); + let color = color::hsv_to_rgb(t, 1.0, 1.0); hue_grad.add_color_stop_rgba(t, color.r, color.g, color.b, 1.0); } ctx.rectangle(x, y, w, h); @@ -122,18 +144,18 @@ pub(super) fn draw_color_indicator(ctx: &cairo::Context, x: f64, y: f64, color: // Outer white ring constants::set_color(ctx, INDICATOR_RING); - ctx.arc(x, y, radius + 2.0, 0.0, std::f64::consts::PI * 2.0); + ctx.arc(x, y, radius + 2.0, 0.0, consts::PI * 2.0); let _ = ctx.fill(); // Inner color circle ctx.set_source_rgba(color.r, color.g, color.b, 1.0); - ctx.arc(x, y, radius, 0.0, std::f64::consts::PI * 2.0); + ctx.arc(x, y, radius, 0.0, consts::PI * 2.0); let _ = ctx.fill(); // Dark outline constants::set_color(ctx, INDICATOR_OUTLINE); ctx.set_line_width(1.0); - ctx.arc(x, y, radius + 2.0, 0.0, std::f64::consts::PI * 2.0); + ctx.arc(x, y, radius + 2.0, 0.0, consts::PI * 2.0); let _ = ctx.stroke(); } @@ -151,7 +173,7 @@ pub(super) fn draw_preview_swatch(ctx: &cairo::Context, x: f64, y: f64, size: f6 let _ = ctx.fill(); // Border - let luminance = crate::draw::perceived_luminance(color.r, color.g, color.b); + let luminance = draw::perceived_luminance(color.r, color.g, color.b); if luminance < 0.3 { constants::set_color(ctx, SWATCH_BORDER_ON_DARK); } else { @@ -272,7 +294,7 @@ pub(super) fn draw_action_button( constants::set_color(ctx, EYEDROPPER_BG); } let _ = ctx.fill_preserve(); - constants::set_color(ctx, crate::ui::theme::popup::border_modal()); + constants::set_color(ctx, popup::border_modal()); ctx.set_line_width(1.0); let _ = ctx.stroke(); constants::set_color(ctx, TEXT_PRIMARY); @@ -304,7 +326,7 @@ pub(super) fn draw_action_tooltip( return; }; - crate::ui::tooltip::draw_tooltip(engine, ctx, text, rect); + tooltip::draw_tooltip(engine, ctx, text, rect); } pub(super) fn action_tooltip_geometry( @@ -315,7 +337,7 @@ pub(super) fn action_tooltip_geometry( screen_width: f64, screen_height: f64, ) -> Option<(f64, f64, f64, f64)> { - let (width, height) = crate::ui::tooltip::tooltip_size(engine, text)?; + let (width, height) = tooltip::tooltip_size(engine, text)?; let max_x = (screen_width - width - TOOLTIP_SCREEN_MARGIN).max(TOOLTIP_SCREEN_MARGIN); let x = (anchor_x + TOOLTIP_POINTER_OFFSET).clamp(TOOLTIP_SCREEN_MARGIN, max_x); let above_y = anchor_y - height - TOOLTIP_POINTER_OFFSET; diff --git a/src/ui/color_picker_popup/tests/engine.rs b/src/ui/color_picker_popup/tests/engine.rs index 4514221f2..af6469d99 100644 --- a/src/ui/color_picker_popup/tests/engine.rs +++ b/src/ui/color_picker_popup/tests/engine.rs @@ -1,3 +1,5 @@ +use crate::input::state::test_support; + use super::*; fn pixels(density: i32, paint: impl FnOnce(&cairo::Context)) -> Vec { @@ -13,7 +15,7 @@ fn pixels(density: i32, paint: impl FnOnce(&cairo::Context)) -> Vec { #[test] fn retained_color_popup_engine_shares_tooltip_damage_and_paint_across_targets() { let engine = UiTextEngine::default(); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); input.open_color_picker_popup(); input.update_color_picker_popup_layout(800, 600); let layout = input.color_picker_popup_layout().unwrap(); @@ -35,12 +37,8 @@ fn retained_color_popup_engine_shares_tooltip_damage_and_paint_across_targets() let (text, x, y) = layout.action_tooltip_anchor_at(anchor.0, anchor.1).unwrap(); let tip = action_tooltip_geometry(&engine, text, x, y, 800.0, 600.0).unwrap(); assert!(bounds.0 <= tip.0 && bounds.1 <= tip.1); - assert!( - bounds.0 + bounds.2 >= tip.0 + tip.2 + crate::ui::tooltip::TOOLTIP_PAINT_OUTSET - ); - assert!( - bounds.1 + bounds.3 >= tip.1 + tip.3 + crate::ui::tooltip::TOOLTIP_PAINT_OUTSET - ); + assert!(bounds.0 + bounds.2 >= tip.0 + tip.2 + tooltip::TOOLTIP_PAINT_OUTSET); + assert!(bounds.1 + bounds.3 >= tip.1 + tip.3 + tooltip::TOOLTIP_PAINT_OUTSET); } let actual = pixels(density, |ctx| { render_color_picker_popup_with_engine(&engine, ctx, &input, 800, 600) diff --git a/src/ui/command_palette.rs b/src/ui/command_palette.rs index 4219ccacf..82d5cf12b 100644 --- a/src/ui/command_palette.rs +++ b/src/ui/command_palette.rs @@ -1,12 +1,14 @@ //! Command palette UI rendering. -use crate::config::action_label; +use std::collections::HashMap; +use std::iter; + +use crate::config::Action; use crate::i18n::tr; use crate::input::InputState; use crate::input::state::{ COMMAND_PALETTE_INPUT_HEIGHT, COMMAND_PALETTE_ITEM_HEIGHT, COMMAND_PALETTE_LIST_GAP, - COMMAND_PALETTE_PADDING, COMMAND_PALETTE_QUERY_PLACEHOLDER, COMMAND_PALETTE_TOP_RATIO, - CommandPaletteListRow, + COMMAND_PALETTE_PADDING, COMMAND_PALETTE_QUERY_PLACEHOLDER, CommandPaletteListRow, }; use crate::ui_text::{UiTextEngine, UiTextStyle}; @@ -61,7 +63,7 @@ const TOOLTIP_POINTER_OFFSET: f64 = 12.0; pub(crate) enum CommandPaletteView { Closed, Capture { - action: crate::config::Action, + action: Action, bindings: Vec, }, List(PaletteListView), @@ -76,7 +78,7 @@ pub(crate) struct PaletteListView { selected: usize, /// The row under the pointer, which also shows its shortcut controls. hovered: Option, - bindings: std::collections::HashMap>, + bindings: HashMap>, tooltip: Option<(String, i32, i32)>, } @@ -705,7 +707,7 @@ fn ellipsize_to_width( let boundaries: Vec = text .char_indices() .map(|(index, _)| index) - .chain(std::iter::once(text.len())) + .chain(iter::once(text.len())) .collect(); let mut low = 0; let mut high = boundaries.len() - 1; diff --git a/src/ui/command_palette/command_palette_row.rs b/src/ui/command_palette/command_palette_row.rs index e1263cbc9..89143b185 100644 --- a/src/ui/command_palette/command_palette_row.rs +++ b/src/ui/command_palette/command_palette_row.rs @@ -1,12 +1,13 @@ use crate::config::KeybindingsConfig; use crate::config::action_meta::ActionMeta; -use crate::input::state::COMMAND_PALETTE_ITEM_HEIGHT; -use crate::input::state::query_tokens; use crate::input::state::{ - COMMAND_PALETTE_ROW_ACTION_COUNT, COMMAND_PALETTE_ROW_ACTION_GAP, + COMMAND_PALETTE_ITEM_HEIGHT, COMMAND_PALETTE_ROW_ACTION_COUNT, COMMAND_PALETTE_ROW_ACTION_GAP, COMMAND_PALETTE_ROW_ACTION_SIZE, COMMAND_PALETTE_ROW_ICON_GAP, COMMAND_PALETTE_ROW_ICON_SIZE, + query_tokens, }; +use crate::toolbar_icons; use crate::ui::text_highlight::{HighlightStyle, draw_highlight_with_engine, find_match_range}; +use crate::ui::theme::Theme; use crate::ui_text::{UiTextEngine, UiTextStyle}; use super::super::constants::{self, RADIUS_SM}; @@ -46,7 +47,7 @@ pub(super) fn command_palette_row_styles() -> CommandPaletteRowStyle { #[allow(clippy::too_many_arguments)] pub(super) fn render_command_row( - theme: &crate::ui::theme::Theme, + theme: &Theme, engine: &UiTextEngine, ctx: &cairo::Context, query: &str, @@ -80,7 +81,7 @@ pub(super) fn render_command_row( if let Some(icon) = cmd.icon { let icon_alpha = if is_selected { 0.95 } else { 0.7 }; constants::set_color(ctx, constants::with_alpha(theme.text_primary, icon_alpha)); - crate::toolbar_icons::action_icon_painter(icon)( + toolbar_icons::action_icon_painter(icon)( ctx, inner_x + 10.0, item_y + (COMMAND_PALETTE_ITEM_HEIGHT - COMMAND_PALETTE_ROW_ICON_SIZE) / 2.0, @@ -172,7 +173,7 @@ pub(super) fn render_command_row( } fn render_command_row_actions( - theme: &crate::ui::theme::Theme, + theme: &Theme, ctx: &cairo::Context, content_right: f64, item_y: f64, @@ -183,19 +184,19 @@ fn render_command_row_actions( let icon_y = item_y + (COMMAND_PALETTE_ITEM_HEIGHT - COMMAND_PALETTE_ROW_ACTION_SIZE) / 2.0; let alpha = if selected { 0.95 } else { 0.62 }; constants::set_color(ctx, constants::with_alpha(theme.text_primary, alpha)); - crate::toolbar_icons::draw_icon_pencil( + toolbar_icons::draw_icon_pencil( ctx, left + 3.0, icon_y + 3.0, COMMAND_PALETTE_ROW_ACTION_SIZE - 6.0, ); - crate::toolbar_icons::draw_icon_clear( + toolbar_icons::draw_icon_clear( ctx, left + stride + 3.0, icon_y + 3.0, COMMAND_PALETTE_ROW_ACTION_SIZE - 6.0, ); - crate::toolbar_icons::draw_icon_refresh( + toolbar_icons::draw_icon_refresh( ctx, left + stride * 2.0 + 3.0, icon_y + 3.0, @@ -219,7 +220,7 @@ fn render_command_row_label( /// drawn by the caller afterwards, over these boxes. #[allow(clippy::too_many_arguments)] fn draw_label_match_highlights( - theme: &crate::ui::theme::Theme, + theme: &Theme, engine: &UiTextEngine, ctx: &cairo::Context, query: &str, @@ -249,7 +250,7 @@ fn draw_label_match_highlights( #[allow(clippy::too_many_arguments)] pub(super) fn render_command_row_shortcut_badge( - theme: &crate::ui::theme::Theme, + theme: &Theme, engine: &UiTextEngine, ctx: &cairo::Context, item_y: f64, @@ -332,7 +333,7 @@ pub(super) fn render_command_row_shortcut_badge( #[allow(clippy::too_many_arguments)] pub(super) fn render_command_row_description( - theme: &crate::ui::theme::Theme, + theme: &Theme, engine: &UiTextEngine, ctx: &cairo::Context, desc_style: &UiTextStyle, diff --git a/src/ui/command_palette/modals.rs b/src/ui/command_palette/modals.rs index 242856205..f0ad95279 100644 --- a/src/ui/command_palette/modals.rs +++ b/src/ui/command_palette/modals.rs @@ -1,8 +1,18 @@ //! Palette shortcut-capture modal and action tooltips. +use crate::config::Action; use crate::i18n::{tr, trf}; +use crate::ui::theme::Theme; -use super::*; +use super::super::constants; +use super::super::primitives::draw_rounded_rect; +use super::{ + COMMAND_PALETTE_SHORTCUT_TEXT_SIZE, FRAME_SHADOW_OFFSET, TOOLTIP_PADDING_X, TOOLTIP_PADDING_Y, + TOOLTIP_POINTER_OFFSET, command_palette_text_style, draw_command_palette_frame, +}; +use crate::config::action_label; +use crate::input::state::COMMAND_PALETTE_TOP_RATIO; +use crate::ui_text::UiTextEngine; pub(super) fn command_palette_action_tooltip_geometry( engine: &UiTextEngine, @@ -29,7 +39,7 @@ pub(super) fn command_palette_action_tooltip_geometry( #[allow(clippy::too_many_arguments)] pub(super) fn draw_command_palette_action_tooltip( - theme: &crate::ui::theme::Theme, + theme: &Theme, engine: &UiTextEngine, ctx: &cairo::Context, text: &str, @@ -81,11 +91,11 @@ pub(super) fn keybinding_capture_geometry( } pub(super) fn render_keybinding_capture( - theme: &crate::ui::theme::Theme, + theme: &Theme, engine: &UiTextEngine, ctx: &cairo::Context, current: &[String], - action: crate::config::Action, + action: Action, screen_width: u32, screen_height: u32, ) { diff --git a/src/ui/command_palette/tests/engine.rs b/src/ui/command_palette/tests/engine.rs index cc8ee8bd9..9e9066062 100644 --- a/src/ui/command_palette/tests/engine.rs +++ b/src/ui/command_palette/tests/engine.rs @@ -1,3 +1,10 @@ +use std::fs::File; +use std::path::PathBuf; +use std::{env, fs}; + +use crate::input::state::test_support; +use crate::ui::theme::Theme; + use super::*; fn pixels(density: i32, paint: impl FnOnce(&cairo::Context)) -> Vec { @@ -13,7 +20,7 @@ fn pixels(density: i32, paint: impl FnOnce(&cairo::Context)) -> Vec { #[test] fn retained_palette_engine_matches_public_paint_and_geometry_across_targets() { let engine = UiTextEngine::default(); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); input.toggle_command_palette(); for query in ["", "capture", "No match 測試 🖌️"] { input.command_palette.set_query(query); @@ -52,7 +59,7 @@ fn retained_palette_tooltip_geometry_contains_pixels_and_unicode_highlight_is_vi .unwrap(); let actual = pixels(density, |ctx| { draw_command_palette_action_tooltip( - &crate::ui::theme::Theme::dark(), + &Theme::dark(), &engine, ctx, text, @@ -64,7 +71,7 @@ fn retained_palette_tooltip_geometry_contains_pixels_and_unicode_highlight_is_vi }); let expected = pixels(density, |ctx| { draw_command_palette_action_tooltip( - &crate::ui::theme::Theme::dark(), + &Theme::dark(), &UiTextEngine::default(), ctx, text, @@ -129,7 +136,7 @@ fn prepared_palette_paints_without_application_state() { let engine = UiTextEngine::default(); let closed = pixels(1, |ctx| { paint_command_palette( - &crate::ui::theme::Theme::dark(), + &Theme::dark(), &engine, ctx, &CommandPaletteView::Closed, @@ -150,14 +157,7 @@ fn prepared_palette_paints_without_application_state() { tooltip: None, }); let painted = pixels(1, |ctx| { - paint_command_palette( - &crate::ui::theme::Theme::dark(), - &engine, - ctx, - &view, - 800, - 600, - ) + paint_command_palette(&Theme::dark(), &engine, ctx, &view, 800, 600) }); assert!(painted.iter().any(|byte| *byte != 0)); // The dimmer and the panel have different opacity, so this checks the @@ -169,27 +169,24 @@ fn prepared_palette_paints_without_application_state() { #[test] fn palette_theme_refresh_covers_list_capture_and_tooltip() { let engine = UiTextEngine::default(); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); input.toggle_command_palette(); for query in ["", "capture", "No match 測試 🖌️"] { input.command_palette.set_query(query); let view = CommandPaletteView::prepare(&input, 800, 480); - let paint = |theme: &crate::ui::theme::Theme| { + let paint = |theme: &Theme| { pixels(1, |ctx| { paint_command_palette(theme, &engine, ctx, &view, 800, 480); }) }; - let dark = paint(&crate::ui::theme::Theme::dark()); - let light = paint(&crate::ui::theme::Theme::light()); + let dark = paint(&Theme::dark()); + let light = paint(&Theme::light()); assert_ne!(dark, light, "query {query}"); - assert_eq!(dark, paint(&crate::ui::theme::Theme::dark())); - if let Some(dir) = std::env::var_os("WAYSCRIBER_PALETTE_REFERENCES") { - let dir = std::path::PathBuf::from(dir); - std::fs::create_dir_all(&dir).unwrap(); - for (name, theme) in [ - ("dark", crate::ui::theme::Theme::dark()), - ("light", crate::ui::theme::Theme::light()), - ] { + assert_eq!(dark, paint(&Theme::dark())); + if let Some(dir) = env::var_os("WAYSCRIBER_PALETTE_REFERENCES") { + let dir = PathBuf::from(dir); + fs::create_dir_all(&dir).unwrap(); + for (name, theme) in [("dark", Theme::dark()), ("light", Theme::light())] { let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 800, 480).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); paint_command_palette(&theme, &engine, &ctx, &view, 800, 480); @@ -204,13 +201,13 @@ fn palette_theme_refresh_covers_list_capture_and_tooltip() { } ); surface - .write_to_png(&mut std::fs::File::create(dir.join(name)).unwrap()) + .write_to_png(&mut File::create(dir.join(name)).unwrap()) .unwrap(); } } } for capture in [false, true] { - let paint = |theme: &crate::ui::theme::Theme| { + let paint = |theme: &Theme| { pixels(1, |ctx| { if capture { render_keybinding_capture( @@ -218,7 +215,7 @@ fn palette_theme_refresh_covers_list_capture_and_tooltip() { &engine, ctx, &["Ctrl+C".into()], - crate::config::Action::CopySelection, + Action::CopySelection, 800, 600, ); @@ -236,10 +233,10 @@ fn palette_theme_refresh_covers_list_capture_and_tooltip() { } }) }; - let dark = paint(&crate::ui::theme::Theme::dark()); - let light = paint(&crate::ui::theme::Theme::light()); + let dark = paint(&Theme::dark()); + let light = paint(&Theme::light()); assert_ne!(dark, light); - assert_eq!(dark, paint(&crate::ui::theme::Theme::dark())); + assert_eq!(dark, paint(&Theme::dark())); } } @@ -278,7 +275,7 @@ fn strip_is_blank(pixels: &[u8], (left, top, right, bottom): (i32, i32, i32, i32 #[test] fn row_shortcut_controls_show_only_on_the_selected_and_hovered_rows() { let engine = UiTextEngine::default(); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); input.toggle_command_palette(); input.update_pointer_position(0, 0); let rows = input.command_palette_rows(); @@ -316,7 +313,7 @@ fn row_shortcut_controls_show_only_on_the_selected_and_hovered_rows() { #[test] fn palette_rows_highlight_literal_label_matches_but_not_fuzzy_matches() { let engine = UiTextEngine::default(); - let mut input = crate::input::state::test_support::make_test_input_state(); + let mut input = test_support::make_test_input_state(); input.toggle_command_palette(); input.command_palette.set_query("tool"); let mut view = CommandPaletteView::prepare(&input, 800, 600); @@ -326,7 +323,7 @@ fn palette_rows_highlight_literal_label_matches_but_not_fuzzy_matches() { let (x, y, width, height) = list.geometry; let rows_top = y + COMMAND_PALETTE_PADDING + COMMAND_PALETTE_INPUT_HEIGHT + COMMAND_PALETTE_LIST_GAP; - let mut theme = crate::ui::theme::Theme::dark(); + let mut theme = Theme::dark(); theme.accent = (1.0, 0.0, 1.0, 1.0); let count_highlight = |query_view: &CommandPaletteView| { diff --git a/src/ui/context_menu.rs b/src/ui/context_menu.rs index 4fb0e4a4a..70050951b 100644 --- a/src/ui/context_menu.rs +++ b/src/ui/context_menu.rs @@ -1,7 +1,7 @@ use crate::input::InputState; use crate::input::state::{ContextMenuEntry, ContextMenuLayout, ContextMenuState, SubmenuSide}; use crate::ui::primitives::draw_rounded_rect; -use crate::ui::theme::Rgba; +use crate::ui::theme::{Rgba, popup}; use crate::ui_text::{UiTextEngine, UiTextStyle}; use super::constants::{ @@ -58,7 +58,7 @@ pub(crate) fn render_context_menu_with_engine( &layout, &entries, MenuStyle { - surface: crate::ui::theme::popup::bg_context_menu(), + surface: popup::bg_context_menu(), arrow_side: side, shadow: false, }, @@ -81,7 +81,7 @@ pub(crate) fn render_context_menu_with_engine( pane, &submenu_entries, MenuStyle { - surface: crate::ui::theme::popup::bg_context_submenu(), + surface: popup::bg_context_submenu(), arrow_side: side, shadow: true, }, @@ -172,7 +172,7 @@ fn draw_menu( ); constants::set_color(ctx, style.surface); let _ = ctx.fill_preserve(); - constants::set_color(ctx, crate::ui::theme::popup::border_context_menu()); + constants::set_color(ctx, popup::border_context_menu()); ctx.set_line_width(1.0); let _ = ctx.stroke(); @@ -355,6 +355,8 @@ fn draw_hint_footer( #[cfg(test)] mod engine_tests { + use crate::input::state::test_support; + use super::*; use crate::input::state::ContextMenuKind; @@ -378,7 +380,7 @@ mod engine_tests { #[ignore = "isolated by tools/lint-and-test.sh; Cairo race: https://gitlab.freedesktop.org/cairo/cairo/-/merge_requests/81"] fn retained_context_menu_owner_preserves_layout_pixels_and_row_hits() { let engine = UiTextEngine::default(); - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); for (kind, density) in [ (ContextMenuKind::Canvas, 1), (ContextMenuKind::Zoom, 2), diff --git a/src/ui/font_picker.rs b/src/ui/font_picker.rs index 3b36f92ff..ca227c3e0 100644 --- a/src/ui/font_picker.rs +++ b/src/ui/font_picker.rs @@ -5,7 +5,11 @@ //! visible window is capped: only the rows on screen are ever laid out, so a //! system with 269 families costs the same as one with 12. +use pangocairo::functions; + +use crate::draw::font; use crate::i18n::{tr, trf}; +use crate::input::state; use crate::input::state::{ FontPickerLayout, FontPickerRow, FontPickerTarget, InputState, font_picker_layout, }; @@ -33,7 +37,7 @@ pub fn render_font_picker(ctx: &cairo::Context, state: &InputState, width: u32, let families = state.font_picker_families(); let layout = font_picker_layout(width, height, families.len()); let current = state.font_picker_current_family(); - let rows = crate::input::state::font_picker_rows( + let rows = state::font_picker_rows( layout, &families, state.font_picker_scroll(), @@ -361,8 +365,8 @@ fn bounded_layout( max_width: f64, text: &str, ) -> pango::Layout { - let layout = pangocairo::functions::create_layout(ctx); - let description = crate::draw::font::family_description(family, size); + let layout = functions::create_layout(ctx); + let description = font::family_description(family, size); layout.set_font_description(Some(&description)); layout.set_text(text); layout.set_ellipsize(pango::EllipsizeMode::End); @@ -372,8 +376,8 @@ fn bounded_layout( /// Natural width of `text`, for callers dividing a row between two labels. fn text_width(ctx: &cairo::Context, family: &str, size: f64, text: &str) -> f64 { - let layout = pangocairo::functions::create_layout(ctx); - let description = crate::draw::font::family_description(family, size); + let layout = functions::create_layout(ctx); + let description = font::family_description(family, size); layout.set_font_description(Some(&description)); layout.set_text(text); f64::from(layout.extents().1.width()) / f64::from(pango::SCALE) @@ -394,7 +398,7 @@ fn draw_text( let layout = bounded_layout(ctx, family, size, max_width, text); let baseline = f64::from(layout.baseline()) / f64::from(pango::SCALE); ctx.move_to(x, y - baseline); - pangocairo::functions::show_layout(ctx, &layout); + functions::show_layout(ctx, &layout); } fn draw_text_right( @@ -413,7 +417,7 @@ fn draw_text_right( let width = (f64::from(layout.extents().1.width()) / f64::from(pango::SCALE)).min(max_width); let baseline = f64::from(layout.baseline()) / f64::from(pango::SCALE); ctx.move_to(right - width, y - baseline); - pangocairo::functions::show_layout(ctx, &layout); + functions::show_layout(ctx, &layout); } #[cfg(test)] diff --git a/src/ui/help_overlay/content.rs b/src/ui/help_overlay/content.rs index 0d30c68ff..a0b0559c9 100644 --- a/src/ui/help_overlay/content.rs +++ b/src/ui/help_overlay/content.rs @@ -1,36 +1,26 @@ +use std::collections::hash_map::DefaultHasher; + use crate::config::{QuickColorPalette, RadialMenuMouseBinding}; use crate::input::InputState; use std::hash::{Hash, Hasher}; -use super::sections::{HelpOverlayBindings, SectionSets, build_section_sets}; +use super::sections::{HelpContentOptions, HelpOverlayBindings, SectionSets, build_section_sets}; #[derive(Clone, PartialEq)] struct HelpContentKey { shortcut_revision: u64, radial_menu_mouse_binding: RadialMenuMouseBinding, quick_colors: QuickColorPalette, - frozen_enabled: bool, - context_filter: bool, - board_enabled: bool, - capture_enabled: bool, + options: HelpContentOptions, } impl HelpContentKey { - fn from_input( - input: &InputState, - frozen_enabled: bool, - context_filter: bool, - board_enabled: bool, - capture_enabled: bool, - ) -> Self { + fn from_input(input: &InputState, options: HelpContentOptions) -> Self { Self { shortcut_revision: input.keymap_revision(), radial_menu_mouse_binding: input.radial_menu.mouse_binding(), quick_colors: input.style.quick_colors.clone(), - frozen_enabled, - context_filter, - board_enabled, - capture_enabled, + options, } } } @@ -44,27 +34,15 @@ pub(crate) struct HelpContentSnapshot { impl HelpContentSnapshot { pub(super) fn from_bindings( bindings: &HelpOverlayBindings, - frozen_enabled: bool, - context_filter: bool, - board_enabled: bool, - capture_enabled: bool, + options: HelpContentOptions, ) -> Self { - let mut hasher = std::collections::hash_map::DefaultHasher::new(); + let mut hasher = DefaultHasher::new(); bindings.cache_key().hash(&mut hasher); - frozen_enabled.hash(&mut hasher); - context_filter.hash(&mut hasher); - board_enabled.hash(&mut hasher); - capture_enabled.hash(&mut hasher); + options.hash(&mut hasher); Self { revision: hasher.finish(), bindings: bindings.clone(), - sections: build_section_sets( - bindings, - frozen_enabled, - context_filter, - board_enabled, - capture_enabled, - ), + sections: build_section_sets(bindings, options), } } } @@ -88,28 +66,13 @@ impl HelpContentCache { pub(crate) fn get_or_build( &mut self, input: &InputState, - frozen_enabled: bool, - context_filter: bool, - board_enabled: bool, - capture_enabled: bool, + options: HelpContentOptions, ) -> &HelpContentSnapshot { - let key = HelpContentKey::from_input( - input, - frozen_enabled, - context_filter, - board_enabled, - capture_enabled, - ); + let key = HelpContentKey::from_input(input, options); let rebuild = self.entry.as_ref().is_none_or(|entry| entry.key != key); if rebuild { let bindings = HelpOverlayBindings::from_input_state(input); - let sections = build_section_sets( - &bindings, - frozen_enabled, - context_filter, - board_enabled, - capture_enabled, - ); + let sections = build_section_sets(&bindings, options); self.next_revision = self.next_revision.wrapping_add(1); self.entry = Some(CachedHelpContent { key, @@ -144,11 +107,14 @@ mod tests { let mut cache = HelpContentCache::default(); let mut input = make_test_input_state(); - let first = cache.get_or_build(&input, true, true, false, true).revision; - assert_eq!( - cache.get_or_build(&input, true, true, false, true).revision, - first - ); + let single_board = HelpContentOptions { + frozen_enabled: true, + context_filter: true, + board_enabled: false, + capture_enabled: true, + }; + let first = cache.get_or_build(&input, single_board).revision; + assert_eq!(cache.get_or_build(&input, single_board).revision, first); assert_eq!(cache.builds, 1); input.help_overlay.open(false); @@ -156,7 +122,7 @@ mod tests { input.help_overlay.insert_search("zoom"); input.help_overlay.scroll_by(12.0); assert_eq!( - cache.get_or_build(&input, true, true, false, true).revision, + cache.get_or_build(&input, single_board).revision, first, "page, search, and scroll are display state, not content inputs" ); @@ -166,11 +132,17 @@ mod tests { Action::ToggleHelp, vec![Shortcut::parse("F10").unwrap()], )])); - let rebound = cache.get_or_build(&input, true, true, false, true).revision; + let rebound = cache.get_or_build(&input, single_board).revision; assert_ne!(rebound, first); assert_eq!(cache.builds, 2); - let capability_content = cache.get_or_build(&input, true, true, true, true); + let capability_content = cache.get_or_build( + &input, + HelpContentOptions { + board_enabled: true, + ..single_board + }, + ); let capability_change = capability_content.revision; assert!( capability_content diff --git a/src/ui/help_overlay/grid.rs b/src/ui/help_overlay/grid.rs index 3f1382f24..91b300510 100644 --- a/src/ui/help_overlay/grid.rs +++ b/src/ui/help_overlay/grid.rs @@ -1,3 +1,5 @@ +use crate::ui_text::UiTextEngine; + use super::super::primitives::{draw_rounded_rect, text_extents_for_with_engine}; use super::keycaps::{KeyComboStyle, draw_key_combo, draw_key_combo_highlight, measure_key_combo}; use super::layout::GridLayout; @@ -21,8 +23,8 @@ const LEADER_DOT_PITCH: f64 = 4.0; /// the font size. const LABEL_CAP_CENTER: f64 = 0.36; -pub(crate) struct GridStyle<'a> { - pub(crate) help_font_family: &'a str, +pub(crate) struct GridStyle<'font> { + pub(crate) help_font_family: &'font str, pub(crate) body_font_size: f64, pub(crate) key_font_size: f64, pub(crate) heading_font_size: f64, @@ -99,14 +101,14 @@ struct CardPlacement { } /// Borrowed paint inputs shared by every card, row, and badge of one grid. -struct GridPainter<'a> { - engine: &'a crate::ui_text::UiTextEngine, - ctx: &'a cairo::Context, - style: &'a GridStyle<'a>, - colors: &'a GridColors, - key_combo_style: &'a KeyComboStyle<'a>, +struct GridPainter<'render> { + engine: &'render UiTextEngine, + ctx: &'render cairo::Context, + style: &'render GridStyle<'render>, + colors: &'render GridColors, + key_combo_style: &'render KeyComboStyle<'render>, /// Lowercase search query while search is active. - search: Option<&'a str>, + search: Option<&'render str>, /// Vertical band of the visible grid; hit rects are clipped to it so a /// partially scrolled row never reports a click outside the view. clip: (f64, f64), @@ -382,7 +384,7 @@ impl GridPainter<'_> { #[allow(clippy::too_many_arguments)] pub(crate) fn draw_sections_grid( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, grid: &GridLayout, grid_start_y: f64, diff --git a/src/ui/help_overlay/keycaps.rs b/src/ui/help_overlay/keycaps.rs index 2ec9fc979..9146f6bed 100644 --- a/src/ui/help_overlay/keycaps.rs +++ b/src/ui/help_overlay/keycaps.rs @@ -1,3 +1,5 @@ +use crate::ui_text::UiTextEngine; + use super::super::primitives::{ draw_rounded_rect, keycap_size_with_engine, text_extents_for_with_engine, }; @@ -5,8 +7,8 @@ use crate::ui::primitives::draw_keycap_with_engine; use crate::ui::theme::toolbar; use crate::ui_text::UiTextStyle; -pub(crate) struct KeyComboStyle<'a> { - pub(crate) font_family: &'a str, +pub(crate) struct KeyComboStyle<'font> { + pub(crate) font_family: &'font str, pub(crate) font_size: f64, pub(crate) text_color: [f64; 4], pub(crate) separator_color: [f64; 4], @@ -41,7 +43,7 @@ fn for_each_key_token(combo: &str, mut emit: impl FnMut(&str)) { /// used (`baseline - font_size / 2`), so replacing the cap did not shift the /// rows or the highlight geometry in [`draw_key_combo_highlight`]. fn draw_single_keycap( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, x: f64, baseline: f64, @@ -66,7 +68,7 @@ fn draw_single_keycap( /// Measure the width of a key combination string with keycap styling pub(crate) fn measure_key_combo( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, key_str: &str, font_family: &str, @@ -130,7 +132,7 @@ pub(crate) fn measure_key_combo( /// Draw a key combination string with keycap styling, returns total width pub(crate) fn draw_key_combo( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, x: f64, baseline: f64, diff --git a/src/ui/help_overlay/layout.rs b/src/ui/help_overlay/layout.rs index 89073ed0a..1e58f019b 100644 --- a/src/ui/help_overlay/layout.rs +++ b/src/ui/help_overlay/layout.rs @@ -1,3 +1,5 @@ +use crate::ui_text::UiTextEngine; + use super::super::primitives::text_extents_for_with_engine; use super::grid::GridStyle; use super::keycaps::measure_key_combo; @@ -15,7 +17,7 @@ pub(crate) struct GridLayout { /// Measure each section card. Rows read "label … keys": the label column is /// as wide as the widest label, and the key chips start one gap past it. pub(crate) fn measure_sections( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, sections: Vec
, style: &GridStyle<'_>, diff --git a/src/ui/help_overlay/mod.rs b/src/ui/help_overlay/mod.rs index cb1fdace5..a7c8fef65 100644 --- a/src/ui/help_overlay/mod.rs +++ b/src/ui/help_overlay/mod.rs @@ -13,7 +13,7 @@ pub use render::{ HelpHitMap, HelpOverlayRegion, HelpRenderResult, render_help_overlay, render_help_overlay_result, }; -pub use sections::HelpOverlayBindings; +pub use sections::{HelpContentOptions, HelpOverlayBindings}; pub(crate) use content::HelpContentCache; pub(in crate::ui) use render::HelpLayoutCache; diff --git a/src/ui/help_overlay/nav/render.rs b/src/ui/help_overlay/nav/render.rs index ba9701b93..32c09a0ff 100644 --- a/src/ui/help_overlay/nav/render.rs +++ b/src/ui/help_overlay/nav/render.rs @@ -1,7 +1,7 @@ use crate::i18n::tr; use crate::ui::primitives::draw_rounded_rect; use crate::ui::theme::{self, Rgba}; -use crate::ui_text::UiTextStyle; +use crate::ui_text::{UiTextEngine, UiTextStyle}; use super::super::search::{draw_segmented_text, ellipsize_to_fit_with_engine}; use super::NavState; @@ -10,8 +10,8 @@ use super::NavState; /// shadow token, but semantically an input well, so it stays file-local. const SEARCH_BOX_BG: Rgba = (0.0, 0.0, 0.0, 0.3); -pub(crate) struct NavDrawStyle<'a> { - pub(crate) font_family: &'a str, +pub(crate) struct NavDrawStyle<'font> { + pub(crate) font_family: &'font str, pub(crate) subtitle_color: [f64; 4], pub(crate) search_color: [f64; 4], pub(crate) nav_line_gap: f64, @@ -29,7 +29,7 @@ pub(crate) struct NavRender { } pub(crate) fn draw_nav( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, inner_x: f64, mut cursor_y: f64, diff --git a/src/ui/help_overlay/nav/state.rs b/src/ui/help_overlay/nav/state.rs index d52b49373..2e5a95bc5 100644 --- a/src/ui/help_overlay/nav/state.rs +++ b/src/ui/help_overlay/nav/state.rs @@ -1,5 +1,8 @@ +use std::fmt::Display; + use crate::i18n::{tr, trf}; use crate::ui::primitives::text_extents_for_with_engine; +use crate::ui_text::UiTextEngine; use super::super::search::ellipsize_to_fit_with_engine; @@ -19,7 +22,7 @@ pub(crate) struct NavState { #[allow(clippy::too_many_arguments)] pub(crate) fn build_nav_state( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, help_font_family: &str, nav_title: &str, @@ -40,7 +43,7 @@ pub(crate) fn build_nav_state( trf( "{} {} Page {}/{}", &[ - &nav_title as &dyn std::fmt::Display, + &nav_title as &dyn Display, &BULLET, &(page_index + 1), &page_count, @@ -54,7 +57,7 @@ pub(crate) fn build_nav_state( vec![ ("Esc".to_string(), nav_key_color), (tr(": Close").to_string(), subtitle_color), - (nav_separator.clone(), subtitle_color), + (nav_separator, subtitle_color), ("Backspace".to_string(), nav_key_color), (tr(": Remove").to_string(), subtitle_color), ] diff --git a/src/ui/help_overlay/render/cache.rs b/src/ui/help_overlay/render/cache.rs index f3b4a3aa1..f82ee38b3 100644 --- a/src/ui/help_overlay/render/cache.rs +++ b/src/ui/help_overlay/render/cache.rs @@ -1,3 +1,6 @@ +use crate::config::HelpOverlayStyle; +use crate::ui_text::UiTextEngine; + use super::super::content::HelpContentSnapshot; use super::state::{OverlayLayout, build_overlay_layout}; @@ -16,7 +19,7 @@ struct StyleKey { } impl StyleKey { - fn from_style(style: &crate::config::HelpOverlayStyle) -> Self { + fn from_style(style: &HelpOverlayStyle) -> Self { fn to_hundredths(v: f64) -> i32 { (v * 100.0).round() as i32 } @@ -77,9 +80,9 @@ impl HelpLayoutCache { #[allow(clippy::too_many_arguments)] pub(super) fn get_or_build_overlay_layout( &mut self, - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, - style: &crate::config::HelpOverlayStyle, + style: &HelpOverlayStyle, screen_width: u32, screen_height: u32, page_index: usize, diff --git a/src/ui/help_overlay/render/entry.rs b/src/ui/help_overlay/render/entry.rs index e2d88d445..4379e00cf 100644 --- a/src/ui/help_overlay/render/entry.rs +++ b/src/ui/help_overlay/render/entry.rs @@ -1,21 +1,30 @@ -use super::{HelpOverlayBindings, HelpRenderResult, render_help_overlay_result_with_context}; +use crate::config::HelpOverlayStyle; +use crate::ui::UiRenderCaches; +use crate::ui::theme::Theme; +use crate::ui_text::UiTextEngine; + +use super::{ + HelpContentOptions, HelpOverlayBindings, HelpRenderResult, + render_help_overlay_result_with_context, +}; /// Render help overlay showing all keybindings with call-local paint resources. /// Returns the maximum scroll offset and discards interaction geometry. /// Interactive callers should use [`render_help_overlay_result`] instead. #[allow(clippy::too_many_arguments)] +#[expect( + clippy::must_use_candidate, + reason = "painting is the purpose; drawing-only callers may ignore the scroll range" +)] pub fn render_help_overlay( ctx: &cairo::Context, - style: &crate::config::HelpOverlayStyle, + style: &HelpOverlayStyle, screen_width: u32, screen_height: u32, - frozen_enabled: bool, + options: HelpContentOptions, page_index: usize, bindings: &HelpOverlayBindings, search_query: &str, - context_filter: bool, - board_enabled: bool, - capture_enabled: bool, scroll_offset: f64, quick_mode: bool, ) -> f64 { @@ -24,13 +33,10 @@ pub fn render_help_overlay( style, screen_width, screen_height, - frozen_enabled, + options, page_index, bindings, search_query, - context_filter, - board_enabled, - capture_enabled, scroll_offset, quick_mode, ) @@ -45,24 +51,22 @@ pub fn render_help_overlay( /// that input owner's click and cursor queries. Each caller retains its own map; /// painting another overlay cannot replace it. #[allow(clippy::too_many_arguments)] +#[must_use] pub fn render_help_overlay_result( ctx: &cairo::Context, - style: &crate::config::HelpOverlayStyle, + style: &HelpOverlayStyle, screen_width: u32, screen_height: u32, - frozen_enabled: bool, + options: HelpContentOptions, page_index: usize, bindings: &HelpOverlayBindings, search_query: &str, - context_filter: bool, - board_enabled: bool, - capture_enabled: bool, scroll_offset: f64, quick_mode: bool, ) -> HelpRenderResult { - let engine = crate::ui_text::UiTextEngine::default(); - let mut caches = crate::ui::UiRenderCaches::default(); - let theme = crate::ui::theme::Theme::dark(); + let engine = UiTextEngine::default(); + let mut caches = UiRenderCaches::default(); + let theme = Theme::dark(); render_help_overlay_result_with_context( &engine, &mut crate::ui::UiRenderCtx { @@ -73,13 +77,10 @@ pub fn render_help_overlay_result( style, screen_width, screen_height, - frozen_enabled, + options, page_index, bindings, search_query, - context_filter, - board_enabled, - capture_enabled, scroll_offset, quick_mode, false, diff --git a/src/ui/help_overlay/render/footer.rs b/src/ui/help_overlay/render/footer.rs index 5a23e0b30..7fcc08c74 100644 --- a/src/ui/help_overlay/render/footer.rs +++ b/src/ui/help_overlay/render/footer.rs @@ -1,3 +1,7 @@ +use crate::toolbar_icons; +use crate::toolbar_icons::ToolbarIconPainter; +use crate::ui_text::UiTextEngine; + use super::header; use crate::config::{Action, action_label}; use crate::i18n::tr; @@ -34,11 +38,11 @@ pub(super) struct FooterPill { pub(super) target: FooterTarget, pub(super) label: &'static str, pub(super) key_hint: Option<&'static str>, - pub(super) icon: crate::toolbar_icons::ToolbarIconPainter, + pub(super) icon: ToolbarIconPainter, } impl FooterPill { - fn action(action: Action, icon: crate::toolbar_icons::ToolbarIconPainter) -> Self { + fn action(action: Action, icon: ToolbarIconPainter) -> Self { Self { target: FooterTarget::Action(action), label: action_label(action), @@ -52,8 +56,8 @@ impl FooterPill { /// action registry, never a hardcoded string. pub(super) fn footer_pills(show_unbound: bool) -> [FooterPill; 3] { [ - FooterPill::action(Action::ReplayTour, crate::toolbar_icons::draw_icon_refresh), - FooterPill::action(Action::OpenAbout, crate::toolbar_icons::draw_icon_info), + FooterPill::action(Action::ReplayTour, toolbar_icons::draw_icon_refresh), + FooterPill::action(Action::OpenAbout, toolbar_icons::draw_icon_info), FooterPill { target: FooterTarget::ToggleUnbound, label: if show_unbound { @@ -62,7 +66,7 @@ pub(super) fn footer_pills(show_unbound: bool) -> [FooterPill; 3] { tr("Show Unbound") }, key_hint: Some(TOGGLE_UNBOUND_KEY), - icon: crate::toolbar_icons::draw_icon_visibility, + icon: toolbar_icons::draw_icon_visibility, }, ] } @@ -74,14 +78,14 @@ pub(super) struct FooterHit { } /// Geometry and styling shared by every footer pill. -pub(super) struct FooterPillLayout<'a> { +pub(super) struct FooterPillLayout<'font> { pub(super) inner_x: f64, pub(super) inner_width: f64, pub(super) top_y: f64, pub(super) pill_height: f64, pub(super) font_size: f64, pub(super) key_font_size: f64, - pub(super) font_family: &'a str, + pub(super) font_family: &'font str, pub(super) accent: [f64; 4], pub(super) accent_muted: [f64; 4], } @@ -92,7 +96,7 @@ struct PillWidths { } fn measure_pill( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, font_family: &str, font_size: f64, @@ -127,7 +131,7 @@ fn measure_pill( /// Width the whole footer pill row occupies, so the overlay box can grow to /// fit it. pub(super) fn measure_footer_pills( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, font_family: &str, font_size: f64, @@ -144,9 +148,9 @@ pub(super) fn measure_footer_pills( /// Draw the footer pills as one centred row and return their clickable rects, /// each tagged with what a click should do. pub(super) fn draw_footer_pills( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, - layout: FooterPillLayout<'_>, + layout: &FooterPillLayout<'_>, pills: &[FooterPill], ) -> Vec { let icon_size = layout.font_size; diff --git a/src/ui/help_overlay/render/header.rs b/src/ui/help_overlay/render/header.rs index 381dd3dcb..a5f0d2e00 100644 --- a/src/ui/help_overlay/render/header.rs +++ b/src/ui/help_overlay/render/header.rs @@ -3,6 +3,8 @@ //! The hint line mirrors the keycap styling used throughout the grid rows so the //! header reads as part of the same visual system instead of flat plain text. +use crate::ui_text::UiTextEngine; + use super::super::super::primitives::{draw_rounded_rect, text_extents_for_with_engine}; use super::super::keycaps::{KeyComboStyle, draw_key_combo, measure_key_combo}; use crate::ui_text::UiTextStyle; @@ -25,18 +27,18 @@ pub(super) const PILL_RADIUS: f64 = 6.0; const TITLE_PILL_GAP: f64 = 12.0; /// A single "keys → action" hint shown in the header. -pub(super) struct HeaderHint<'a> { - pub(super) keys: &'a str, - pub(super) label: &'a str, +pub(super) struct HeaderHint<'text> { + pub(super) keys: &'text str, + pub(super) label: &'text str, } /// Everything the header needs beyond the title itself. -pub(super) struct HeaderContent<'a> { +pub(super) struct HeaderContent<'text> { /// Short version badge text, e.g. `v0.9.21`. - pub(super) version: &'a str, + pub(super) version: &'text str, /// Optional leading phrase drawn before the hints (quick-reference mode). - pub(super) intro: Option<&'a str>, - pub(super) hints: &'a [HeaderHint<'a>], + pub(super) intro: Option<&'text str>, + pub(super) hints: &'text [HeaderHint<'text>], } fn normal_style(font_family: &str, font_size: f64) -> UiTextStyle<'_> { @@ -50,7 +52,7 @@ fn normal_style(font_family: &str, font_size: f64) -> UiTextStyle<'_> { /// Measure the total width the hint line occupies (intro + hints + separators). pub(super) fn measure_hints( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, font_family: &str, font_size: f64, @@ -92,7 +94,7 @@ pub(super) fn measure_hints( /// Draw the hint line starting at `x`, on `baseline`. Returns the width drawn. #[allow(clippy::too_many_arguments)] pub(super) fn draw_hints( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, x: f64, baseline: f64, @@ -157,7 +159,7 @@ pub(super) fn draw_hints( /// Width of the version pill (rounded chip) for the given text. pub(super) fn measure_version_pill( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, font_family: &str, font_size: f64, @@ -178,7 +180,7 @@ pub(super) fn measure_version_pill( /// Extra width the title row needs so the title and version pill never overlap. pub(super) fn title_row_width( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, font_family: &str, title_font_size: f64, @@ -204,7 +206,7 @@ pub(super) fn title_row_width( /// centred against a title whose baseline is `title_baseline`. #[allow(clippy::too_many_arguments)] pub(super) fn draw_version_pill( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, right_edge: f64, title_baseline: f64, diff --git a/src/ui/help_overlay/render/mod.rs b/src/ui/help_overlay/render/mod.rs index d6669c267..b6215b069 100644 --- a/src/ui/help_overlay/render/mod.rs +++ b/src/ui/help_overlay/render/mod.rs @@ -2,7 +2,7 @@ use super::content::HelpContentSnapshot; use super::grid::{GridColors, draw_sections_grid}; use super::keycaps::KeyComboStyle; use super::nav::{NavDrawStyle, draw_nav}; -use super::sections::HelpOverlayBindings; +use super::sections::{HelpContentOptions, HelpOverlayBindings}; mod cache; mod entry; @@ -24,35 +24,30 @@ use footer::{FooterPillLayout, FooterTarget, draw_footer_pills, footer_pills}; use frame::draw_overlay_frame; use header::{HeaderContent, HeaderHint, draw_hints, draw_version_pill}; +use crate::build_info; +use crate::config::HelpOverlayStyle; pub use crate::help_overlay_interaction::{HelpHitMap, HelpOverlayRegion, HelpRenderResult}; +use crate::ui::UiRenderCtx; +use crate::ui_text::UiTextEngine; const BULLET: &str = "\u{2022}"; #[allow(clippy::too_many_arguments)] pub(crate) fn render_help_overlay_result_with_context( - engine: &crate::ui_text::UiTextEngine, - render: &mut crate::ui::UiRenderCtx<'_, '_, '_>, - style: &crate::config::HelpOverlayStyle, + engine: &UiTextEngine, + render: &mut UiRenderCtx<'_, '_, '_>, + style: &HelpOverlayStyle, screen_width: u32, screen_height: u32, - frozen_enabled: bool, + options: HelpContentOptions, page_index: usize, bindings: &HelpOverlayBindings, search_query: &str, - context_filter: bool, - board_enabled: bool, - capture_enabled: bool, scroll_offset: f64, quick_mode: bool, show_unbound: bool, ) -> HelpRenderResult { - let content = HelpContentSnapshot::from_bindings( - bindings, - frozen_enabled, - context_filter, - board_enabled, - capture_enabled, - ); + let content = HelpContentSnapshot::from_bindings(bindings, options); render_help_overlay_result_with_content( engine, render, @@ -70,9 +65,9 @@ pub(crate) fn render_help_overlay_result_with_context( #[allow(clippy::too_many_arguments)] pub(crate) fn render_help_overlay_result_with_content( - engine: &crate::ui_text::UiTextEngine, - render: &mut crate::ui::UiRenderCtx<'_, '_, '_>, - style: &crate::config::HelpOverlayStyle, + engine: &UiTextEngine, + render: &mut UiRenderCtx<'_, '_, '_>, + style: &HelpOverlayStyle, screen_width: u32, screen_height: u32, page_index: usize, @@ -99,7 +94,7 @@ pub(crate) fn render_help_overlay_result_with_content( .and_then(|labels| labels.first()) .map(|label| label.as_str()) .unwrap_or(NOT_BOUND_LABEL); - let version_text = format!("v{}", crate::build_info::version()); + let version_text = format!("v{}", build_info::version()); let header_hints = if quick_mode { vec![HeaderHint { keys: palette_binding, @@ -349,7 +344,7 @@ pub(crate) fn render_help_overlay_result_with_content( let footer_hits = draw_footer_pills( engine, ctx, - FooterPillLayout { + &FooterPillLayout { inner_x, inner_width, top_y: cursor_y, diff --git a/src/ui/help_overlay/render/state.rs b/src/ui/help_overlay/render/state.rs index 7ee408dad..d8ecdcbf7 100644 --- a/src/ui/help_overlay/render/state.rs +++ b/src/ui/help_overlay/render/state.rs @@ -1,4 +1,6 @@ +use crate::config::HelpOverlayStyle; use crate::i18n::{tr, trf}; +use crate::ui_text::UiTextEngine; use super::super::super::primitives::text_extents_for_with_engine; use super::super::content::HelpContentSnapshot; @@ -36,9 +38,9 @@ pub(super) struct OverlayLayout { #[allow(clippy::too_many_arguments)] pub(super) fn build_overlay_layout( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, - style: &crate::config::HelpOverlayStyle, + style: &HelpOverlayStyle, screen_width: u32, screen_height: u32, page_index: usize, diff --git a/src/ui/help_overlay/render/tests/cache.rs b/src/ui/help_overlay/render/tests/cache.rs index 0e0f9b7d7..8b950c525 100644 --- a/src/ui/help_overlay/render/tests/cache.rs +++ b/src/ui/help_overlay/render/tests/cache.rs @@ -1,18 +1,20 @@ +use crate::help_overlay_interaction::HelpRenderResult; +use crate::input::state::test_support; +use crate::ui::UiRenderCaches; +use crate::ui::theme::Theme; + use super::*; use crate::config::HelpOverlayStyle; -use crate::ui::HelpOverlayBindings; +use crate::ui::{HelpContentOptions, HelpOverlayBindings}; struct Inputs { style: HelpOverlayStyle, width: u32, height: u32, - frozen: bool, + content: HelpContentOptions, page: usize, bindings: HelpOverlayBindings, query: String, - context_filter: bool, - board: bool, - capture: bool, quick: bool, show_unbound: bool, } @@ -23,13 +25,15 @@ impl Default for Inputs { style: HelpOverlayStyle::default(), width: 800, height: 400, - frozen: true, + content: HelpContentOptions { + frozen_enabled: true, + context_filter: false, + board_enabled: true, + capture_enabled: true, + }, page: 0, bindings: HelpOverlayBindings::default(), query: String::new(), - context_filter: false, - board: true, - capture: true, quick: false, show_unbound: false, } @@ -40,19 +44,13 @@ fn layout(cache: &mut HelpLayoutCache, inputs: &Inputs, scroll: f64) -> OverlayL let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 1, 1).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); cache.get_or_build_overlay_layout( - &crate::ui_text::UiTextEngine::default(), + &UiTextEngine::default(), &ctx, &inputs.style, inputs.width, inputs.height, inputs.page, - &HelpContentSnapshot::from_bindings( - &inputs.bindings, - inputs.frozen, - inputs.context_filter, - inputs.board, - inputs.capture, - ), + &HelpContentSnapshot::from_bindings(&inputs.bindings, inputs.content), &inputs.query, scroll, "Wayscriber Controls", @@ -85,15 +83,15 @@ fn scrolling_clamps_a_returned_clone_without_rebuilding_or_mutating_the_entry() assert_eq!(cache.entry.as_ref().unwrap().layout.scroll_offset, 0.0); } -fn assert_rebuilds_from_baseline(changed: Inputs, label: &str) { +fn assert_rebuilds_from_baseline(changed: &Inputs, label: &str) { let baseline = Inputs::default(); let mut cache = HelpLayoutCache::default(); layout(&mut cache, &baseline, 0.0); - let changed_layout = layout(&mut cache, &changed, 0.0); + let changed_layout = layout(&mut cache, changed, 0.0); assert_eq!(cache.builds, 2, "{label} must invalidate independently"); - layout(&mut cache, &changed, 1.0); + layout(&mut cache, changed, 1.0); assert_eq!(cache.builds, 2, "{label} replacement must be reusable"); - let fresh = layout(&mut HelpLayoutCache::default(), &changed, 0.0); + let fresh = layout(&mut HelpLayoutCache::default(), changed, 0.0); assert_eq!(changed_layout.box_width, fresh.box_width, "{label}"); assert_eq!(changed_layout.box_height, fresh.box_height, "{label}"); assert_eq!(changed_layout.scroll_max, fresh.scroll_max, "{label}"); @@ -109,17 +107,17 @@ fn every_layout_key_dimension_invalidates_independently() { let changes: &[Change] = &[ ("width", |v| v.width += 1), ("height", |v| v.height += 1), - ("frozen", |v| v.frozen = false), + ("frozen", |v| v.content.frozen_enabled = false), ("page", |v| v.page = 1), ("bindings", |v| { - let input = crate::input::state::test_support::make_test_input_state(); + let input = test_support::make_test_input_state(); v.bindings = HelpOverlayBindings::from_input_state(&input); }), ("query", |v| v.query = "draw".into()), ("raw whitespace query", |v| v.query = " ".into()), - ("context filter", |v| v.context_filter = true), - ("board", |v| v.board = false), - ("capture", |v| v.capture = false), + ("context filter", |v| v.content.context_filter = true), + ("board", |v| v.content.board_enabled = false), + ("capture", |v| v.content.capture_enabled = false), ("quick", |v| v.quick = true), ("show unbound", |v| v.show_unbound = true), ("font size", |v| v.style.font_size += 1.0), @@ -131,7 +129,7 @@ fn every_layout_key_dimension_invalidates_independently() { for (label, change) in changes { let mut changed = Inputs::default(); change(&mut changed); - assert_rebuilds_from_baseline(changed, label); + assert_rebuilds_from_baseline(&changed, label); } for channel in 0..4 { for color in 0..3 { @@ -142,7 +140,7 @@ fn every_layout_key_dimension_invalidates_independently() { _ => &mut changed.style.text_color, }; channels[channel] += 0.1; - assert_rebuilds_from_baseline(changed, &format!("color {color}, channel {channel}")); + assert_rebuilds_from_baseline(&changed, &format!("color {color}, channel {channel}")); } } } @@ -162,11 +160,11 @@ fn style_quantization_retains_existing_hundredths_policy() { } fn paint( - engine: &crate::ui_text::UiTextEngine, - caches: &mut crate::ui::UiRenderCaches, + engine: &UiTextEngine, + caches: &mut UiRenderCaches, inputs: &Inputs, scroll: f64, -) -> (Vec, crate::help_overlay_interaction::HelpRenderResult) { +) -> (Vec, HelpRenderResult) { let mut surface = cairo::ImageSurface::create( cairo::Format::ARgb32, inputs.width as i32, @@ -176,7 +174,7 @@ fn paint( let extent; { let cairo = cairo::Context::new(&surface).unwrap(); - let theme = crate::ui::theme::Theme::dark(); + let theme = Theme::dark(); let mut render = crate::ui::UiRenderCtx { cairo: &cairo, theme: &theme, @@ -188,13 +186,10 @@ fn paint( &inputs.style, inputs.width, inputs.height, - inputs.frozen, + inputs.content, inputs.page, &inputs.bindings, &inputs.query, - inputs.context_filter, - inputs.board, - inputs.capture, scroll, inputs.quick, inputs.show_unbound, @@ -205,8 +200,8 @@ fn paint( } fn assert_frame_matches( - actual: &(Vec, crate::help_overlay_interaction::HelpRenderResult), - expected: &(Vec, crate::help_overlay_interaction::HelpRenderResult), + actual: &(Vec, HelpRenderResult), + expected: &(Vec, HelpRenderResult), ) { assert_eq!( actual.1, expected.1, @@ -221,10 +216,10 @@ fn assert_frame_matches( #[test] fn owners_keep_independent_layouts_and_reused_rendering_matches_fresh_pixels() { - let engine = crate::ui_text::UiTextEngine::default(); - let second_engine = crate::ui_text::UiTextEngine::default(); - let mut caches = crate::ui::UiRenderCaches::default(); - let mut other_caches = crate::ui::UiRenderCaches::default(); + let engine = UiTextEngine::default(); + let second_engine = UiTextEngine::default(); + let mut caches = UiRenderCaches::default(); + let mut other_caches = UiRenderCaches::default(); let inputs = Inputs::default(); let initial = paint(&engine, &mut caches, &inputs, 0.0); assert!(initial.0.iter().any(|&byte| byte != 0)); @@ -269,8 +264,8 @@ fn owners_keep_independent_layouts_and_reused_rendering_matches_fresh_pixels() { for scroll in [0.0, 40.0] { let actual = paint(&engine, &mut caches, &input, scroll); let expected = paint( - &crate::ui_text::UiTextEngine::default(), - &mut crate::ui::UiRenderCaches::default(), + &UiTextEngine::default(), + &mut UiRenderCaches::default(), &input, scroll, ); diff --git a/src/ui/help_overlay/render/tests/layout.rs b/src/ui/help_overlay/render/tests/layout.rs index 19e82ea35..8249a8515 100644 --- a/src/ui/help_overlay/render/tests/layout.rs +++ b/src/ui/help_overlay/render/tests/layout.rs @@ -1,21 +1,32 @@ +use crate::input::state::test_support; +use crate::ui::primitives; + use super::*; use crate::config::{Action, HelpOverlayStyle, action_label}; use crate::label_format::NOT_BOUND_LABEL; -use crate::ui::HelpOverlayBindings; +use crate::ui::{HelpContentOptions, HelpOverlayBindings}; fn build(query: &str, page: usize, show_unbound: bool, width: u32, height: u32) -> OverlayLayout { let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 1, 1).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); - let input = crate::input::state::test_support::make_test_input_state(); + let input = test_support::make_test_input_state(); let bindings = HelpOverlayBindings::from_input_state(&input); build_overlay_layout( - &crate::ui_text::UiTextEngine::default(), + &UiTextEngine::default(), &ctx, &HelpOverlayStyle::default(), width, height, page, - &HelpContentSnapshot::from_bindings(&bindings, true, false, true, true), + &HelpContentSnapshot::from_bindings( + &bindings, + HelpContentOptions { + frozen_enabled: true, + context_filter: false, + board_enabled: true, + capture_enabled: true, + }, + ), query, 0.0, "Wayscriber Controls", @@ -82,7 +93,7 @@ fn key_column_starts_after_the_widest_label() { let metrics = layout.metrics; let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 1, 1).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); - let engine = crate::ui_text::UiTextEngine::default(); + let engine = UiTextEngine::default(); for measured in layout.grid.rows.iter().flatten() { let widest_label = measured @@ -90,7 +101,7 @@ fn key_column_starts_after_the_widest_label() { .rows .iter() .map(|row| { - crate::ui::primitives::text_extents_for_with_engine( + primitives::text_extents_for_with_engine( &engine, &ctx, &layout.help_font_family, diff --git a/src/ui/help_overlay/search.rs b/src/ui/help_overlay/search.rs index e886f78b3..a9dd0ba3b 100644 --- a/src/ui/help_overlay/search.rs +++ b/src/ui/help_overlay/search.rs @@ -13,7 +13,9 @@ pub(crate) use crate::ui::text_highlight::{ }; // Measured trimming lives with the other shared text primitives so surfaces // outside this overlay (the color picker's title) use the same implementation. +use crate::config; pub(crate) use crate::ui::primitives::ellipsize_to_fit_with_engine; +use crate::ui_text::UiTextEngine; /// Fuzzy row match: every query token must fuzzy-match the shortcut string /// (`key`), the visible action description, or — for rows that carry an action @@ -30,7 +32,7 @@ pub(crate) fn row_matches(row: &Row, needle_lower: &str) -> bool { if tokens.is_empty() { return false; } - let meta = row.action_id.and_then(crate::config::action_meta); + let meta = row.action_id.and_then(config::action_meta); let key_names = canonical_key_names(&row.key); tokens.iter().all(|token| { fuzzy_score(token, &row.key) > 0 @@ -53,7 +55,7 @@ pub(crate) fn title_matches(title: &str, needle_lower: &str) -> bool { #[allow(clippy::too_many_arguments)] pub(crate) fn draw_segmented_text( - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, ctx: &cairo::Context, x: f64, baseline: f64, diff --git a/src/ui/help_overlay/sections/bindings.rs b/src/ui/help_overlay/sections/bindings.rs index b048aca4a..36a20cba7 100644 --- a/src/ui/help_overlay/sections/bindings.rs +++ b/src/ui/help_overlay/sections/bindings.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet}; +use crate::config; use crate::config::{ Action, QuickColorPalette, QuickColorSlot, RadialMenuMouseBinding, action_meta_iter, }; @@ -29,7 +30,8 @@ impl Default for HelpOverlayBindings { impl HelpOverlayBindings { pub fn from_input_state(state: &InputState) -> Self { let mut labels = HashMap::new(); - for meta in action_meta_iter().filter(|meta| meta.in_help) { + let help_actions = action_meta_iter().filter(|meta| meta.in_help); + for meta in help_actions { let bindings = state.action_binding_labels(meta.action); if !bindings.is_empty() { labels.insert(meta.action, bindings); @@ -37,7 +39,8 @@ impl HelpOverlayBindings { } let mut cache_parts = Vec::new(); - for meta in action_meta_iter().filter(|meta| meta.in_help) { + let help_actions = action_meta_iter().filter(|meta| meta.in_help); + for meta in help_actions { if let Some(values) = labels.get(&meta.action) { cache_parts.push(format!("{:?}={}", meta.action, values.join("/"))); } @@ -152,7 +155,7 @@ pub(super) fn action_row( ) -> super::super::types::Row { super::super::types::row( binding_or_fallback(bindings, action, fallback), - crate::config::action_label(action), + config::action_label(action), ) .with_action(action) } @@ -219,6 +222,8 @@ fn split_numeric_suffix(label: &str) -> Option<(String, u32)> { #[cfg(test)] mod tests { + use crate::draw::color; + use super::*; use crate::config::{Action, QuickColorPaletteEntry}; use crate::draw::Color; @@ -298,7 +303,7 @@ mod tests { state.set_quick_colors(QuickColorPalette::from_entries(vec![ QuickColorPaletteEntry { label: "Red".to_string(), - color: crate::draw::color::RED, + color: color::RED, }, QuickColorPaletteEntry { label: "Custom green".to_string(), diff --git a/src/ui/help_overlay/sections/builder.rs b/src/ui/help_overlay/sections/builder.rs index 2e4bc9ad5..e961c159d 100644 --- a/src/ui/help_overlay/sections/builder.rs +++ b/src/ui/help_overlay/sections/builder.rs @@ -17,20 +17,24 @@ pub(crate) struct SectionSets { pub(crate) quick: Vec
, } +/// Runtime capabilities and filtering that decide which help rows exist. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +pub struct HelpContentOptions { + /// Frozen mode is available, so its toggle is listed. + pub frozen_enabled: bool, + /// Hide sections for features this runtime does not offer. + pub context_filter: bool, + /// Board shortcuts apply because more than one board exists. + pub board_enabled: bool, + /// Screen capture is enabled, so screenshot shortcuts apply. + pub capture_enabled: bool, +} + pub(crate) fn build_section_sets( bindings: &HelpOverlayBindings, - frozen_enabled: bool, - context_filter: bool, - board_enabled: bool, - capture_enabled: bool, + options: HelpContentOptions, ) -> SectionSets { - let mut sections = build_main_sections( - bindings, - frozen_enabled, - context_filter, - board_enabled, - capture_enabled, - ); + let mut sections = build_main_sections(bindings, options); let mut all_sections = vec![ sections.drawing.clone(), diff --git a/src/ui/help_overlay/sections/builder/sections.rs b/src/ui/help_overlay/sections/builder/sections.rs index f691be1bf..478928ef2 100644 --- a/src/ui/help_overlay/sections/builder/sections.rs +++ b/src/ui/help_overlay/sections/builder/sections.rs @@ -8,6 +8,7 @@ use super::super::bindings::{ HelpOverlayBindings, action_row, binding_or_fallback, bindings_compact_or_fallback, bindings_or_fallback, joined_labels, primary_or_fallback, }; +use super::HelpContentOptions; pub(super) struct MainSections { pub(super) board_modes: Option
, @@ -31,11 +32,15 @@ fn color_badge(bindings: &HelpOverlayBindings, index: usize) -> Option { pub(super) fn build_main_sections( bindings: &HelpOverlayBindings, - frozen_enabled: bool, - context_filter: bool, - board_enabled: bool, - capture_enabled: bool, + options: HelpContentOptions, ) -> MainSections { + let HelpContentOptions { + frozen_enabled, + context_filter, + board_enabled, + capture_enabled, + } = options; + let board_modes = (!context_filter || board_enabled).then(|| Section { title: tr("Boards"), rows: vec![ @@ -163,7 +168,7 @@ pub(super) fn build_main_sections( action_row(bindings, Action::IncreaseFontSize, NOT_BOUND_LABEL), action_row(bindings, Action::DecreaseFontSize, NOT_BOUND_LABEL), ], - badges: color_badges.clone(), + badges: color_badges, icon: Some(toolbar_icons::draw_icon_select), }; diff --git a/src/ui/help_overlay/sections/mod.rs b/src/ui/help_overlay/sections/mod.rs index 0642dd4d9..bf9cdcfa8 100644 --- a/src/ui/help_overlay/sections/mod.rs +++ b/src/ui/help_overlay/sections/mod.rs @@ -5,6 +5,7 @@ mod builder; mod tests; pub use bindings::HelpOverlayBindings; +pub use builder::HelpContentOptions; pub(crate) use builder::{ SectionSets, build_section_sets, filter_sections_for_search, hide_unbound_rows, }; diff --git a/src/ui/help_overlay/sections/tests.rs b/src/ui/help_overlay/sections/tests.rs index bc14dc7f7..402b4fc25 100644 --- a/src/ui/help_overlay/sections/tests.rs +++ b/src/ui/help_overlay/sections/tests.rs @@ -1,19 +1,30 @@ +use crate::domain::Language; +use crate::{i18n, label_format, test_env}; + use super::super::types::{Badge, Section, row}; -use super::{HelpOverlayBindings, build_section_sets, hide_unbound_rows}; +use super::{HelpContentOptions, HelpOverlayBindings, build_section_sets, hide_unbound_rows}; use crate::config::{Action, action_label}; use crate::label_format::NOT_BOUND_LABEL; #[test] fn chinese_help_search_finds_undo_without_reordering_sections() { - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::chinese_help_search_finds_undo_without_reordering_sections" ), || { - crate::i18n::set_language(crate::domain::Language::SimplifiedChinese); - let sections = - build_section_sets(&HelpOverlayBindings::default(), false, false, true, true).all; + i18n::set_language(Language::SimplifiedChinese); + let sections = build_section_sets( + &HelpOverlayBindings::default(), + HelpContentOptions { + frozen_enabled: false, + context_filter: false, + board_enabled: true, + capture_enabled: true, + }, + ) + .all; let titles: Vec<_> = sections.iter().map(|section| section.title).collect(); let filtered = super::filter_sections_for_search(sections, "撤销"); assert!( @@ -40,7 +51,16 @@ fn chinese_help_search_finds_undo_without_reordering_sections() { #[test] fn gesture_hints_remain_present() { let bindings = HelpOverlayBindings::default(); - let sections = build_section_sets(&bindings, false, false, true, true).all; + let sections = build_section_sets( + &bindings, + HelpContentOptions { + frozen_enabled: false, + context_filter: false, + board_enabled: true, + capture_enabled: true, + }, + ) + .all; let rows: Vec<(&str, &str)> = sections .iter() .flat_map(|section| section.rows.iter()) @@ -71,7 +91,16 @@ fn gesture_hints_remain_present() { #[test] fn canvas_export_rows_remain_visible_when_capture_context_is_disabled() { let bindings = HelpOverlayBindings::default(); - let sections = build_section_sets(&bindings, false, true, true, false).all; + let sections = build_section_sets( + &bindings, + HelpContentOptions { + frozen_enabled: false, + context_filter: true, + board_enabled: true, + capture_enabled: false, + }, + ) + .all; let rows: Vec<&str> = sections .iter() .flat_map(|section| section.rows.iter()) @@ -100,7 +129,16 @@ fn canvas_export_rows_remain_visible_when_capture_context_is_disabled() { #[test] fn interactive_region_capture_has_a_help_row_when_capture_is_enabled() { let bindings = HelpOverlayBindings::default(); - let sections = build_section_sets(&bindings, false, true, true, true).all; + let sections = build_section_sets( + &bindings, + HelpContentOptions { + frozen_enabled: false, + context_filter: true, + board_enabled: true, + capture_enabled: true, + }, + ) + .all; let rows = sections .iter() .flat_map(|section| section.rows.iter()) @@ -108,22 +146,30 @@ fn interactive_region_capture_has_a_help_row_when_capture_is_enabled() { assert!(rows.iter().any(|row| { row.action == action_label(Action::CaptureRegionInteractive) - && row.key == crate::label_format::NOT_BOUND_LABEL + && row.key == label_format::NOT_BOUND_LABEL })); } #[test] fn capture_free_measure_mode_stays_visible_when_capture_is_disabled() { let bindings = HelpOverlayBindings::default(); - let sections = build_section_sets(&bindings, false, true, true, false).all; + let sections = build_section_sets( + &bindings, + HelpContentOptions { + frozen_enabled: false, + context_filter: true, + board_enabled: true, + capture_enabled: false, + }, + ) + .all; let rows = sections .iter() .flat_map(|section| section.rows.iter()) .collect::>(); assert!(rows.iter().any(|row| { - row.action == action_label(Action::MeasureMode) - && row.key == crate::label_format::NOT_BOUND_LABEL + row.action == action_label(Action::MeasureMode) && row.key == label_format::NOT_BOUND_LABEL })); } diff --git a/src/ui/help_overlay/types.rs b/src/ui/help_overlay/types.rs index 3bc416cfb..8be7352c7 100644 --- a/src/ui/help_overlay/types.rs +++ b/src/ui/help_overlay/types.rs @@ -1,3 +1,6 @@ +use crate::config::Action; +use crate::label_format; + pub(crate) type IconFn = fn(&cairo::Context, f64, f64, f64); #[derive(Clone)] @@ -6,11 +9,11 @@ pub(crate) struct Row { pub(crate) action: &'static str, /// The action a click on this row executes (rows describing gestures or /// multi-action pairs stay non-clickable). - pub(crate) action_id: Option, + pub(crate) action_id: Option, } impl Row { - pub(crate) fn with_action(mut self, action: crate::config::Action) -> Self { + pub(crate) fn with_action(mut self, action: Action) -> Self { self.action_id = Some(action); self } @@ -18,7 +21,7 @@ impl Row { /// Whether this row only documents an action that has no binding. Such /// rows are hidden by default and shown on request or by search. pub(crate) fn is_unbound(&self) -> bool { - self.key == crate::label_format::NOT_BOUND_LABEL + self.key == label_format::NOT_BOUND_LABEL } } @@ -71,5 +74,5 @@ pub(crate) struct HelpRowHit { pub(crate) y: f64, pub(crate) w: f64, pub(crate) h: f64, - pub(crate) action: crate::config::Action, + pub(crate) action: Action, } diff --git a/src/ui/input_hud.rs b/src/ui/input_hud.rs index 1d46c99b6..9b03c228a 100644 --- a/src/ui/input_hud.rs +++ b/src/ui/input_hud.rs @@ -6,6 +6,8 @@ //! keycap language and tinted from `status_bar_style`, so the HUD reads as the //! same chrome family as the status bar and zoom chip. +use std::time::Instant; + use crate::config::{InputHudPosition, StatusBarStyle}; use crate::input::state::{InputHudEntryKind, InputState}; @@ -68,7 +70,7 @@ fn compute_input_hud_layout( return None; } let font_size = input_state.input_hud_font_size(); - let now = std::time::Instant::now(); + let now = Instant::now(); // Newest chip first: the row is right-aligned in spirit (new chips arrive // on the right and push older ones left), so overflow drops the oldest. diff --git a/src/ui/input_hud/tests.rs b/src/ui/input_hud/tests.rs index 00ddd32ab..eea2494fa 100644 --- a/src/ui/input_hud/tests.rs +++ b/src/ui/input_hud/tests.rs @@ -1,16 +1,17 @@ use super::*; use crate::config::InputHudConfig; -use crate::input::state::{InputHudSettings, test_support::make_test_input_state}; +use crate::input::state::InputHudSettings; +use crate::input::state::test_support::make_test_input_state; use crate::input::{Key, Modifiers}; -fn state_with(config: InputHudConfig) -> InputState { +fn state_with(config: &InputHudConfig) -> InputState { let mut state = make_test_input_state(); - state.init_input_hud_from_config(InputHudSettings::from(&config)); + state.init_input_hud_from_config(InputHudSettings::from(config)); state } fn enabled_state() -> InputState { - state_with(InputHudConfig { + state_with(&InputHudConfig { enabled: true, ..InputHudConfig::default() }) @@ -18,7 +19,7 @@ fn enabled_state() -> InputState { #[test] fn hidden_hud_has_no_geometry() { - let state = state_with(InputHudConfig::default()); + let state = state_with(&InputHudConfig::default()); assert!(input_hud_geometry(&state, 1920, 1080).is_none()); let empty = enabled_state(); @@ -53,7 +54,7 @@ fn anchors_place_the_row_on_the_requested_edges() { InputHudPosition::BottomLeft, InputHudPosition::BottomRight, ] { - let mut state = state_with(InputHudConfig { + let mut state = state_with(&InputHudConfig { enabled: true, position, ..InputHudConfig::default() @@ -86,7 +87,7 @@ fn anchors_place_the_row_on_the_requested_edges() { /// the label at the box edge) so the row never leaves the surface. #[test] fn the_newest_chip_clamps_to_the_available_width() { - let mut state = state_with(InputHudConfig { + let mut state = state_with(&InputHudConfig { enabled: true, font_size: 72.0, ..InputHudConfig::default() @@ -196,7 +197,7 @@ fn paint(engine: &UiTextEngine, layout: &InputHudLayout, density: i32) -> Vec() .0 .iter() .enumerate() - .filter(|(_, p)| p[3] != 0) - { + .filter(|(_, p)| p[3] != 0); + for (index, _) in painted_pixels { let x = (index * 4 % stride) as f64 / (4.0 * f64::from(density)); let y = (index * 4 / stride) as f64 / f64::from(density); assert!( diff --git a/src/ui/ocr_scan.rs b/src/ui/ocr_scan.rs index 3ae3d2c43..fc07c3e26 100644 --- a/src/ui/ocr_scan.rs +++ b/src/ui/ocr_scan.rs @@ -1,5 +1,7 @@ +use std::time::Duration; + use crate::input::state::{OcrScanOutcome, result_opacity}; -use crate::ui::theme::{self, Rgba, overlay}; +use crate::ui::theme::{self, Rgba, overlay, popup}; use crate::util::Rect; use super::primitives::draw_rounded_rect; @@ -205,7 +207,7 @@ pub(crate) fn render_ocr_scan_result( ctx: &cairo::Context, region: Rect, outcome: OcrScanOutcome, - shown: std::time::Duration, + shown: Duration, screen: (u32, u32), ) { let opacity = result_opacity(shown); @@ -227,10 +229,10 @@ pub(crate) fn render_ocr_scan_result( 1.0, ); - theme::set_color(ctx, crate::ui::theme::popup::bg_context_menu()); + theme::set_color(ctx, popup::bg_context_menu()); draw_rounded_rect(ctx, card.x, card.y, card.width, card.height, CARD_RADIUS); let _ = ctx.fill(); - theme::set_color(ctx, crate::ui::theme::popup::border_context_menu()); + theme::set_color(ctx, popup::border_context_menu()); ctx.set_line_width(1.0); draw_rounded_rect( ctx, diff --git a/src/ui/onboarding_card.rs b/src/ui/onboarding_card.rs index 7e26b8af6..7fe653c13 100644 --- a/src/ui/onboarding_card.rs +++ b/src/ui/onboarding_card.rs @@ -1,3 +1,5 @@ +use std::f64::consts; + use super::primitives::draw_rounded_rect; use super::theme::{self, Rgba, overlay}; use crate::ui_text::{UiTextEngine, UiTextStyle}; @@ -296,13 +298,7 @@ pub(crate) fn render_onboarding_card_with_engine( for item in &card.items { let dot_x = content_x + item_dot_size * 0.5; let dot_y = cursor_y + item_dot_size * 0.5 + 1.0 * CARD_TYPE_SCALE; - ctx.arc( - dot_x, - dot_y, - item_dot_size * 0.5, - 0.0, - std::f64::consts::TAU, - ); + ctx.arc(dot_x, dot_y, item_dot_size * 0.5, 0.0, consts::TAU); if item.done { theme::set_color(ctx, DOT_DONE); } else { diff --git a/src/ui/onboarding_card/tests.rs b/src/ui/onboarding_card/tests.rs index fb31daf9e..89aee41b5 100644 --- a/src/ui/onboarding_card/tests.rs +++ b/src/ui/onboarding_card/tests.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeSet; + use super::*; fn button( @@ -142,7 +144,7 @@ fn buttons_wrap_instead_of_leaving_a_narrow_card() { for hit in &layout.buttons { assert!(hit.x + hit.width <= layout.x + layout.width, "{hit:?}"); } - let rows: std::collections::BTreeSet = layout + let rows: BTreeSet = layout .buttons .iter() .map(|hit| hit.y.round() as i64) diff --git a/src/ui/precision_entry.rs b/src/ui/precision_entry.rs index 4ce6a80a3..0224946ba 100644 --- a/src/ui/precision_entry.rs +++ b/src/ui/precision_entry.rs @@ -8,6 +8,7 @@ use crate::input::InputState; use crate::ui::primitives::{draw_rounded_rect, text_extents_for_with_engine}; +use crate::ui::theme::popup; use crate::ui_text::{UiTextEngine, UiTextStyle}; use super::theme::overlay::{ @@ -71,9 +72,9 @@ pub(crate) fn render_precision_entry_popup_with_engine( // Panel card. draw_rounded_rect(ctx, x, y, POPUP_W, POPUP_H, RADIUS_STD); - set_rgba(ctx, crate::ui::theme::popup::bg_modal()); + set_rgba(ctx, popup::bg_modal()); let _ = ctx.fill_preserve(); - set_rgba(ctx, crate::ui::theme::popup::border_modal()); + set_rgba(ctx, popup::border_modal()); ctx.set_line_width(1.0); let _ = ctx.stroke(); diff --git a/src/ui/precision_entry/tests.rs b/src/ui/precision_entry/tests.rs index b2e85ffab..c1340b0ce 100644 --- a/src/ui/precision_entry/tests.rs +++ b/src/ui/precision_entry/tests.rs @@ -1,3 +1,7 @@ +use crate::input::state::test_support; +use crate::ui::toolbar::PrecisionEntryTarget; +use crate::ui::{OnboardingCardAction, OnboardingCardButton}; + use super::*; use crate::ui::onboarding_card::{ OnboardingCard, OnboardingChecklistItem, render_onboarding_card_with_engine, @@ -29,22 +33,22 @@ fn assert_owner_parity( #[test] fn retained_overlay_owner_matches_fresh_across_density_and_visible_state_changes() { let engine = UiTextEngine::default(); - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let mut card = OnboardingCard { eyebrow: "你好 Καλημέρα onboarding".into(), title: "A long title that needs fitting on a small output".into(), body: "Wrapped body text with café and שלום repeated across the narrow card. More words to occupy another line.".into(), items: vec![OnboardingChecklistItem { label: "A long Unicode checklist label 你好 café".into(), done: false }], - buttons: vec![crate::ui::OnboardingCardButton { + buttons: vec![OnboardingCardButton { label: "Ünïcode button 你好".into(), key_hint: Some("Shift+Esc".into()), - action: crate::ui::OnboardingCardAction::SkipTour, + action: OnboardingCardAction::SkipTour, primary: true, }], footer: "Long footer explaining the next action without changing layout policy".into(), }; for density in [1, 2, 1] { - state.open_precision_entry(crate::ui::toolbar::PrecisionEntryTarget::Thickness); + state.open_precision_entry(PrecisionEntryTarget::Thickness); assert_owner_parity(&engine, density, |engine, ctx| { render_precision_entry_popup_with_engine(engine, ctx, &state, 420, 360, (400.0, 350.0)) }); diff --git a/src/ui/primitives.rs b/src/ui/primitives.rs index a39f6d516..8db79eee4 100644 --- a/src/ui/primitives.rs +++ b/src/ui/primitives.rs @@ -1,7 +1,7 @@ use std::f64::consts::{FRAC_PI_2, PI}; use crate::ui::theme::{self, Rgba}; -use crate::ui_text::{UiTextEngine, UiTextStyle}; +use crate::ui_text::{UiTextEngine, UiTextExtents, UiTextStyle}; pub(crate) fn text_extents_for_with_engine( engine: &UiTextEngine, @@ -396,6 +396,7 @@ pub(crate) const BADGE_RADIUS: f64 = 8.0; pub(crate) const BADGE_STACK_GAP: f64 = 8.0; /// Horizontal anchoring for [`draw_badge_with_engine`]. +#[derive(Clone, Copy)] pub(crate) enum BadgeAlign { /// `anchor_x` is the badge's left edge. Left, @@ -407,8 +408,8 @@ pub(crate) enum BadgeAlign { /// Shared by [`draw_badge_with_engine`] and [`measure_badge_with_engine`] so layout and rendering can /// never disagree about badge geometry. fn badge_box( - label_extents: &crate::ui_text::UiTextExtents, - hint_extents: Option<&crate::ui_text::UiTextExtents>, + label_extents: &UiTextExtents, + hint_extents: Option<&UiTextExtents>, ) -> (f64, f64, f64) { let padding = BADGE_PADDING; match hint_extents { diff --git a/src/ui/properties_panel.rs b/src/ui/properties_panel.rs index eb028f7a1..6ca9f2a90 100644 --- a/src/ui/properties_panel.rs +++ b/src/ui/properties_panel.rs @@ -8,7 +8,6 @@ use cairo::FontWeight; use crate::i18n::tr; -use crate::input::InputState; use crate::input::state::properties_panel_metrics::{ BODY_FONT, FOOTER_FONT, FOOTER_HEIGHT, SUBTITLE_FONT, SWITCH_VALUE_GAP, TITLE_FONT, TOOLTIP_FONT, TOOLTIP_PADDING_X, TOOLTIP_PADDING_Y, VALUE_FONT, text_style, @@ -17,14 +16,14 @@ use crate::input::state::{ PanelScroll, PropertiesPanelHit, PropertiesPanelLayout, PropertiesRowControl, PropertiesRowGeometry, ShapePropertiesPanel, }; -use crate::input::{SelectionPropertyEntry, SelectionPropertyValue}; +use crate::input::{InputState, SelectionPropertyEntry, SelectionPropertyValue}; use crate::ui::primitives::{draw_keycap_with_engine, draw_rounded_rect, keycap_size_with_engine}; use crate::ui::theme::overlay::{ BG_HOVER_WASH, BORDER_FOCUS, BORDER_PROPERTIES, DIVIDER, DIVIDER_LIGHT, FOCUS_RING_WIDTH, PANEL_BG_CONTEXT_SUBMENU, RADIUS_PANEL, RADIUS_STD, TEXT_DISABLED, TEXT_HINT, TEXT_PRIMARY, TEXT_SECONDARY, TEXT_TERTIARY, empty_properties, }; -use crate::ui::theme::{Rgba, set_color, with_alpha}; +use crate::ui::theme::{Rgba, popup, set_color, with_alpha}; use crate::ui_text::UiTextEngine; mod actions; @@ -86,9 +85,9 @@ pub(crate) fn render_properties_panel_with_engine( layout.height, RADIUS_PANEL, ); - set_color(ctx, crate::ui::theme::popup::bg_properties()); + set_color(ctx, popup::bg_properties()); let _ = ctx.fill_preserve(); - set_color(ctx, crate::ui::theme::popup::border_properties()); + set_color(ctx, popup::border_properties()); ctx.set_line_width(1.0); let _ = ctx.stroke(); diff --git a/src/ui/properties_panel/actions.rs b/src/ui/properties_panel/actions.rs index 54899245e..23c6c27c3 100644 --- a/src/ui/properties_panel/actions.rs +++ b/src/ui/properties_panel/actions.rs @@ -1,6 +1,8 @@ //! The panel's actions area: the four ordering buttons, and Duplicate and //! Delete under them. +use std::f64::consts; + use cairo::FontWeight; use crate::draw::Color; @@ -124,7 +126,7 @@ fn draw_preset_chip( let left = cx - (dot * 2.0 + gap + number_width) / 2.0; ctx.new_path(); - ctx.arc(left + dot, cy, dot, 0.0, std::f64::consts::TAU); + ctx.arc(left + dot, cy, dot, 0.0, consts::TAU); match color { Some(color) => { set_color(ctx, (color.r, color.g, color.b, color.a.max(0.35))); diff --git a/src/ui/properties_panel/controls.rs b/src/ui/properties_panel/controls.rs index 3b8a2ba5a..7c47e3628 100644 --- a/src/ui/properties_panel/controls.rs +++ b/src/ui/properties_panel/controls.rs @@ -1,6 +1,7 @@ //! Painters for the properties panel's controls. Each takes the rectangles //! the panel geometry laid out, so drawing never re-derives a position. +use std::f64::consts; use std::f64::consts::{PI, TAU}; use cairo::FontWeight; @@ -16,6 +17,7 @@ use crate::input::state::{ }; use crate::toolbar_icons::draw_arrow_style_preview; use crate::ui::primitives::{checkerboard_behind, draw_rounded_rect}; +use crate::ui::theme; use crate::ui::theme::overlay::{ ACCENT_BRIGHT, ACCENT_PRIMARY, BG_HOVER, BG_HOVER_WASH, PANEL_BG_PROPERTIES, RADIUS_MD, RADIUS_SM, RADIUS_STD, TEXT_DISABLED, TEXT_HINT, TEXT_PRIMARY, TEXT_SECONDARY, TEXT_TERTIARY, @@ -34,7 +36,7 @@ const QUIET_OUTLINE: Rgba = (1.0, 1.0, 1.0, 0.16); /// A locked selection's padlock: the same amber the toolbar uses for "held". const LOCK_ACTIVE: Rgba = (0.965, 0.827, 0.176, 1.0); /// The stroke across the "no fill" swatch: the destructive red, as "none". -const NO_FILL_STRIKE: Rgba = crate::ui::theme::rgba(crate::ui::theme::DESTRUCTIVE_RGB, 0.9); +const NO_FILL_STRIKE: Rgba = theme::rgba(theme::DESTRUCTIVE_RGB, 0.9); /// How much of its color a control keeps while its row is locked. const DISABLED_ALPHA: f64 = 0.35; const GLYPH_WIDTH: f64 = 1.6; @@ -213,7 +215,7 @@ fn draw_no_fill(ctx: &cairo::Context, rect: PanelRect, selected: bool, state: Co ); ctx.set_line_width(1.5); let _ = ctx.stroke(); - let reach = radius * std::f64::consts::FRAC_1_SQRT_2; + let reach = radius * consts::FRAC_1_SQRT_2; set_color(ctx, state.fade(NO_FILL_STRIKE)); round_line(ctx, (cx - reach, cy + reach), (cx + reach, cy - reach)); if selected { diff --git a/src/ui/radial_menu/cache.rs b/src/ui/radial_menu/cache.rs index 2c611a0ab..5b38eed4f 100644 --- a/src/ui/radial_menu/cache.rs +++ b/src/ui/radial_menu/cache.rs @@ -12,6 +12,8 @@ use crate::ui_text::UiTextEngine; use cairo::{Context, Format, ImageSurface}; +use crate::draw::Color; +use crate::input::DrawingState; use crate::input::state::{ InputState, RADIAL_COMPASS_SLICES, RadialMenuLayout, RadialRingSwatch, RadialSliceKind, sub_ring_children, @@ -174,10 +176,7 @@ pub(super) fn base_cache_key( ) -> BaseKey { let active_tool = input_state.active_tool(); let active_color = input_state.color_for_tool(active_tool); - let text_input = matches!( - input_state.state, - crate::input::DrawingState::TextInput { .. } - ); + let text_input = matches!(input_state.state, DrawingState::TextInput { .. }); let bindings = RADIAL_COMPASS_SLICES .iter() @@ -254,7 +253,7 @@ fn physical_size(extent: f64, scale: f64) -> i32 { ((extent * 2.0) * scale).ceil() as i32 } -fn color_key(color: &crate::draw::Color) -> String { +fn color_key(color: &Color) -> String { format!( "{:.4},{:.4},{:.4},{:.4}", color.r, color.g, color.b, color.a @@ -263,6 +262,11 @@ fn color_key(color: &crate::draw::Color) -> String { #[cfg(test)] mod tests { + use std::f64::consts; + + use crate::config::{KeybindingsConfig, QuickColorPalette, QuickColorPaletteEntry}; + use crate::draw::TextMeasurer; + use super::*; use crate::draw::Color; use crate::input::Tool; @@ -295,7 +299,7 @@ mod tests { // Recents arc state.apply_color_from_ui_with_measurer( - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), Color { r: 0.123, g: 0.456, @@ -327,8 +331,8 @@ mod tests { fn base_cache_key_changes_with_quick_palette() { let mut state = make_test_input_state(); let base = key_for(&state); - state.set_quick_colors(crate::config::QuickColorPalette::from_entries(vec![ - crate::config::QuickColorPaletteEntry { + state.set_quick_colors(QuickColorPalette::from_entries(vec![ + QuickColorPaletteEntry { label: "Only".to_string(), color: Color { r: 0.2, @@ -347,9 +351,9 @@ mod tests { #[test] fn a_transparent_ring_swatch_paints_the_checkerboard() { let mut state = make_test_input_state(); - state.set_quick_colors(crate::config::QuickColorPalette::from_entries( + state.set_quick_colors(QuickColorPalette::from_entries( (0..8) - .map(|index| crate::config::QuickColorPaletteEntry { + .map(|index| QuickColorPaletteEntry { label: format!("Clear {index}"), color: Color { r: 1.0, @@ -385,7 +389,7 @@ mod tests { let data = surface.data().expect("pixel data"); let covered = (0..samples) .filter(|step| { - let angle = std::f64::consts::TAU * f64::from(*step) / f64::from(samples); + let angle = consts::TAU * f64::from(*step) / f64::from(samples); let x = (EXTENT + radius * angle.cos()).round() as usize; let y = (EXTENT + radius * angle.sin()).round() as usize; // ARGB32 is native-endian, so alpha is the last byte. @@ -409,7 +413,7 @@ mod tests { use crate::input::state::test_support::make_test_input_state_with_action_bindings; let default_state = make_test_input_state(); - let mut bindings = crate::config::KeybindingsConfig::default() + let mut bindings = KeybindingsConfig::default() .build_action_bindings() .expect("default bindings"); bindings.insert( diff --git a/src/ui/radial_menu/cache/owner_tests.rs b/src/ui/radial_menu/cache/owner_tests.rs index deb78347d..76e773b26 100644 --- a/src/ui/radial_menu/cache/owner_tests.rs +++ b/src/ui/radial_menu/cache/owner_tests.rs @@ -1,3 +1,10 @@ +use std::f64::consts; + +use crate::config::{KeybindingsConfig, QuickColorPalette, QuickColorPaletteEntry}; +use crate::input::state; +use crate::input::state::RadialMenuState; +use crate::ui::UiRenderCaches; + use super::*; use crate::draw::Color; use crate::input::Tool; @@ -123,8 +130,8 @@ fn palette_recents_and_active_changes_replace_rendered_base() { let theme = theme::Theme::dark(); let mut cache = RadialBaseCache::default(); paint(&engine, &mut cache, &state, &theme, 1.0); - state.set_quick_colors(crate::config::QuickColorPalette::from_entries(vec![ - crate::config::QuickColorPaletteEntry { + state.set_quick_colors(QuickColorPalette::from_entries(vec![ + QuickColorPaletteEntry { label: "Transparent".into(), color: Color::new(1.0, 0.0, 0.0, 0.0), }, @@ -146,7 +153,7 @@ fn bindings_replace_rendered_base() { use crate::config::{Action, Shortcut}; use crate::input::state::test_support::make_test_input_state_with_action_bindings; let state = state(); - let mut bindings = crate::config::KeybindingsConfig::default() + let mut bindings = KeybindingsConfig::default() .build_action_bindings() .unwrap(); bindings.insert(Action::SelectPenTool, vec![Shortcut::parse("F9").unwrap()]); @@ -164,9 +171,9 @@ fn bindings_replace_rendered_base() { fn transparent_swatches_keep_checkerboard_across_themes_and_densities() { let engine = UiTextEngine::default(); let mut state = state(); - state.set_quick_colors(crate::config::QuickColorPalette::from_entries( + state.set_quick_colors(QuickColorPalette::from_entries( (0..8) - .map(|index| crate::config::QuickColorPaletteEntry { + .map(|index| QuickColorPaletteEntry { label: format!("Clear {index}"), color: Color::new(1.0, 0.0, 0.0, 0.0), }) @@ -181,7 +188,7 @@ fn transparent_swatches_keep_checkerboard_across_themes_and_densities() { let stride = (500.0 * scale) as usize * 4; let covered = (0..64) .filter(|index| { - let angle = std::f64::consts::TAU * f64::from(*index) / 64.0; + let angle = consts::TAU * f64::from(*index) / 64.0; let x = ((layout.center_x + radius * angle.cos()) * scale).round() as usize; let y = ((layout.center_y + radius * angle.sin()) * scale).round() as usize; let offset = y * stride + x * 4; @@ -257,7 +264,7 @@ fn failed_allocation_paints_directly_without_replacing_cache_and_can_retry() { fn paint_full( engine: &UiTextEngine, - caches: &mut crate::ui::UiRenderCaches, + caches: &mut UiRenderCaches, input: &InputState, theme: &theme::Theme, scale: f64, @@ -289,12 +296,12 @@ fn paint_full( #[test] fn dynamic_hover_sub_ring_and_center_labels_match_fresh_owners() { let engine = UiTextEngine::default(); - let mut caches = crate::ui::UiRenderCaches::default(); + let mut caches = UiRenderCaches::default(); let mut input = state(); let layout = input.radial_menu.layout().unwrap(); - let parent = crate::input::state::RADIAL_COMPASS_SLICES + let parent = state::RADIAL_COMPASS_SLICES .iter() - .position(|slice| matches!(slice.kind, crate::input::state::RadialSliceKind::Parent(_))) + .position(|slice| matches!(slice.kind, RadialSliceKind::Parent(_))) .unwrap(); let (segment, offset, _) = super::super::compass_geometry(&layout); let (x, y) = super::super::compass_wedge_midpoint( @@ -315,7 +322,7 @@ fn dynamic_hover_sub_ring_and_center_labels_match_fresh_owners() { ); assert!(matches!( input.radial_menu.state(), - crate::input::state::RadialMenuState::Open { + RadialMenuState::Open { expanded_sub_ring: Some(_), .. } @@ -325,7 +332,7 @@ fn dynamic_hover_sub_ring_and_center_labels_match_fresh_owners() { let actual = paint_full(&engine, &mut caches, &input, &theme, scale); let expected = paint_full( &UiTextEngine::default(), - &mut crate::ui::UiRenderCaches::default(), + &mut UiRenderCaches::default(), &input, &theme, scale, @@ -348,7 +355,7 @@ fn dynamic_hover_sub_ring_and_center_labels_match_fresh_owners() { paint_full(&engine, &mut caches, &input, &theme, 1.0) == paint_full( &UiTextEngine::default(), - &mut crate::ui::UiRenderCaches::default(), + &mut UiRenderCaches::default(), &input, &theme, 1.0 diff --git a/src/ui/radial_menu/mod.rs b/src/ui/radial_menu/mod.rs index c198b05ac..1d346adea 100644 --- a/src/ui/radial_menu/mod.rs +++ b/src/ui/radial_menu/mod.rs @@ -9,6 +9,7 @@ use text::{draw_centered_label, draw_wedge_content}; use std::f64::consts::PI; use crate::config::{Action, action_meta, action_short_label}; +use crate::draw::Color; use crate::input::state::{ RADIAL_COMPASS_SLICES, RADIAL_TOOL_SEGMENT_COUNT, RadialMenuLayout, RadialMenuState, RadialParent, RadialRingSwatch, RadialSegmentId, RadialSlice, RadialSliceKind, @@ -22,7 +23,10 @@ use crate::toolbar_icons::{ }; use crate::ui::primitives::{draw_keycap_with_engine, keycap_size_with_engine}; use crate::ui::theme::{self, Rgba, overlay, toolbar}; +use crate::ui::toolbar::model; +use crate::ui::{UiRenderCaches, UiRenderCtx}; use crate::ui_text::UiTextEngine; +use crate::{toolbar_icons, ui}; // ── File-local style values without a matching token in ui/theme.rs ── @@ -50,7 +54,7 @@ pub fn render_radial_menu(ctx: &cairo::Context, input_state: &InputState, width: return; } let engine = UiTextEngine::default(); - let mut caches = crate::ui::UiRenderCaches::default(); + let mut caches = UiRenderCaches::default(); render_radial_menu_with_context( &engine, &mut crate::ui::UiRenderCtx { @@ -66,7 +70,7 @@ pub fn render_radial_menu(ctx: &cairo::Context, input_state: &InputState, width: pub(crate) fn render_radial_menu_with_context( engine: &UiTextEngine, - render: &mut crate::ui::UiRenderCtx<'_, '_, '_>, + render: &mut UiRenderCtx<'_, '_, '_>, input_state: &InputState, width: u32, height: u32, @@ -188,7 +192,7 @@ fn draw_static_base( end - color_gap, ); }; - crate::ui::checkerboard_behind(ctx, c.a, segment); + ui::checkerboard_behind(ctx, c.a, segment); segment(ctx); ctx.set_source_rgba(c.r, c.g, c.b, c.a); let _ = ctx.fill_preserve(); @@ -392,7 +396,7 @@ fn draw_sub_ring( let color = wedge_content_color(theme, is_hovered, is_active); let icon = action_meta(*action) .and_then(|meta| meta.icon) - .map(crate::toolbar_icons::action_icon_painter); + .map(toolbar_icons::action_icon_painter); let label = action_short_label(*action); match icon { Some(_) if show_labels => { @@ -501,7 +505,7 @@ fn draw_center_well( active_color.b, active_color.a, ), - ring_width: crate::ui::toolbar::model::micro_ring_width(size), + ring_width: model::micro_ring_width(size), icon_color: if is_center_hovered { toolbar::COLOR_ICON_HOVER } else { @@ -676,7 +680,7 @@ fn slice_icon(slice: &RadialSlice) -> Option { match slice.kind { RadialSliceKind::Action(action) => action_meta(action) .and_then(|meta| meta.icon) - .map(crate::toolbar_icons::action_icon_painter), + .map(toolbar_icons::action_icon_painter), RadialSliceKind::Parent(RadialParent::Shapes) => Some(draw_icon_shape_picker), RadialSliceKind::Parent(RadialParent::Notes) => Some(draw_icon_note), } @@ -734,6 +738,6 @@ fn center_glyph(input_state: &InputState, active_tool: Tool) -> ToolbarIconPaint } /// Check whether two colors are approximately equal. -fn colors_match(a: &crate::draw::Color, b: &crate::draw::Color) -> bool { +fn colors_match(a: &Color, b: &Color) -> bool { (a.r - b.r).abs() < 0.01 && (a.g - b.g).abs() < 0.01 && (a.b - b.b).abs() < 0.01 } diff --git a/src/ui/region_action_bar/render.rs b/src/ui/region_action_bar/render.rs index 8b46668de..752523f18 100644 --- a/src/ui/region_action_bar/render.rs +++ b/src/ui/region_action_bar/render.rs @@ -1,3 +1,5 @@ +use crate::ui::theme::popup; + use super::controls::{draw_action, draw_toggle}; use super::layout::{RegionActionBar, RegionActionRect, STATUS_ROW_HEIGHT}; use super::model::{RegionAction, RegionActionBarVisual, RegionCutStatus}; @@ -85,7 +87,7 @@ fn draw_bar_frame(ctx: &cairo::Context, bounds: RegionActionRect) { let _ = ctx.fill(); } - theme::set_color(ctx, crate::ui::theme::popup::bg_context_menu()); + theme::set_color(ctx, popup::bg_context_menu()); draw_rounded_rect( ctx, bounds.x, @@ -96,7 +98,7 @@ fn draw_bar_frame(ctx: &cairo::Context, bounds: RegionActionRect) { ); let _ = ctx.fill(); - theme::set_color(ctx, crate::ui::theme::popup::border_context_menu()); + theme::set_color(ctx, popup::border_context_menu()); ctx.set_line_width(1.0); draw_rounded_rect( ctx, diff --git a/src/ui/region_capture_picker.rs b/src/ui/region_capture_picker.rs index c8ea5dd84..9ebe16d57 100644 --- a/src/ui/region_capture_picker.rs +++ b/src/ui/region_capture_picker.rs @@ -14,6 +14,8 @@ pub(crate) use types::{ RegionCaptureWindowVisual, RegionCutDragVisual, RegionCutPreviewVisual, }; +use crate::draw::Color; +use crate::ui; use crate::ui::region_action_bar::{ RegionActionBar, RegionActionBarVisual, render_region_action_bar, }; @@ -34,10 +36,10 @@ pub(crate) fn render_region_capture_loupe( ctx: &cairo::Context, screen: (u32, u32), visual: RegionCaptureLoupeVisual, - sample: impl FnMut(f64, f64) -> Option, + sample: impl FnMut(f64, f64) -> Option, ) { - let layout = crate::ui::compute_eyedropper_loupe_layout(visual.pointer, screen); - crate::ui::render_eyedropper_loupe(ctx, layout, visual.image_center, sample); + let layout = ui::compute_eyedropper_loupe_layout(visual.pointer, screen); + ui::render_eyedropper_loupe(ctx, layout, visual.image_center, sample); } pub(crate) fn render_region_capture_picker( @@ -46,7 +48,7 @@ pub(crate) fn render_region_capture_picker( screen_width: u32, screen_height: u32, visual: &RegionCapturePickerVisual<'_>, - mut sample_loupe: impl FnMut(f64, f64) -> Option, + mut sample_loupe: impl FnMut(f64, f64) -> Option, ) { let width = f64::from(screen_width); let height = f64::from(screen_height); diff --git a/src/ui/region_capture_picker/tests.rs b/src/ui/region_capture_picker/tests.rs index a4b2c3961..c49ee825a 100644 --- a/src/ui/region_capture_picker/tests.rs +++ b/src/ui/region_capture_picker/tests.rs @@ -1,3 +1,6 @@ +use crate::ui::theme::overlay; +use crate::ui::{RegionAction, RegionResizeHandles}; + use super::layout::{ PointerPanelLayout, covered_by_action_bar, pointer_panel_layout, selection_badge_layout, }; @@ -79,7 +82,7 @@ fn the_shared_legend_paints_across_the_top_of_any_selector() { "the strip is tall enough for its readable text" ); assert_eq!(alpha(400, 300), 0, "and nowhere else"); - const { assert!(super::legend::LEGEND_FONT_SIZE >= crate::ui::theme::overlay::FONT_SIZE_MIN_TEXT) }; + const { assert!(super::legend::LEGEND_FONT_SIZE >= overlay::FONT_SIZE_MIN_TEXT) }; } #[test] @@ -413,7 +416,7 @@ fn review_visual_composes_the_action_bar_after_the_scrim() { start: (100.0, 100.0), end: (300.0, 200.0), }; - let bar = crate::ui::RegionActionBar::place(selection, (800, 600)); + let bar = ui::RegionActionBar::place(selection, (800, 600)); let mut surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 800, 600).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); render_region_capture_picker( @@ -432,7 +435,7 @@ fn review_visual_composes_the_action_bar_after_the_scrim() { show_legend: false, loupe: None, action_bar: Some(bar), - hovered_action: Some(crate::ui::RegionAction::Both), + hovered_action: Some(RegionAction::Both), include_drawings: true, cut: Default::default(), window: RegionCaptureWindowVisual::disabled(), @@ -641,7 +644,7 @@ fn review_paints_grips_where_targeting_paints_corner_arms() { // the two frames can be compared pixel for pixel. show_scrim: false, review, - resize_handles: review.then(|| crate::ui::RegionResizeHandles::place(selection)), + resize_handles: review.then(|| RegionResizeHandles::place(selection)), hovered_handle: None, show_legend: false, loupe: None, @@ -692,7 +695,7 @@ fn capture_loupe_reuses_the_pixel_loupe_renderer_when_enabled() { let mut surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 200, 200).unwrap(); let ctx = cairo::Context::new(&surface).unwrap(); render_region_capture_loupe(&ctx, (200, 200), visual, |_x, _y| { - Some(crate::draw::Color::new(1.0, 0.0, 0.0, 1.0)) + Some(Color::new(1.0, 0.0, 0.0, 1.0)) }); drop(ctx); surface.flush(); @@ -708,9 +711,12 @@ fn accepted_cut_preview_paints_before_the_scrim_hole() { 2, 1, 8, - [0x33, 0x22, 0x11, 0xFF, 0xCC, 0xBB, 0xAA, 0xFF].to_vec(), + [0x33, 0x22, 0x11, 0xff, 0xcc, 0xbb, 0xaa, 0xff].to_vec(), ) .unwrap(); + // SAFETY: `pixels` holds exactly stride * height = 8 bytes and outlives + // `created`, which is dropped below without being drawn on, so Cairo never + // writes through the pointer cast from the shared slice. let created = unsafe { cairo::ImageSurface::create_for_data_unsafe( pixels.data().as_ptr() as *mut u8, @@ -773,12 +779,12 @@ fn accepted_cut_preview_paints_before_the_scrim_hole() { assert!(alpha(2, 2) > 0, "vacated source is dimmed by the scrim"); assert_eq!( &data[18 * stride + 18 * 4..18 * stride + 18 * 4 + 4], - &[0x33, 0x22, 0x11, 0xFF], + &[0x33, 0x22, 0x11, 0xff], "first source pixel fills the left half of the displayed output" ); assert_eq!( &data[18 * stride + 34 * 4..18 * stride + 34 * 4 + 4], - &[0xCC, 0xBB, 0xAA, 0xFF], + &[0xcc, 0xbb, 0xaa, 0xff], "second source pixel fills the right half of the displayed output" ); } diff --git a/src/ui/region_capture_picker/types.rs b/src/ui/region_capture_picker/types.rs index 9d7763d0a..7c16989ad 100644 --- a/src/ui/region_capture_picker/types.rs +++ b/src/ui/region_capture_picker/types.rs @@ -8,10 +8,10 @@ use crate::ui::region_action_bar::{ use crate::ui::region_resize_handles::RegionResizeHandles; #[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) struct RegionCaptureWindowVisual<'a> { +pub(crate) struct RegionCaptureWindowVisual<'picker> { pub available: bool, pub active: bool, - pub targets: &'a [RegionSelection], + pub targets: &'picker [RegionSelection], /// The pointer-hovered or keyboard-focused window candidate. pub highlighted_target: Option, } @@ -38,11 +38,11 @@ impl RegionCaptureWindowVisual<'_> { } #[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) struct RegionCapturePickerVisual<'a> { +pub(crate) struct RegionCapturePickerVisual<'picker> { pub selection: Option, pub pointer: (f64, f64), /// Authoritative pixel coordinates or size supplied by the picker owner. - pub measurement: Option<&'a str>, + pub measurement: Option<&'picker str>, pub show_scrim: bool, pub show_legend: bool, /// The selection is committed and awaiting a destination choice. Review @@ -57,13 +57,13 @@ pub(crate) struct RegionCapturePickerVisual<'a> { pub action_bar: Option, pub hovered_action: Option, pub include_drawings: bool, - pub cut: RegionCaptureCutVisual<'a>, - pub window: RegionCaptureWindowVisual<'a>, + pub cut: RegionCaptureCutVisual<'picker>, + pub window: RegionCaptureWindowVisual<'picker>, } #[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) struct RegionCutPreviewVisual<'a> { - pub pixels: &'a PackedArgb32, +pub(crate) struct RegionCutPreviewVisual<'picker> { + pub pixels: &'picker PackedArgb32, pub display: RegionSelection, } @@ -74,8 +74,8 @@ pub(crate) struct RegionCutDragVisual { } #[derive(Debug, Clone, Copy, PartialEq, Default)] -pub(crate) struct RegionCaptureCutVisual<'a> { - pub preview: Option>, +pub(crate) struct RegionCaptureCutVisual<'picker> { + pub preview: Option>, pub drag: Option, pub availability: RegionActionAvailability, pub cut_armed: bool, diff --git a/src/ui/render_context.rs b/src/ui/render_context.rs index 361f1b84e..1bd8d7ad8 100644 --- a/src/ui/render_context.rs +++ b/src/ui/render_context.rs @@ -21,8 +21,8 @@ impl UiRenderCaches { } /// A short UI paint pass borrowing an explicit theme and its owner's resources. -pub(crate) struct UiRenderCtx<'c, 't, 'r> { - pub cairo: &'c cairo::Context, - pub theme: &'t Theme, - pub caches: &'r mut UiRenderCaches, +pub(crate) struct UiRenderCtx<'cairo, 'theme, 'caches> { + pub cairo: &'cairo cairo::Context, + pub theme: &'theme Theme, + pub caches: &'caches mut UiRenderCaches, } diff --git a/src/ui/spotlight_control.rs b/src/ui/spotlight_control.rs index e6e3c25c5..6557091e4 100644 --- a/src/ui/spotlight_control.rs +++ b/src/ui/spotlight_control.rs @@ -4,10 +4,14 @@ //! [`crate::input::state::SpotlightMagnificationTrack`], and the caller decides //! when the control is visible. +use std::f64::consts; + +use crate::draw; use crate::input::state::SpotlightMagnificationTrack; use crate::ui::primitives::{draw_rounded_rect, text_extents_for_with_engine}; use crate::ui::theme::{self, overlay}; use crate::ui_text::{UiTextEngine, UiTextStyle}; +use crate::util::Rect; /// Height of the drawn track bar, which is thinner than the knob it carries. const TRACK_BAR_HEIGHT: f64 = 4.0; @@ -32,7 +36,7 @@ pub(crate) fn render_spotlight_magnification_control( track: SpotlightMagnificationTrack, magnification: f64, unavailable_reason: Option<&str>, - visible: crate::util::Rect, + visible: Rect, ) { let bounds = track.track; let knob = track.knob; @@ -73,7 +77,7 @@ pub(crate) fn render_spotlight_magnification_control( f64::from(knob.y) + radius, radius, 0.0, - std::f64::consts::TAU, + consts::TAU, ); theme::set_color(ctx, overlay::TEXT_WHITE); let _ = ctx.fill_preserve(); @@ -84,9 +88,9 @@ pub(crate) fn render_spotlight_magnification_control( let label = match unavailable_reason { Some(reason) => format!( "{} - {reason}", - crate::draw::format_spotlight_magnification(magnification) + draw::format_spotlight_magnification(magnification) ), - None => crate::draw::format_spotlight_magnification(magnification), + None => draw::format_spotlight_magnification(magnification), }; let style = UiTextStyle { family: "Sans", diff --git a/src/ui/status/badges.rs b/src/ui/status/badges.rs index 4c3f48872..065468e7b 100644 --- a/src/ui/status/badges.rs +++ b/src/ui/status/badges.rs @@ -1,3 +1,5 @@ +use crate::util; + use super::super::primitives::{ BADGE_PADDING, BADGE_STACK_GAP, BadgeAlign, draw_badge_with_engine, }; @@ -97,6 +99,10 @@ pub(crate) fn render_frozen_badge_with_engine( /// /// Returns the vertical space consumed (badge height plus stacking gap) so /// callers can position the next stacked badge below it. +#[expect( + clippy::must_use_candidate, + reason = "painting is the purpose; the last stacked badge may ignore its height" +)] pub fn render_zoom_badge( ctx: &cairo::Context, screen_width: u32, @@ -141,6 +147,10 @@ pub(crate) fn render_zoom_badge_with_engine( /// /// Returns the vertical space consumed (badge height plus stacking gap) so /// callers can position the next stacked badge below it. +#[expect( + clippy::must_use_candidate, + reason = "painting is the purpose; the last stacked badge may ignore its height" +)] pub fn render_pan_badge( ctx: &cairo::Context, screen_width: u32, @@ -256,7 +266,7 @@ pub(crate) fn render_page_badge_with_engine( page_index: usize, page_count: usize, ) { - let truncated_name = crate::util::truncate_with_ellipsis(board_name, 20); + let truncated_name = util::truncate_with_ellipsis(board_name, 20); let board_label = if !truncated_name.trim().is_empty() { if board_count > 1 { Some(trf( diff --git a/src/ui/status/bar.rs b/src/ui/status/bar.rs index 2525b1750..308b5a9ec 100644 --- a/src/ui/status/bar.rs +++ b/src/ui/status/bar.rs @@ -1,22 +1,14 @@ -use std::f64::consts::PI; - -use super::super::primitives::{ - BADGE_STACK_GAP, BadgeAlign, draw_badge_with_engine, draw_pill, draw_rounded_rect, - measure_badge_with_engine, -}; -use super::super::theme::{self, overlay}; -use super::badges::{ - EDITING_BADGE_FONT_SIZE, EDITING_BADGE_HINT_FONT_SIZE, EDITING_BADGE_TINT, - FROZEN_BADGE_FONT_SIZE, FROZEN_BADGE_TINT, PAN_BADGE_FONT_SIZE, PAN_BADGE_TINT, - ZOOM_BADGE_FONT_SIZE, ZOOM_BADGE_TINT, editing_badge_hint, editing_badge_label, - frozen_badge_label, pan_badge_label, zoom_badge_label, -}; +use super::super::theme::overlay; +#[cfg(test)] +use super::badges::FROZEN_BADGE_TINT; +#[cfg(test)] use crate::config::{Action, StatusPosition, action_display_label}; use crate::i18n::tr; -use crate::input::{BoardBackground, DrawingState, InputState, TextInputMode, Tool}; -use crate::label_format::{format_binding_labels, join_binding_labels}; -use crate::ui::toolbar::bindings::action_for_tool; -use crate::ui_text::{UiTextEngine, UiTextExtents, UiTextStyle}; +use crate::input::InputState; +#[cfg(test)] +use crate::input::{DrawingState, Tool}; +use crate::ui::tooltip; +use crate::ui_text::UiTextEngine; mod content; mod helpers; @@ -187,12 +179,12 @@ impl StatusHudLayout { /// in the lower half of the screen, below it otherwise, kept on screen. pub(crate) fn tooltip_for( &self, - engine: &crate::ui_text::UiTextEngine, + engine: &UiTextEngine, kind: StatusHudSegmentKind, ) -> Option { let segment = self.segments.iter().find(|segment| segment.kind == kind)?; let text = kind.tooltip(); - let (width, height) = crate::ui::tooltip::tooltip_size(engine, text)?; + let (width, height) = tooltip::tooltip_size(engine, text)?; let screen_width = f64::from(self.screen_width); let screen_height = f64::from(self.screen_height); @@ -228,7 +220,7 @@ impl StatusHudTooltip { /// The painted footprint, shadow included, for damage. pub(crate) fn bounds(&self) -> (f64, f64, f64, f64) { let (x, y, w, h) = self.rect; - let outset = crate::ui::tooltip::TOOLTIP_PAINT_OUTSET; + let outset = tooltip::TOOLTIP_PAINT_OUTSET; (x, y, w + outset, h + outset) } diff --git a/src/ui/status/bar/content.rs b/src/ui/status/bar/content.rs index 927199cbc..3590c3e69 100644 --- a/src/ui/status/bar/content.rs +++ b/src/ui/status/bar/content.rs @@ -1,7 +1,18 @@ -use super::helpers::*; -use super::measurement::*; -use super::*; +use crate::config::StatusBarStyle; +use crate::draw::TextMeasurer; +use crate::{build_info, util}; + +use super::helpers::{help_binding_label, pill_origin, tool_display_name, toolbar_hint_label}; +use super::measurement::{cluster_line_metrics, layout_mode_badges, measure_status_bar}; +use super::{ + BOARD_NAME_DEGRADATION_RUNGS, BOARD_NAME_MAX_CHARS, MIN_INTERACTIVE_WIDTH, SEGMENT_SEPARATOR, + STATUS_BAR_MAX_WIDTH_FRACTION, StatusHudLayout, StatusHudPrefix, StatusHudRun, + StatusHudSegment, StatusHudSegmentKind, +}; +use crate::config::{Action, StatusPosition, action_display_label}; use crate::i18n::trf; +use crate::input::{DrawingState, InputState, Tool}; +use crate::ui_text::{UiTextEngine, UiTextExtents, UiTextStyle}; pub(super) fn status_text_style(font_size: f64) -> UiTextStyle<'static> { UiTextStyle { @@ -65,12 +76,12 @@ impl StatusHudPiece { pub fn compute_status_hud_layout( input_state: &InputState, position: StatusPosition, - style: &crate::config::StatusBarStyle, + style: &StatusBarStyle, screen_width: u32, screen_height: u32, ) -> Option { let engine = UiTextEngine::default(); - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); compute_status_hud_layout_with_resources( &engine, &measurer, @@ -84,10 +95,10 @@ pub fn compute_status_hud_layout( pub(crate) fn compute_status_hud_layout_with_resources( engine: &UiTextEngine, - measurer: &crate::draw::TextMeasurer, + measurer: &TextMeasurer, input_state: &InputState, position: StatusPosition, - style: &crate::config::StatusBarStyle, + style: &StatusBarStyle, screen_width: u32, screen_height: u32, ) -> Option { @@ -290,7 +301,7 @@ fn board_segment_label(input_state: &InputState, max_name_chars: Option) let index = input_state.boards.active_index() + 1; let count = input_state.boards.board_count().max(1); let name = max_name_chars - .map(|limit| crate::util::truncate_with_ellipsis(input_state.board_name(), limit)) + .map(|limit| util::truncate_with_ellipsis(input_state.board_name(), limit)) .unwrap_or_default(); if name.trim().is_empty() { trf("Board {}/{}", &[&index, &count]) @@ -437,7 +448,7 @@ pub(super) fn build_cluster_pieces(input_state: &InputState) -> Vec Vec Option { let mut parts: Vec = Vec::new(); if let Some(label) = input_state.status_output_label() { - let label = crate::util::truncate_with_ellipsis(label, 28); + let label = util::truncate_with_ellipsis(label, 28); parts.push(trf("Output: {}", &[&label])); } if input_state.ui_visibility.show_status_selection_info diff --git a/src/ui/status/bar/helpers.rs b/src/ui/status/bar/helpers.rs index 9b1ab3a1e..a9dd20b14 100644 --- a/src/ui/status/bar/helpers.rs +++ b/src/ui/status/bar/helpers.rs @@ -1,5 +1,9 @@ -use super::*; +use super::STATUS_BAR_EDGE_INSET; +use crate::config::{Action, StatusPosition, action_display_label}; use crate::i18n::{tr, trf}; +use crate::input::{DrawingState, InputState, TextInputMode, Tool}; +use crate::label_format::{format_binding_labels, join_binding_labels}; +use crate::ui::toolbar::bindings::action_for_tool; // ============================================================================ // Helpers diff --git a/src/ui/status/bar/measurement.rs b/src/ui/status/bar/measurement.rs index 94b5971bf..43ad3629f 100644 --- a/src/ui/status/bar/measurement.rs +++ b/src/ui/status/bar/measurement.rs @@ -18,7 +18,8 @@ pub(super) fn cluster_line_metrics( ) -> ClusterLineMetrics { let mut ascent = (-sep_extents.y_bearing()).max(0.0); let mut descent = (sep_extents.height() + sep_extents.y_bearing()).max(0.0); - for extents in pieces.iter().filter_map(|piece| piece.extents.as_ref()) { + let piece_extents = pieces.iter().filter_map(|piece| piece.extents.as_ref()); + for extents in piece_extents { ascent = ascent.max(-extents.y_bearing()); descent = descent.max(extents.height() + extents.y_bearing()); } @@ -48,7 +49,7 @@ pub(super) struct StatusBarMeasurement { /// cluster pieces (the cluster cannot re-wrap the way the M0 suffix could). pub(super) fn measure_status_bar( engine: &UiTextEngine, - style: &crate::config::StatusBarStyle, + style: &StatusBarStyle, prefix_text: &str, cluster_width: f64, cluster_line_height: f64, @@ -210,5 +211,20 @@ pub(super) fn layout_mode_badges( } badges } +use crate::config::StatusBarStyle; + +use super::super::badges::{ + EDITING_BADGE_FONT_SIZE, EDITING_BADGE_HINT_FONT_SIZE, EDITING_BADGE_TINT, + FROZEN_BADGE_FONT_SIZE, FROZEN_BADGE_TINT, PAN_BADGE_FONT_SIZE, PAN_BADGE_TINT, + ZOOM_BADGE_FONT_SIZE, ZOOM_BADGE_TINT, editing_badge_hint, editing_badge_label, + frozen_badge_label, pan_badge_label, zoom_badge_label, +}; use super::content::{StatusHudPiece, status_text_style}; -use super::*; +use super::{ + MIN_INTERACTIVE_HEIGHT, MIN_PREFIX_BUDGET_FRACTION, SEGMENT_SEPARATOR, + STATUS_BAR_MAX_WIDTH_FRACTION, StatusHudBadge, +}; +use crate::config::StatusPosition; +use crate::input::{DrawingState, InputState}; +use crate::ui::primitives::{BADGE_STACK_GAP, measure_badge_with_engine}; +use crate::ui_text::{UiTextEngine, UiTextExtents}; diff --git a/src/ui/status/bar/render.rs b/src/ui/status/bar/render.rs index 0c46a220f..b342638d6 100644 --- a/src/ui/status/bar/render.rs +++ b/src/ui/status/bar/render.rs @@ -1,5 +1,12 @@ +use crate::config::StatusBarStyle; + use super::content::status_text_style; -use super::*; +use super::{STATUS_BAR_CORNER_RADIUS, StatusHudRun}; +use crate::input::{BoardBackground, InputState}; +use crate::ui::primitives::{BadgeAlign, draw_badge_with_engine, draw_pill, draw_rounded_rect}; +use crate::ui::theme; +use crate::ui_text::UiTextEngine; +use std::f64::consts::PI; // ============================================================================ // Rendering @@ -10,7 +17,7 @@ use super::*; pub fn render_status_bar( ctx: &cairo::Context, input_state: &InputState, - style: &crate::config::StatusBarStyle, + style: &StatusBarStyle, screen_width: u32, screen_height: u32, ) { @@ -33,7 +40,7 @@ pub fn render_status_bar_with_theme( ctx: &cairo::Context, theme: &theme::Theme, input_state: &InputState, - style: &crate::config::StatusBarStyle, + style: &StatusBarStyle, screen_width: u32, screen_height: u32, ) { @@ -53,7 +60,7 @@ pub(crate) fn render_status_bar_with_resources( ctx: &cairo::Context, theme: &theme::Theme, input_state: &InputState, - style: &crate::config::StatusBarStyle, + style: &StatusBarStyle, screen_width: u32, screen_height: u32, ) { diff --git a/src/ui/status/bar/tests.rs b/src/ui/status/bar/tests.rs index 29e5db0c6..f01c62022 100644 --- a/src/ui/status/bar/tests.rs +++ b/src/ui/status/bar/tests.rs @@ -1,3 +1,5 @@ +use crate::input::state::test_support; + use super::*; use crate::config::{StatusBarItem, StatusBarStyle}; use crate::draw::{Color, Shape}; @@ -12,7 +14,7 @@ const CLUSTER_LINE_HEIGHT: f64 = 21.0; const DOT_DIAMETER: f64 = 12.0; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } fn measure( diff --git a/src/ui/status/bar/tests/width_budget.rs b/src/ui/status/bar/tests/width_budget.rs index 787c07edb..1b1194e84 100644 --- a/src/ui/status/bar/tests/width_budget.rs +++ b/src/ui/status/bar/tests/width_budget.rs @@ -1,3 +1,6 @@ +use crate::draw::TextMeasurer; +use crate::util; + use super::*; #[test] @@ -48,7 +51,7 @@ fn each_core_content_flag_removes_only_its_segment() { state.ui_visibility.show_status_about = true; assert!(state.set_status_bar_item_visible_with_resources( &UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), item, false )); @@ -87,35 +90,32 @@ fn prefix_content_keeps_output_before_selection_and_honors_both_flags() { state.set_selection(vec![shape_id]); assert_eq!( - build_prefix_text(&state, &crate::draw::TextMeasurer::default()).as_deref(), + build_prefix_text(&state, &TextMeasurer::default()).as_deref(), Some("Output: DP-3 · 34×44px") ); state.set_status_bar_item_visible_with_resources( &UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), StatusBarItem::ActiveOutput, false, ); assert_eq!( - build_prefix_text(&state, &crate::draw::TextMeasurer::default()).as_deref(), + build_prefix_text(&state, &TextMeasurer::default()).as_deref(), Some("34×44px") ); state.set_status_bar_item_visible_with_resources( &UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), StatusBarItem::SelectionInfo, false, ); - assert_eq!( - build_prefix_text(&state, &crate::draw::TextMeasurer::default()), - None - ); + assert_eq!(build_prefix_text(&state, &TextMeasurer::default()), None); } #[test] fn output_item_needs_two_outputs_unless_always_is_set() { - let measurer = crate::draw::TextMeasurer::default(); + let measurer = TextMeasurer::default(); let mut state = make_state(); state.ui_visibility.show_active_output_badge = true; assert!(state.set_active_output_label(Some("WAYLAND-1".to_string()))); @@ -180,7 +180,7 @@ fn context_indicator_flag_gates_transient_status_text() { state.set_status_bar_item_visible_with_resources( &UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), StatusBarItem::ContextIndicators, false, ); @@ -197,14 +197,14 @@ fn shedding_the_last_optional_piece_does_not_leave_an_empty_pill() { for item in StatusBarItem::ALL { state.set_status_bar_item_visible_with_resources( &UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), item, false, ); } state.set_status_bar_item_visible_with_resources( &UiTextEngine::default(), - &crate::draw::TextMeasurer::default(), + &TextMeasurer::default(), StatusBarItem::About, true, ); @@ -322,7 +322,7 @@ fn width_budget_degrades_board_label_then_drops_help() { first_text(&wide), format!( "{} {}/{}", - crate::util::truncate_with_ellipsis(LONG_BOARD_NAME, BOARD_NAME_MAX_CHARS), + util::truncate_with_ellipsis(LONG_BOARD_NAME, BOARD_NAME_MAX_CHARS), index, count ) @@ -427,7 +427,7 @@ fn the_size_segment_follows_the_tool() { state.set_tool_override(Some(Tool::Pen)); state.style.current_font_size = 32.0; - state.state = crate::input::DrawingState::text_input(10, 10, String::new()); + state.state = DrawingState::text_input(10, 10, String::new()); assert_eq!( segment_text(&state, StatusHudSegmentKind::Size).as_deref(), Some("32px") diff --git a/src/ui/status/mod.rs b/src/ui/status/mod.rs index f63c84d44..6121366c0 100644 --- a/src/ui/status/mod.rs +++ b/src/ui/status/mod.rs @@ -2,8 +2,10 @@ mod badges; mod bar; mod zoom_chip; -pub use badges::render_pan_badge; -pub use badges::{render_editing_badge, render_frozen_badge, render_page_badge, render_zoom_badge}; +pub use badges::{ + render_editing_badge, render_frozen_badge, render_page_badge, render_pan_badge, + render_zoom_badge, +}; pub use bar::{ StatusHudLayout, StatusHudSegmentKind, compute_status_hud_layout, render_status_bar, render_status_bar_with_theme, status_hud_geometry, diff --git a/src/ui/status/tests.rs b/src/ui/status/tests.rs index 6a0403665..fd977c2c5 100644 --- a/src/ui/status/tests.rs +++ b/src/ui/status/tests.rs @@ -1,3 +1,6 @@ +use crate::draw::TextMeasurer; +use crate::input::state::test_support; + use super::*; use crate::config::{StatusBarStyle, StatusPosition}; use crate::input::InputState; @@ -5,13 +8,13 @@ use crate::ui::theme::Theme; use crate::ui_text::UiTextEngine; fn state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } fn update(engine: &UiTextEngine, state: &mut InputState, style: &StatusBarStyle, focused: bool) { state.update_status_hud_layout_for_pointer_with_resources( crate::input::state::InputTextResources { - measurer: &crate::draw::TextMeasurer::default(), + measurer: &TextMeasurer::default(), ui_engine: engine, }, StatusPosition::BottomLeft, diff --git a/src/ui/status/zoom_chip.rs b/src/ui/status/zoom_chip.rs index c0fffc44f..7c287f8a3 100644 --- a/src/ui/status/zoom_chip.rs +++ b/src/ui/status/zoom_chip.rs @@ -115,6 +115,7 @@ pub enum ZoomChipPress { impl ZoomChipPress { /// True for a press that landed inside the chip (`Passive` or `Button`) and /// whose release the chip must therefore consume; false for `None`. + #[must_use] pub fn is_pending(self) -> bool { !matches!(self, ZoomChipPress::None) } @@ -729,11 +730,15 @@ fn draw_zoom_sign(ctx: &cairo::Context, m: &ZoomGlyphMetrics, run_x: f64, plus: #[cfg(test)] mod tests { + use crate::config::StatusPosition; + use crate::input::state::test_support; + use crate::ui; + use super::*; use crate::config::StatusBarStyle; fn make_state() -> InputState { - crate::input::state::test_support::make_test_input_state() + test_support::make_test_input_state() } /// The chip anchors to the bottom-right corner: its right/bottom edges sit @@ -1013,14 +1018,9 @@ mod tests { // And the status HUD suppresses its ZOOM badge in this config, so the // chip is the sole zoom % indicator (never two). - let hud = crate::ui::compute_status_hud_layout( - &state, - crate::config::StatusPosition::BottomLeft, - &style, - 1920, - 1080, - ) - .expect("hud layout"); + let hud = + ui::compute_status_hud_layout(&state, StatusPosition::BottomLeft, &style, 1920, 1080) + .expect("hud layout"); assert!( !hud.badges.iter().any(|badge| badge.label.contains("ZOOM")), "the HUD ZOOM badge yields to the chip when zoom actions are on" @@ -1062,9 +1062,9 @@ mod tests { // Status-HUD ZOOM badge: only rendered while the bar is // shown, and only present when the chip does not own it. let hud_badge = show_status_bar - && crate::ui::compute_status_hud_layout( + && ui::compute_status_hud_layout( &state, - crate::config::StatusPosition::BottomLeft, + StatusPosition::BottomLeft, &style, w, h, @@ -1104,7 +1104,7 @@ mod tests { state.ui_visibility.show_status_bar = true; // The damage collector refreshes the status HUD layout before the chip // each frame; cache it by hand here at the bottom-right corner. - state.update_status_hud_layout(crate::config::StatusPosition::BottomRight, &style, w, h); + state.update_status_hud_layout(StatusPosition::BottomRight, &style, w, h); let status = state.status_hud_layout().expect("status layout").clone(); let chip = compute_zoom_chip_layout(&state, &style, w, h).expect("chip layout"); @@ -1139,7 +1139,7 @@ mod tests { let style = StatusBarStyle::default(); let (w, h) = (1920_u32, 1080_u32); state.ui_visibility.show_status_bar = true; - state.update_status_hud_layout(crate::config::StatusPosition::BottomLeft, &style, w, h); + state.update_status_hud_layout(StatusPosition::BottomLeft, &style, w, h); let chip = compute_zoom_chip_layout(&state, &style, w, h).expect("chip layout"); assert!( diff --git a/src/ui/tests/theme_compatibility.rs b/src/ui/tests/theme_compatibility.rs index 7b52bc03a..5878904fc 100644 --- a/src/ui/tests/theme_compatibility.rs +++ b/src/ui/tests/theme_compatibility.rs @@ -1,13 +1,18 @@ +use std::env; +use std::process::Command; + use crate::config::{StatusBarStyle, StatusPosition}; use crate::input::InputState; +use crate::input::state::test_support; use crate::ui::theme::{Theme, ThemeMode}; use crate::ui::*; +use crate::ui_text::UiTextEngine; const WIDTH: u32 = 800; const HEIGHT: u32 = 600; fn state() -> InputState { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let style = StatusBarStyle::default(); state.set_zoom_status(true, true, 2.0, (0.0, 0.0)); state.update_status_hud_layout(StatusPosition::BottomLeft, &style, WIDTH, HEIGHT); @@ -74,8 +79,8 @@ fn explicit_status_and_zoom_theme_changes_pixels_without_changing_geometry() { #[test] fn legacy_light_wrappers_match_explicit_light_in_isolated_process() { const CHILD: &str = "WAYSCRIBER_TEST_LEGACY_LIGHT_THEME"; - if std::env::var_os(CHILD).is_none() { - let output = std::process::Command::new(std::env::current_exe().unwrap()) + if env::var_os(CHILD).is_none() { + let output = Command::new(env::current_exe().unwrap()) .args(["--exact", "ui::theme_compatibility::legacy_light_wrappers_match_explicit_light_in_isolated_process", "--nocapture"]) .env(CHILD, "1") .output().unwrap(); @@ -89,7 +94,7 @@ fn legacy_light_wrappers_match_explicit_light_in_isolated_process() { } // Invisible calls used to return before consulting the process theme. // Keep that behavior so a later configured Light theme can still install. - let hidden = crate::input::state::test_support::make_test_input_state(); + let hidden = test_support::make_test_input_state(); let hidden_style = StatusBarStyle::default(); pixels(|ctx| { render_status_bar(ctx, &hidden, &hidden_style, WIDTH, HEIGHT); @@ -123,7 +128,7 @@ fn legacy_light_wrappers_match_explicit_light_in_isolated_process() { state.open_radial_menu(400.0, 300.0); state.update_radial_menu_layout(WIDTH, HEIGHT); let legacy = pixels(|ctx| render_radial_menu(ctx, &state, WIDTH, HEIGHT)); - let engine = crate::ui_text::UiTextEngine::default(); + let engine = UiTextEngine::default(); let explicit = |theme: &Theme| { pixels(|ctx| { let mut caches = UiRenderCaches::default(); diff --git a/src/ui/text_highlight.rs b/src/ui/text_highlight.rs index 2ad5d0849..136ca07ea 100644 --- a/src/ui/text_highlight.rs +++ b/src/ui/text_highlight.rs @@ -20,8 +20,8 @@ pub(crate) fn find_match_range(haystack: &str, needle_lower: &str) -> Option<(us .map(|start| (start, start + needle_lower.len())) } -pub(crate) struct HighlightStyle<'a> { - pub(crate) font_family: &'a str, +pub(crate) struct HighlightStyle<'font> { + pub(crate) font_family: &'font str, pub(crate) font_size: f64, pub(crate) font_weight: cairo::FontWeight, pub(crate) color: [f64; 4], diff --git a/src/ui/theme.rs b/src/ui/theme.rs index c931747a3..b42bf885a 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -28,6 +28,9 @@ pub mod swatch; use std::sync::OnceLock; +use crate::config::UiTheme; +use crate::ui::theme; + /// RGB color tuple (0.0–1.0 channels). pub type Rgb = (f64, f64, f64); @@ -35,6 +38,7 @@ pub type Rgb = (f64, f64, f64); pub type Rgba = (f64, f64, f64, f64); /// Attach an alpha channel to an RGB root in const context. +#[must_use] pub const fn rgba(rgb: Rgb, alpha: f64) -> Rgba { (rgb.0, rgb.1, rgb.2, alpha) } @@ -330,36 +334,44 @@ pub mod overlay { // rather than constants: the translation guard reads plain string literals // and cannot see through a constant. /// Context menu navigation hint + #[must_use] pub fn nav_hint_menu() -> &'static str { tr("↑↓ to navigate • Enter to select • Esc to close") } /// Context menu navigation hint when a row opens a submenu + #[must_use] pub fn nav_hint_menu_submenus() -> &'static str { tr("↑↓ navigate • → open • Enter select • Esc close") } /// Context menu navigation hint while a submenu holds the selection + #[must_use] pub fn nav_hint_submenu() -> &'static str { tr("↑↓ navigate • ← back • Enter select • Esc close") } /// Board picker navigation hint + #[must_use] pub fn nav_hint_board_picker() -> &'static str { tr("↑↓ Navigate • Type to search") } /// Modal close hint + #[must_use] pub fn hint_press_esc() -> &'static str { tr("Press Escape to close") } // ---- Empty state messages ---- /// Properties panel empty state + #[must_use] pub fn empty_properties() -> &'static str { tr("Select a shape to view properties") } /// Command palette no results - main message + #[must_use] pub fn empty_command_palette() -> &'static str { tr("No matching commands") } /// Command palette no results - suggestions + #[must_use] pub fn empty_command_suggestions() -> &'static str { tr("Try: pen, color, undo, help") } @@ -756,6 +768,7 @@ pub struct Theme { impl Theme { /// Resolve a startup preference without reading or installing global state. + #[must_use] pub fn resolve(mode: ThemeMode) -> Self { match mode { ThemeMode::Light => Self::light(), @@ -765,6 +778,7 @@ impl Theme { /// The default OSD-dark chrome (canonical values, matching the const /// tokens above). + #[must_use] pub fn dark() -> Self { Self { surface_pill: toolbar::COLOR_PANEL_BACKGROUND, @@ -787,6 +801,7 @@ impl Theme { /// chrome. Board adaptivity is orthogonal to the installed variant /// (`ThemeMode::Auto` still resolves the overall theme to dark), so this /// is an associated helper rather than a method on the active theme. + #[must_use] pub fn status_palette_for_background(r: f64, g: f64, b: f64) -> ([f64; 4], [f64; 4]) { if relative_luminance(r, g, b) > STATUS_PALETTE_LUMINANCE_THRESHOLD { ([0.15, 0.15, 0.15, 0.85], [1.0, 1.0, 1.0, 1.0]) @@ -797,6 +812,7 @@ impl Theme { /// Light chrome variant (for light solid boards once surfaces consume /// the runtime theme). Accent/radii/spacing match dark. + #[must_use] pub fn light() -> Self { Self { surface_pill: (0.980, 0.980, 0.988, 1.0), @@ -858,46 +874,57 @@ pub fn current() -> &'static Theme { pub mod popup { use super::{Rgba, overlay}; + #[must_use] pub fn bg_context_menu() -> Rgba { overlay::PANEL_BG_CONTEXT_MENU } + #[must_use] pub fn bg_context_submenu() -> Rgba { overlay::PANEL_BG_CONTEXT_SUBMENU } + #[must_use] pub fn bg_board_picker() -> Rgba { overlay::PANEL_BG_BOARD_PICKER } + #[must_use] pub fn bg_properties() -> Rgba { overlay::PANEL_BG_PROPERTIES } + #[must_use] pub fn bg_command_palette() -> Rgba { overlay::PANEL_BG_COMMAND_PALETTE } + #[must_use] pub fn bg_modal() -> Rgba { overlay::PANEL_BG_MODAL } + #[must_use] pub fn border_context_menu() -> Rgba { overlay::BORDER_CONTEXT_MENU } + #[must_use] pub fn border_board_picker() -> Rgba { overlay::BORDER_BOARD_PICKER } + #[must_use] pub fn border_properties() -> Rgba { overlay::BORDER_PROPERTIES } + #[must_use] pub fn border_command_palette() -> Rgba { overlay::BORDER_COMMAND_PALETTE } + #[must_use] pub fn border_modal() -> Rgba { overlay::BORDER_MODAL } @@ -921,6 +948,7 @@ const CURSOR_PREVIEW_OUTLINE_LUMINANCE_THRESHOLD: f64 = 0.6; /// instead, as [`crate::draw::perceived_luminance`]. Two names because they are /// two metrics; they disagree most on green. #[inline] +#[must_use] pub fn relative_luminance(r: f64, g: f64, b: f64) -> f64 { 0.2126 * r + 0.7152 * g + 0.0722 * b } @@ -928,6 +956,7 @@ pub fn relative_luminance(r: f64, g: f64, b: f64) -> f64 { /// Auto-contrast outline for the tool-preview width dot: a dark ring on light /// dot colors, a light ring on dark ones (by Rec. 709 luminance). #[inline] +#[must_use] pub fn cursor_preview_outline(r: f64, g: f64, b: f64) -> Rgba { if relative_luminance(r, g, b) > CURSOR_PREVIEW_OUTLINE_LUMINANCE_THRESHOLD { overlay::CURSOR_PREVIEW_OUTLINE_DARK @@ -950,12 +979,14 @@ pub fn set_color_alpha(ctx: &cairo::Context, color: Rgb, alpha: f64) { /// Apply an RGBA color with modified alpha #[inline] +#[must_use] pub fn with_alpha(color: Rgba, alpha: f64) -> Rgba { (color.0, color.1, color.2, alpha) } /// Linear interpolation between two colors #[inline] +#[must_use] pub fn lerp_color(from: Rgba, to: Rgba, t: f64) -> Rgba { let t = t.clamp(0.0, 1.0); ( @@ -966,13 +997,14 @@ pub fn lerp_color(from: Rgba, to: Rgba, t: f64) -> Rgba { ) } -impl crate::config::UiTheme { +impl UiTheme { /// Maps the config value onto the runtime theme mode. - pub fn to_theme_mode(self) -> crate::ui::theme::ThemeMode { + #[must_use] + pub fn to_theme_mode(self) -> theme::ThemeMode { match self { - crate::config::UiTheme::Auto => crate::ui::theme::ThemeMode::Auto, - crate::config::UiTheme::Dark => crate::ui::theme::ThemeMode::Dark, - crate::config::UiTheme::Light => crate::ui::theme::ThemeMode::Light, + UiTheme::Auto => theme::ThemeMode::Auto, + UiTheme::Dark => theme::ThemeMode::Dark, + UiTheme::Light => theme::ThemeMode::Light, } } } diff --git a/src/ui/theme/css.rs b/src/ui/theme/css.rs index 6bdf802f9..89a27ae8d 100644 --- a/src/ui/theme/css.rs +++ b/src/ui/theme/css.rs @@ -18,6 +18,7 @@ fn channel(value: f64) -> u8 { /// Format an RGBA token as a CSS `rgba()` string with three-decimal alpha — /// the precision the generated stylesheet has always used for values routed /// through its original `rgba()` helper. +#[must_use] pub fn rgba_css(color: Rgba) -> String { format!( "rgba({}, {}, {}, {:.3})", @@ -34,6 +35,7 @@ pub fn rgba_css(color: Rgba) -> String { /// byte-identical. // TODO(theme-consolidation): fold into `rgba_css` (visual no-op, byte diff // only) once byte-stability against the pre-M1 sheet no longer matters. +#[must_use] pub fn rgba_css_compact(color: Rgba) -> String { format!( "rgba({}, {}, {}, {:.2})", @@ -46,6 +48,7 @@ pub fn rgba_css_compact(color: Rgba) -> String { /// Format a fully opaque token as a `#rrggbb` hex color (the sheet's /// shorthand for foregrounds on filled controls). +#[must_use] pub fn hex_css(color: Rgba) -> String { debug_assert!( color.3 >= 1.0, @@ -62,6 +65,7 @@ pub fn hex_css(color: Rgba) -> String { /// A token's channels with alpha forced to integer `0`, as the sheet's /// capture-suppression rules spell it: keeps box-shadow geometry identical /// while painting nothing. +#[must_use] pub fn transparent_css(color: Rgba) -> String { format!( "rgba({}, {}, {}, 0)", @@ -73,11 +77,13 @@ pub fn transparent_css(color: Rgba) -> String { /// Scale-dependent pixel size: multiply by the toolbar scale, round, and /// floor at 1px so no scaled dimension ever vanishes. +#[must_use] pub fn scaled_px(value: f64, scale: f64) -> i32 { (value * scale).round().max(1.0) as i32 } /// Scale-independent pixel size (hairlines and fixed chrome details). +#[must_use] pub fn fixed_px(value: f64) -> i32 { value.round() as i32 } @@ -182,6 +188,7 @@ impl GtkStylesheetValues { /// Build the full value set at the given toolbar scale. Non-finite /// scales fall back to 1.0; finite scales clamp to the 0.5–3.0 range /// the sheet has always supported. + #[must_use] pub fn new(scale: f64) -> Self { let scale = if scale.is_finite() { scale.clamp(0.5, 3.0) diff --git a/src/ui/theme/swatch.rs b/src/ui/theme/swatch.rs index a26ad3900..6914c49ba 100644 --- a/src/ui/theme/swatch.rs +++ b/src/ui/theme/swatch.rs @@ -28,6 +28,7 @@ pub const SWATCH_OUTLINE_WIDTH: f64 = 1.5; /// Not [`super::relative_luminance`], which weights the channels without /// linearizing them: close enough to pick light or dark chrome, but it rates /// the palette's black three times brighter than it is. +#[must_use] pub fn srgb_luminance(color: Rgb) -> f64 { fn linear(channel: f64) -> f64 { let channel = channel.clamp(0.0, 1.0); @@ -42,6 +43,7 @@ pub fn srgb_luminance(color: Rgb) -> f64 { } /// WCAG contrast ratio between two colors, from 1.0 (identical) to 21.0. +#[must_use] pub fn contrast_ratio(first: Rgb, second: Rgb) -> f64 { let first = srgb_luminance(first); let second = srgb_luminance(second); @@ -59,6 +61,7 @@ pub fn contrast_ratio(first: Rgb, second: Rgb) -> f64 { /// A translucent fill is judged as it shows over the chrome. The ring takes /// the tone that contrasts with the chrome, not with the fill: it has to be /// seen against the chrome to draw the swatch's edge. +#[must_use] pub fn swatch_contrast_outline(fill: Rgba, background: Rgb) -> Option { let alpha = fill.3.clamp(0.0, 1.0); let shown = ( @@ -81,6 +84,7 @@ pub fn swatch_contrast_outline(fill: Rgba, background: Rgb) -> Option { /// Edge stroke for a swatch: the contrast ring when the fill needs one, /// otherwise the surface's own quiet `hairline` at `hairline_width`. +#[must_use] pub fn swatch_edge_stroke( fill: Rgba, background: Rgb, @@ -94,12 +98,15 @@ pub fn swatch_edge_stroke( } /// The RGB of a chrome surface token, for use as a swatch `background`. +#[must_use] pub const fn chrome_rgb(surface: Rgba) -> Rgb { (surface.0, surface.1, surface.2) } #[cfg(test)] mod tests { + use crate::domain::Color; + use super::*; use crate::domain::color::{ PALETTE_BLACK, PALETTE_BLUE, PALETTE_GREEN, PALETTE_ORANGE, PALETTE_PINK, PALETTE_RED, @@ -107,7 +114,7 @@ mod tests { }; use crate::ui::theme::{Theme, toolbar}; - fn rgba(color: crate::domain::Color) -> Rgba { + fn rgba(color: Color) -> Rgba { (color.r, color.g, color.b, color.a) } diff --git a/src/ui/toasts.rs b/src/ui/toasts.rs index 0a186eb64..eb3f704da 100644 --- a/src/ui/toasts.rs +++ b/src/ui/toasts.rs @@ -155,6 +155,7 @@ pub(crate) fn preset_toast_geometry_with_engine( } /// The four screen-edge strips flashed by blocked-action feedback. +#[must_use] pub fn blocked_feedback_rects(screen_width: u32, screen_height: u32) -> [(f64, f64, f64, f64); 4] { let w = screen_width as f64; let h = screen_height as f64; @@ -408,6 +409,9 @@ pub fn render_blocked_feedback( #[cfg(test)] mod tests { + use crate::input::state::test_support; + use crate::ui; + use super::*; use crate::domain::Action; use crate::input::state::{Toast, ToastPriority}; @@ -416,7 +420,7 @@ mod tests { fn ui_toast_uses_a_short_fixed_end_fade() { // The fade helpers read the process-wide motion flag; hold the // override guard so parallel reduced-motion tests cannot flip it. - let _motion = crate::ui::anim::override_motion_for_test(true); + let _motion = ui::anim::override_motion_for_test(true); assert_eq!(ui_toast_fade(0.0, 5.0), 1.0); assert_eq!(ui_toast_fade(4.8, 5.0), 1.0); assert!((ui_toast_fade(4.9, 5.0) - 0.5).abs() < 1e-9); @@ -425,7 +429,7 @@ mod tests { #[test] fn preset_toast_keeps_its_proportional_fade() { - let _motion = crate::ui::anim::override_motion_for_test(true); + let _motion = ui::anim::override_motion_for_test(true); assert_eq!(preset_toast_fade(0.0), 1.0); assert_eq!(preset_toast_fade(PRESET_TOAST_HOLD_RATIO), 1.0); assert_eq!(preset_toast_fade(0.875), 0.5); @@ -434,7 +438,7 @@ mod tests { #[test] fn two_action_layout_keeps_chips_whole_and_ellipsizes_the_message() { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.push_toast( ToastPriority::Hint, "tip", diff --git a/src/ui/toasts/layout.rs b/src/ui/toasts/layout.rs index 7e78b6c64..c6dccc7ba 100644 --- a/src/ui/toasts/layout.rs +++ b/src/ui/toasts/layout.rs @@ -1,6 +1,12 @@ //! Toast text measurement, ellipsis and action geometry. -use super::*; +use super::{ + TOAST_ACTION_GAP, TOAST_ACTION_PADDING_X, TOAST_ACTION_PADDING_Y, TOAST_PADDING_X, + TOAST_PADDING_Y, TOAST_SCREEN_MARGIN, ToastBounds, UI_TOAST_FONT_SIZE, UI_TOAST_Y_RATIO, + toast_text_style, +}; +use crate::input::InputState; +use crate::ui_text::UiTextEngine; #[derive(Debug)] pub(super) struct UiToastLayout { diff --git a/src/ui/toasts/tests/engine.rs b/src/ui/toasts/tests/engine.rs index 46296c415..f3bd85342 100644 --- a/src/ui/toasts/tests/engine.rs +++ b/src/ui/toasts/tests/engine.rs @@ -1,3 +1,8 @@ +use std::path::Path; +use std::time::Duration; + +use crate::input::state::test_support; + use super::*; fn pixels(density: i32, paint: impl FnOnce(&cairo::Context)) -> Vec { @@ -15,7 +20,7 @@ fn pixels(density: i32, paint: impl FnOnce(&cairo::Context)) -> Vec { #[test] fn retained_toast_owner_matches_standalone_across_targets() { let engine = UiTextEngine::default(); - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); let mut toast = Toast::info("Saved — Καλημέρα 你好"); toast.duration_ms = 60_000; state.push_toast(ToastPriority::Hint, "paint", toast); @@ -23,7 +28,7 @@ fn retained_toast_owner_matches_standalone_across_targets() { state.preset_slots.set_feedback( 1, PresetFeedbackKind::Save, - Instant::now() + std::time::Duration::from_secs(60), + Instant::now() + Duration::from_secs(60), ); let bounds = ui_toast_geometry_with_engine(&engine, &state, 640, 480).unwrap(); let preset = preset_toast_geometry_with_engine(&engine, &state, 640, 480).unwrap(); @@ -59,7 +64,7 @@ fn retained_toast_owner_matches_standalone_across_targets() { fn explicit_toast_action_layout_handoff_preserves_release_target() { use crate::input::state::ToastCommand; let engine = UiTextEngine::default(); - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.push_toast( ToastPriority::Hint, "actions", @@ -96,9 +101,9 @@ fn explicit_toast_action_layout_handoff_preserves_release_target() { #[test] fn capture_toast_fits_its_message_and_both_buttons_on_a_1080p_output() { let engine = UiTextEngine::default(); - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.set_capture_feedback( - Some(std::path::Path::new( + Some(Path::new( "/very/long/absolute/path/Pictures/Wayscriber/screenshot_2026-09-25_213231.png", )), true, diff --git a/src/ui/toolbar/bindings.rs b/src/ui/toolbar/bindings.rs index 65dff527a..803c780b8 100644 --- a/src/ui/toolbar/bindings.rs +++ b/src/ui/toolbar/bindings.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::config::{Action, action_meta_iter}; +use crate::config::{Action, QuickColorPalette, action_meta_iter}; use crate::input::{InputState, Tool}; use crate::label_format::join_binding_labels; @@ -17,10 +17,12 @@ pub struct ToolbarBindingHints { } impl ToolbarBindingHints { + #[must_use] pub fn for_tool(&self, tool: Tool) -> Option<&str> { action_for_tool(tool).and_then(|action| self.binding_for_action(action)) } + #[must_use] pub fn badge_for_tool(&self, tool: Tool) -> Option<&str> { action_for_tool(tool).and_then(|action| self.badge_for_action(action)) } @@ -28,7 +30,8 @@ impl ToolbarBindingHints { pub fn from_input_state(state: &InputState) -> Self { let mut bindings = HashMap::new(); let mut badges = HashMap::new(); - for meta in action_meta_iter().filter(|meta| meta.in_toolbar) { + let toolbar_actions = action_meta_iter().filter(|meta| meta.in_toolbar); + for meta in toolbar_actions { let labels = state.action_binding_labels(meta.action); if let Some(label) = join_binding_labels(&labels) { bindings.insert(meta.action, label); @@ -48,27 +51,32 @@ impl ToolbarBindingHints { self.badges.get(&action).map(String::as_str) } + #[must_use] pub fn quick_color_badge(&self, index: usize) -> Option<&str> { - crate::config::QuickColorPalette::action_for_index(index) - .and_then(|action| self.badge_for_action(action)) + QuickColorPalette::action_for_index(index).and_then(|action| self.badge_for_action(action)) } + #[must_use] pub fn apply_preset(&self, slot: usize) -> Option<&str> { action_for_apply_preset(slot).and_then(|action| self.binding_for_action(action)) } + #[must_use] pub fn save_preset(&self, slot: usize) -> Option<&str> { action_for_save_preset(slot).and_then(|action| self.binding_for_action(action)) } + #[must_use] pub fn clear_preset(&self, slot: usize) -> Option<&str> { action_for_clear_preset(slot).and_then(|action| self.binding_for_action(action)) } + #[must_use] pub fn binding_for_event(&self, event: &ToolbarEvent) -> Option<&str> { action_for_event(event).and_then(|action| self.binding_for_action(action)) } + #[must_use] pub fn badge_for_event(&self, event: &ToolbarEvent) -> Option<&str> { action_for_event(event).and_then(|action| self.badge_for_action(action)) } diff --git a/src/ui/toolbar/events.rs b/src/ui/toolbar/events.rs index cbdc90b2c..5b56454c2 100644 --- a/src/ui/toolbar/events.rs +++ b/src/ui/toolbar/events.rs @@ -1,11 +1,11 @@ use std::path::PathBuf; use crate::config::{ - Action, StatusBarItem, ToolbarItemId, ToolbarItemOrderGroup, ToolbarLayoutMode, + Action, StatusBarItem, ToolbarItemId, ToolbarItemOrderGroup, ToolbarLayoutMode, TopDisplayMode, }; -use crate::draw::{Color, FontDescriptor}; +use crate::draw::{ArrowStyle, Color, FontDescriptor}; use crate::i18n::tr; -use crate::input::{EraserMode, Tool}; +use crate::input::{EraserMode, SelectionPropertyKind, Tool}; use super::ToolbarSnapshot; @@ -21,6 +21,7 @@ pub enum ToolbarItemCustomizeGroup { } impl ToolbarItemCustomizeGroup { + #[must_use] pub fn label(self) -> &'static str { match self { Self::TopTools => tr("Top tools"), @@ -46,6 +47,7 @@ pub enum PrecisionEntryTarget { } impl PrecisionEntryTarget { + #[must_use] pub fn label(self) -> &'static str { match self { Self::Thickness => tr("Thickness"), @@ -53,6 +55,7 @@ impl PrecisionEntryTarget { } } + #[must_use] pub fn unit(self) -> &'static str { match self { Self::Thickness => "px", @@ -112,7 +115,7 @@ pub enum ToolbarEvent { CycleArrowStyle, /// Use `style` for the next arrow; chosen from the arrow style menu, /// which it closes. - SetArrowStyle(crate::draw::ArrowStyle), + SetArrowStyle(ArrowStyle), ResetArrowLabelCounter, ResetStepMarkerCounter, SetUndoDelay(f64), @@ -225,7 +228,7 @@ pub enum ToolbarEvent { SetTopMinimized(bool), /// Set the top strip's display form (full strip / micro chip / hidden). /// The micro chip's click emits `SetTopDisplayMode(Full)`. - SetTopDisplayMode(crate::config::TopDisplayMode), + SetTopDisplayMode(TopDisplayMode), /// Deprecated alias for `SetTopMinimized(true)`; kept so external /// callers and old code paths keep working. #[allow(dead_code)] @@ -261,7 +264,7 @@ pub enum ToolbarEvent { /// as the overlay properties popup; cycle controls use `direction = 1`, /// steppers -1/+1. AdjustSelectionProperty { - kind: crate::input::SelectionPropertyKind, + kind: SelectionPropertyKind, direction: i32, }, /// Pick a color from the displayed desktop image. @@ -356,12 +359,14 @@ pub enum ToolbarEvent { } impl ToolbarEvent { + #[must_use] pub fn action(&self) -> Option { super::model::action_for_event(self) } /// Events that permanently discard user content; rendered with the /// destructive (red-accent) button treatment. + #[must_use] pub fn is_destructive(&self) -> bool { matches!( self, @@ -378,6 +383,7 @@ impl ToolbarEvent { ) } + #[must_use] pub fn short_label(&self, snapshot: &ToolbarSnapshot, fallback: &'static str) -> &'static str { super::model::short_label_for_event( self, @@ -387,6 +393,7 @@ impl ToolbarEvent { ) } + #[must_use] pub fn tooltip_label( &self, snapshot: &ToolbarSnapshot, diff --git a/src/ui/toolbar/model/activation.rs b/src/ui/toolbar/model/activation.rs index 43a09900d..1ef58344e 100644 --- a/src/ui/toolbar/model/activation.rs +++ b/src/ui/toolbar/model/activation.rs @@ -1,4 +1,5 @@ use crate::domain::{MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; +use crate::draw; use super::super::ToolbarEvent; @@ -172,9 +173,9 @@ impl ToolbarSliderSpec { ticks: &[], }; pub(crate) const SPOTLIGHT_MAGNIFICATION: Self = Self { - min: crate::draw::MIN_SPOTLIGHT_MAGNIFICATION, - max: crate::draw::MAX_SPOTLIGHT_MAGNIFICATION, - step: Some(crate::draw::SPOTLIGHT_MAGNIFICATION_STEP), + min: draw::MIN_SPOTLIGHT_MAGNIFICATION, + max: draw::MAX_SPOTLIGHT_MAGNIFICATION, + step: Some(draw::SPOTLIGHT_MAGNIFICATION_STEP), snap_to_step: true, curve: SliderCurve::Linear, ticks: &[], diff --git a/src/ui/toolbar/model/control.rs b/src/ui/toolbar/model/control.rs index faef592e3..70afc431a 100644 --- a/src/ui/toolbar/model/control.rs +++ b/src/ui/toolbar/model/control.rs @@ -10,6 +10,7 @@ use std::collections::HashSet; use crate::config::Action; use crate::draw::Color; +use crate::label_format; use super::super::ToolbarEvent; use super::activation::ToolbarControlId; @@ -123,7 +124,7 @@ impl ToolbarTooltip { match self { Self::None => None, Self::Text(text) => Some(text.to_string()), - Self::Binding { label, binding } => Some(crate::label_format::format_binding_label( + Self::Binding { label, binding } => Some(label_format::format_binding_label( label, binding.as_deref(), )), diff --git a/src/ui/toolbar/model/event_policy.rs b/src/ui/toolbar/model/event_policy.rs index 03ec33fd6..4616e12d0 100644 --- a/src/ui/toolbar/model/event_policy.rs +++ b/src/ui/toolbar/model/event_policy.rs @@ -1,6 +1,6 @@ use crate::config::{ - Action, ToolbarItemId, ToolbarItemOrderGroup, ToolbarItemVisibilitySetting, ToolbarSectionFlag, - action_label, action_short_label, section_flag_for_item, + Action, StatusBarItem, ToolbarItemId, ToolbarItemOrderGroup, ToolbarItemVisibilitySetting, + ToolbarSectionFlag, action_label, action_short_label, section_flag_for_item, }; use crate::i18n::tr; use crate::input::Tool; @@ -85,7 +85,7 @@ pub(crate) enum ToolbarPersistence { pub(crate) enum ToolbarRuntimeUiPersistenceTarget { TopPinned, TopMinimized, - NamedSection(crate::config::ToolbarSectionFlag), + NamedSection(ToolbarSectionFlag), LayoutMode, ClickHighlight, /// Keyboard-only chrome: no toolbar control routes to these, but they @@ -98,7 +98,7 @@ pub(crate) enum ToolbarRuntimeUiPersistenceTarget { }, ItemOrder(ToolbarItemOrderGroup), StatusBarInteractive, - StatusBarItem(crate::config::StatusBarItem), + StatusBarItem(StatusBarItem), StatusBar, StatusBoardBadge, StatusPageBadge, @@ -889,6 +889,8 @@ mod tests { #[cfg(test)] mod popover_affinity_tests { + use crate::draw::ArrowStyle; + use super::*; /// Every event that starts an item drag must carry the order group its @@ -1053,7 +1055,7 @@ mod popover_affinity_tests { &[P::ArrowStyleMenu] ); for event in [ - ToolbarEvent::SetArrowStyle(crate::draw::ArrowStyle::Pointy), + ToolbarEvent::SetArrowStyle(ArrowStyle::Pointy), ToolbarEvent::CycleArrowStyle, ToolbarEvent::TogglePenFeelPanel(true), ] { @@ -1067,9 +1069,7 @@ mod popover_affinity_tests { ToolbarPersistence::Ephemeral ); assert_eq!( - persistence_for_event(&ToolbarEvent::SetArrowStyle( - crate::draw::ArrowStyle::Curved - )), + persistence_for_event(&ToolbarEvent::SetArrowStyle(ArrowStyle::Curved)), persistence_for_event(&ToolbarEvent::CycleArrowStyle), "picking a style persists like cycling to it" ); diff --git a/src/ui/toolbar/model/header.rs b/src/ui/toolbar/model/header.rs index 8878d8873..dfae62df0 100644 --- a/src/ui/toolbar/model/header.rs +++ b/src/ui/toolbar/model/header.rs @@ -143,6 +143,9 @@ fn segmented_control( #[cfg(test)] mod tests { + use crate::domain::Language; + use crate::{i18n, test_env}; + use super::*; #[test] @@ -167,14 +170,14 @@ mod tests { #[test] fn layout_ids_stay_english_and_unique_when_the_labels_translate() { - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::layout_ids_stay_english_and_unique_when_the_labels_translate" ), || { let english = ToolbarLayoutMode::ALL.map(layout_mode_label); - crate::i18n::set_language(crate::domain::Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); let chinese = ToolbarLayoutMode::ALL.map(layout_mode_label); assert_eq!( diff --git a/src/ui/toolbar/model/mod.rs b/src/ui/toolbar/model/mod.rs index cb10e1e4d..519dfee34 100644 --- a/src/ui/toolbar/model/mod.rs +++ b/src/ui/toolbar/model/mod.rs @@ -78,6 +78,11 @@ pub(crate) use top_spec::{ #[cfg(test)] mod tests { + use std::path::PathBuf; + + use crate::config::{ToolbarItemId, ToolbarItemOrderGroup, ToolbarSectionFlag}; + use crate::ui::toolbar::ToolbarItemCustomizeGroup; + use super::*; use crate::config::{ ToolbarGroupId, ToolbarItemOrderConfig, ToolbarItemsConfig, ToolbarLayoutMode, @@ -207,10 +212,10 @@ mod tests { #[test] fn settings_model_moves_hidden_item_overrides_into_customization_panel() { let mut snapshot = snapshot(); - snapshot.resolved_toolbar_items = crate::config::ToolbarItemsConfig { + snapshot.resolved_toolbar_items = ToolbarItemsConfig { hidden: vec![ids::TOP_TOOL_PEN.as_str().to_string()], shown: Vec::new(), - order: crate::config::ToolbarItemOrderConfig::default(), + order: ToolbarItemOrderConfig::default(), } .resolved(); @@ -239,8 +244,7 @@ mod tests { .any(|group| group.label.as_ref() == "Top tools") ); - snapshot.customize_items_group = - Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopTools); + snapshot.customize_items_group = Some(ToolbarItemCustomizeGroup::TopTools); let model = ToolbarSettingsModel::for_popover(&snapshot).expect("settings"); assert!(model.groups().is_empty()); assert!( @@ -292,7 +296,7 @@ mod tests { assert!(has_visibility_reset_button(&snapshot)); let mut section_only = ToolbarItemsConfig::default(); - section_only.set_hidden(crate::config::ToolbarSectionFlag::Actions.item_id(), false); + section_only.set_hidden(ToolbarSectionFlag::Actions.item_id(), false); snapshot.resolved_toolbar_items = section_only.resolved(); assert!(!has_visibility_reset_button(&snapshot)); @@ -347,7 +351,7 @@ mod tests { #[test] fn factory_visibility_reset_button_uses_unambiguous_wording() { let mut snapshot = snapshot(); - let mut items = crate::config::ToolbarItemsConfig::default(); + let mut items = ToolbarItemsConfig::default(); items.set_hidden(ids::TOP_TOOL_PEN, true); snapshot.resolved_toolbar_items = items.resolved(); @@ -369,16 +373,11 @@ mod tests { #[test] fn factory_order_reset_button_uses_unambiguous_wording() { let mut snapshot = snapshot(); - let mut items = crate::config::ToolbarItemsConfig::default(); - assert!(items.move_item_to_index( - crate::config::ToolbarItemOrderGroup::TopTools, - ids::TOP_TOOL_PEN, - 5, - )); + let mut items = ToolbarItemsConfig::default(); + assert!(items.move_item_to_index(ToolbarItemOrderGroup::TopTools, ids::TOP_TOOL_PEN, 5,)); snapshot.resolved_toolbar_items = items.resolved(); snapshot.customize_items_open = true; - snapshot.customize_items_group = - Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopTools); + snapshot.customize_items_group = Some(ToolbarItemCustomizeGroup::TopTools); let model = ToolbarSettingsModel::for_popover(&snapshot).expect("settings"); let button = model @@ -399,10 +398,10 @@ mod tests { // Paths live behind the "Details" disclosure; expanded, they stay // complete. snapshot.settings_details_open = true; - let runtime_path = std::path::PathBuf::from( + let runtime_path = PathBuf::from( "/a/very/long/runtime/state/location/whose/complete/path/must/remain/visible/runtime-ui.toml", ); - let artifact_path = std::path::PathBuf::from( + let artifact_path = PathBuf::from( "/another/very/long/recovery/location/whose/complete/path/must/remain/visible/wayscriber-recovery.toml", ); snapshot.runtime_ui_persistence = Some(RuntimeUiPersistenceSnapshot { @@ -462,7 +461,7 @@ mod tests { ); snapshot.runtime_ui_persistence = Some(RuntimeUiPersistenceSnapshot { - path: std::path::PathBuf::from("/isolated/runtime-ui.toml"), + path: PathBuf::from("/isolated/runtime-ui.toml"), mode: RuntimeUiPersistenceMode::Unavailable, detail: Some( "writer startup failed; runtime-only toolbar and board changes are process-only" @@ -531,10 +530,10 @@ mod tests { // Hiding side.boards.picker drops the button (config plumbing), while // the rest of the boards row survives. - snapshot.resolved_toolbar_items = crate::config::ToolbarItemsConfig { + snapshot.resolved_toolbar_items = ToolbarItemsConfig { hidden: vec![ids::SIDE_BOARDS_PICKER.as_str().to_string()], shown: Vec::new(), - order: crate::config::ToolbarItemOrderConfig::default(), + order: ToolbarItemOrderConfig::default(), } .resolved(); let hidden = @@ -601,11 +600,11 @@ mod tests { #[test] fn retained_side_ids_still_control_unified_top_models() { - fn hide(snapshot: &mut ToolbarSnapshot, id: crate::config::ToolbarItemId) { - snapshot.resolved_toolbar_items = crate::config::ToolbarItemsConfig { + fn hide(snapshot: &mut ToolbarSnapshot, id: ToolbarItemId) { + snapshot.resolved_toolbar_items = ToolbarItemsConfig { hidden: vec![id.as_str().to_string()], shown: Vec::new(), - order: crate::config::ToolbarItemOrderConfig::default(), + order: ToolbarItemOrderConfig::default(), } .resolved(); } diff --git a/src/ui/toolbar/model/settings.rs b/src/ui/toolbar/model/settings.rs index 2612eed40..feb1da835 100644 --- a/src/ui/toolbar/model/settings.rs +++ b/src/ui/toolbar/model/settings.rs @@ -1,11 +1,10 @@ use std::borrow::Cow; +#[cfg(test)] +use crate::config::{Action, action_short_label}; use crate::config::{ - Action, ToolbarGroupId, ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemId, - ToolbarItemOrderConfig, ToolbarItemOrderGroup, ToolbarItemSurface, ToolbarLayoutMode, - action_label, action_short_label, factory_individual_toolbar_item_visibility_settings, - item_visibility_setting, toolbar_item_definitions, toolbar_item_ids as ids, - toolbar_item_order_group, toolbar_item_visibility_override_allowed, + StatusBarItem, ToolbarItemDefinition, ToolbarItemId, ToolbarItemOrderGroup, ToolbarLayoutMode, + toolbar_item_definitions, toolbar_item_ids as ids, }; use crate::i18n::{tr, trf}; @@ -297,59 +296,59 @@ fn status_bar_content_toggles(snapshot: &ToolbarSnapshot) -> Vec ( + StatusBarItem::ActiveOutput => ( ToolbarControlId::SettingsStatusActiveOutput, tr("Active output"), tr("Status bar: active monitor/output."), ), - crate::config::StatusBarItem::SelectionInfo => ( + StatusBarItem::SelectionInfo => ( ToolbarControlId::SettingsStatusSelectionInfo, tr("Selection size"), tr("Status bar: selected item dimensions."), ), - crate::config::StatusBarItem::Board => ( + StatusBarItem::Board => ( ToolbarControlId::SettingsStatusBoardBadge, tr("Board"), tr("Status bar: board label."), ), - crate::config::StatusBarItem::Page => ( + StatusBarItem::Page => ( ToolbarControlId::SettingsStatusPageBadge, tr("Page"), tr("Status bar: page counter."), ), - crate::config::StatusBarItem::Color => ( + StatusBarItem::Color => ( ToolbarControlId::SettingsStatusColor, tr("Current color"), tr("Status bar: active color dot."), ), - crate::config::StatusBarItem::Tool => ( + StatusBarItem::Tool => ( ToolbarControlId::SettingsStatusTool, tr("Active tool"), tr("Status bar: active tool name."), ), - crate::config::StatusBarItem::Size => ( + StatusBarItem::Size => ( ToolbarControlId::SettingsStatusSize, tr("Tool size"), tr("Status bar: active tool size."), ), - crate::config::StatusBarItem::ContextIndicators => ( + StatusBarItem::ContextIndicators => ( ToolbarControlId::SettingsStatusContextIndicators, tr("Context indicators"), tr("Status bar: text and highlight state."), ), - crate::config::StatusBarItem::ToolbarHint => ( + StatusBarItem::ToolbarHint => ( ToolbarControlId::SettingsStatusToolbarHint, tr("Toolbar hint"), tr("Status bar: recovery hint while toolbars are hidden."), ), - crate::config::StatusBarItem::Help => ( + StatusBarItem::Help => ( ToolbarControlId::SettingsStatusHelp, tr("Help shortcut"), tr("Status bar: Help shortcut."), ), - crate::config::StatusBarItem::About => ( + StatusBarItem::About => ( ToolbarControlId::SettingsStatusAbout, tr("About and version"), tr("Status bar: About/version chip."), @@ -626,6 +625,8 @@ fn push_notice( #[cfg(test)] mod tests { + use crate::config::toolbar_item_ids; + use super::*; use crate::config::StatusBarItem; use crate::input::state::test_support::make_test_input_state; @@ -666,14 +667,13 @@ mod tests { #[test] fn status_bar_contents_subpanel_exposes_every_authored_content_toggle() { let mut state = make_test_input_state(); - assert!(state.set_toolbar_item_hidden( - crate::config::toolbar_item_ids::SIDE_SETTINGS_STATUS_BOARD_BADGE, - true, - )); - assert!(state.set_toolbar_item_hidden( - crate::config::toolbar_item_ids::SIDE_SETTINGS_STATUS_PAGE_BADGE, - true, - )); + assert!( + state + .set_toolbar_item_hidden(toolbar_item_ids::SIDE_SETTINGS_STATUS_BOARD_BADGE, true,) + ); + assert!( + state.set_toolbar_item_hidden(toolbar_item_ids::SIDE_SETTINGS_STATUS_PAGE_BADGE, true,) + ); state.test_set_toolbar_customization(false, None, true); let snapshot = ToolbarSnapshot::from_input_with_bindings(&state, ToolbarBindingHints::default()); diff --git a/src/ui/toolbar/model/settings/helpers.rs b/src/ui/toolbar/model/settings/helpers.rs index 581b1703b..81e6e336f 100644 --- a/src/ui/toolbar/model/settings/helpers.rs +++ b/src/ui/toolbar/model/settings/helpers.rs @@ -1,5 +1,15 @@ -use super::*; +use super::super::activation::ToolbarControlId; +use super::super::control::{ToolbarIcon, ToolbarTooltip}; +use super::{ToolbarSettingsButton, ToolbarSettingsCustomizeGroup, ids}; +use crate::config::{ + Action, ToolbarGroupId, ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemId, + ToolbarItemOrderConfig, ToolbarItemOrderGroup, ToolbarItemSurface, action_label, + action_short_label, factory_individual_toolbar_item_visibility_settings, + item_visibility_setting, toolbar_item_order_group, toolbar_item_visibility_override_allowed, +}; use crate::i18n::tr; +use crate::ui::toolbar::{ToolbarEvent, ToolbarItemCustomizeGroup, ToolbarSnapshot}; +use std::borrow::Cow; pub(super) fn settings_buttons(snapshot: &ToolbarSnapshot) -> Vec { let mut buttons = vec![ diff --git a/src/ui/toolbar/model/style_pill.rs b/src/ui/toolbar/model/style_pill.rs index 45ac3af05..a031aebe8 100644 --- a/src/ui/toolbar/model/style_pill.rs +++ b/src/ui/toolbar/model/style_pill.rs @@ -20,15 +20,12 @@ //! route through the same apply machinery as the overlay properties //! popup, which stays available from the context menu. -use std::borrow::Cow; - -use crate::config::{ - Action, QuickColorPalette, ToolbarStrokeControls, action_label, action_short_label, - toolbar_item_ids as ids, -}; -use crate::i18n::{tr, trf}; -use crate::input::{EraserMode, SelectionPropertyEntry, SelectionPropertyKind}; -use crate::label_format::{format_binding_label, format_quick_color_tooltip}; +#[cfg(test)] +use crate::config::QuickColorPalette; +use crate::config::{ToolbarStrokeControls, toolbar_item_ids as ids}; +#[cfg(test)] +use crate::input::EraserMode; +use crate::input::{SelectionPropertyEntry, SelectionPropertyKind}; use crate::ui::toolbar::{ToolContext, ToolOptionsKind, ToolbarEvent, ToolbarSnapshot}; pub(crate) use arrow_menu::{ diff --git a/src/ui/toolbar/model/style_pill/arrow_menu.rs b/src/ui/toolbar/model/style_pill/arrow_menu.rs index dbe9cae0c..c470f8616 100644 --- a/src/ui/toolbar/model/style_pill/arrow_menu.rs +++ b/src/ui/toolbar/model/style_pill/arrow_menu.rs @@ -7,7 +7,7 @@ //! the user picks the arrow they can see in one click. The keyboard action //! still cycles. -use super::*; +use super::ToolbarEvent; use crate::draw::ArrowStyle; /// Chip geometry in spec units, shared by both frontends: its slot in the @@ -92,6 +92,9 @@ fn arrow_style_hint(style: ArrowStyle) -> &'static str { #[cfg(test)] mod tests { + use crate::domain::Language; + use crate::{i18n, test_env}; + use super::*; #[test] @@ -115,14 +118,14 @@ mod tests { #[test] fn menu_ids_stay_english_and_unique_when_the_labels_translate() { - crate::test_env::in_language_isolated_child( + test_env::in_language_isolated_child( concat!( module_path!(), "::menu_ids_stay_english_and_unique_when_the_labels_translate" ), || { let english = arrow_style_menu_entries(ArrowStyle::Standard); - crate::i18n::set_language(crate::domain::Language::SimplifiedChinese); + i18n::set_language(Language::SimplifiedChinese); let chinese = arrow_style_menu_entries(ArrowStyle::Standard); let ids: Vec = chinese.iter().map(ArrowStyleMenuEntry::id).collect(); diff --git a/src/ui/toolbar/model/style_pill/control.rs b/src/ui/toolbar/model/style_pill/control.rs index 39e8dbeca..e811c9999 100644 --- a/src/ui/toolbar/model/style_pill/control.rs +++ b/src/ui/toolbar/model/style_pill/control.rs @@ -1,4 +1,18 @@ -use super::*; +use crate::draw; +use crate::ui::toolbar::PrecisionEntryTarget; + +use super::super::ToolbarSliderSpec; +use super::slider::StylePillSlider; +use super::{ + StylePillControl, StylePillCounter, StylePillRole, StylePillSegment, selection_entry, + selection_kind_slug, +}; +use crate::config::{Action, QuickColorPalette, action_label, action_short_label}; +use crate::i18n::{tr, trf}; +use crate::input::{EraserMode, SelectionPropertyKind}; +use crate::label_format::{format_binding_label, format_quick_color_tooltip}; +use crate::ui::toolbar::{ToolContext, ToolbarEvent, ToolbarSnapshot}; +use std::borrow::Cow; impl StylePillControl { pub(crate) fn id(self) -> Cow<'static, str> { @@ -96,12 +110,10 @@ impl StylePillControl { Self::CounterReset(StylePillCounter::Arrow) => ToolbarEvent::ResetArrowLabelCounter, Self::CounterReset(StylePillCounter::Step) => ToolbarEvent::ResetStepMarkerCounter, // The numerals open the precise-entry popup on the overlay. - Self::ThicknessValue => ToolbarEvent::OpenPrecisionEntry( - crate::ui::toolbar::PrecisionEntryTarget::Thickness, - ), - Self::FontSizeValue => { - ToolbarEvent::OpenPrecisionEntry(crate::ui::toolbar::PrecisionEntryTarget::FontSize) + Self::ThicknessValue => { + ToolbarEvent::OpenPrecisionEntry(PrecisionEntryTarget::Thickness) } + Self::FontSizeValue => ToolbarEvent::OpenPrecisionEntry(PrecisionEntryTarget::FontSize), // A cycle click is a forward step through the same apply // machinery the properties popup uses. Self::SelectionCycle(kind) => { @@ -262,7 +274,7 @@ impl StylePillControl { } _ => None, }?; - if !crate::draw::spotlight_magnification_is_active(displayed_magnification) { + if !draw::spotlight_magnification_is_active(displayed_magnification) { return None; } snapshot.spotlight_magnifier_source?.unavailable_reason() diff --git a/src/ui/toolbar/model/style_pill/meter.rs b/src/ui/toolbar/model/style_pill/meter.rs index 940100b01..f8cdb20f2 100644 --- a/src/ui/toolbar/model/style_pill/meter.rs +++ b/src/ui/toolbar/model/style_pill/meter.rs @@ -12,7 +12,11 @@ //! inside the Pen feel panel (the default); [`StrokeSetting`] is the one place //! both read their range, level names, and events from. -use super::*; +use crate::config::ToolbarStrokeControls; + +use super::StylePillControl; +use crate::i18n::{tr, trf}; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; /// Logical width reserved for the zero-level dot before the bars. pub(crate) const METER_ZERO_SLOT_W: f64 = 22.0; @@ -205,7 +209,7 @@ impl StrokeSetting { /// that setting. The inline steppers send the same events but never took /// the wheel, so the stepper style keeps them that way. pub(crate) fn for_wheel(event: &ToolbarEvent, snapshot: &ToolbarSnapshot) -> Option { - if snapshot.stroke_controls == crate::config::ToolbarStrokeControls::Stepper { + if snapshot.stroke_controls == ToolbarStrokeControls::Stepper { return None; } match event { @@ -266,11 +270,13 @@ impl StylePillControl { #[cfg(test)] mod tests { + use crate::input::state::test_support; + use super::*; use crate::config::ToolbarStrokeControls; fn snapshot(smoothing: u8, sensitivity: u8) -> ToolbarSnapshot { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.set_pen_smoothing(smoothing); state.set_shape_recognition_sensitivity(sensitivity); ToolbarSnapshot::from_input(&state) diff --git a/src/ui/toolbar/model/style_pill/pen_feel.rs b/src/ui/toolbar/model/style_pill/pen_feel.rs index 141197afc..6a0a88471 100644 --- a/src/ui/toolbar/model/style_pill/pen_feel.rs +++ b/src/ui/toolbar/model/style_pill/pen_feel.rs @@ -8,7 +8,10 @@ //! meters inside reuse [`StrokeSetting::meter`], so they click and scroll //! exactly like the inline ones. -use super::*; +use super::StylePillControl; +use super::meter::{StrokeSetting, StylePillMeter}; +use crate::i18n::{tr, trf}; +use crate::ui::toolbar::{ToolContext, ToolbarSnapshot}; /// The chip's visible label. The caret says it opens something. pub(crate) fn pen_feel_chip_label() -> &'static str { @@ -193,11 +196,13 @@ impl StylePillControl { #[cfg(test)] mod tests { + use crate::input::state::test_support; + use super::*; use crate::input::Tool; fn snapshot_for(tool: Tool, smoothing: u8, sensitivity: u8) -> ToolbarSnapshot { - let mut state = crate::input::state::test_support::make_test_input_state(); + let mut state = test_support::make_test_input_state(); state.set_pen_smoothing(smoothing); state.set_shape_recognition_sensitivity(sensitivity); let mut snapshot = ToolbarSnapshot::from_input(&state); diff --git a/src/ui/toolbar/model/style_pill/slider.rs b/src/ui/toolbar/model/style_pill/slider.rs index 9ff3643e1..7c3f94764 100644 --- a/src/ui/toolbar/model/style_pill/slider.rs +++ b/src/ui/toolbar/model/style_pill/slider.rs @@ -1,3 +1,6 @@ +use crate::draw; +use crate::input::tool; + use super::{ToolbarEvent, ToolbarSliderSpec, ToolbarSnapshot}; /// What the marker opacity slider paints instead of a plain track and a @@ -102,8 +105,7 @@ impl StylePillSlider { (self == Self::Opacity).then(|| { let (spec, value) = self.value(snapshot); let color = snapshot.color; - let stroke_alpha = - |opacity: f64| crate::input::tool::marker_color_with_opacity(color, opacity).a; + let stroke_alpha = |opacity: f64| tool::marker_color_with_opacity(color, opacity).a; let min_alpha = stroke_alpha(spec.min); let max_alpha = stroke_alpha(spec.max); let position_for_alpha = |alpha| { @@ -131,7 +133,7 @@ impl StylePillSlider { match self { Self::Thickness => |value| format!("{value:.0}px"), Self::Opacity => |value| format!("{:.0}%", value * 100.0), - Self::SpotlightMagnification => crate::draw::format_spotlight_magnification, + Self::SpotlightMagnification => draw::format_spotlight_magnification, Self::FontSize => |value| format!("{value:.0}pt"), } } diff --git a/src/ui/toolbar/model/style_pill/stepper.rs b/src/ui/toolbar/model/style_pill/stepper.rs index 23119e43a..9cc8bf9d0 100644 --- a/src/ui/toolbar/model/style_pill/stepper.rs +++ b/src/ui/toolbar/model/style_pill/stepper.rs @@ -2,7 +2,12 @@ //! and, with `stroke_controls = "stepper"`, pen smoothing and Shape Pen //! detection. -use super::*; +use crate::{config, draw}; + +use super::{StylePillControl, StylePillStep, selection_entry}; +use crate::i18n::{tr, trf}; +use crate::input::SelectionPropertyKind; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; impl StylePillControl { /// The −/+ halves of a stepper, in reading order. @@ -84,7 +89,7 @@ fn pen_smoothing_steps(snapshot: &ToolbarSnapshot) -> [StylePillStep; 2] { id: "top.style.pen-smoothing.plus", label: "+", event: ToolbarEvent::SetPenSmoothing( - level.saturating_add(1).min(crate::draw::MAX_PEN_SMOOTHING), + level.saturating_add(1).min(draw::MAX_PEN_SMOOTHING), ), tooltip: tr("More smoothing").to_string(), }, @@ -107,7 +112,7 @@ fn shape_sensitivity_steps(snapshot: &ToolbarSnapshot) -> [StylePillStep; 2] { event: ToolbarEvent::SetShapeRecognitionSensitivity( level .saturating_add(1) - .min(crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY), + .min(config::MAX_SHAPE_RECOGNITION_SENSITIVITY), ), tooltip: tr("Recognize rougher strokes").to_string(), }, diff --git a/src/ui/toolbar/model/style_pill/tests/selection.rs b/src/ui/toolbar/model/style_pill/tests/selection.rs index 51ef30138..78a0e2848 100644 --- a/src/ui/toolbar/model/style_pill/tests/selection.rs +++ b/src/ui/toolbar/model/style_pill/tests/selection.rs @@ -1,3 +1,5 @@ +use crate::config::TopDisplayMode; + use super::*; fn selection_entry( @@ -230,7 +232,7 @@ fn allocation_free_queries_match_the_materialized_spec() { let mut minimized = snapshot(); minimized.top_minimized = true; let mut micro = snapshot(); - micro.top_display_mode = crate::config::TopDisplayMode::Micro; + micro.top_display_mode = TopDisplayMode::Micro; let mut text = snapshot(); text.text_active = true; diff --git a/src/ui/toolbar/model/style_pill/tests/stroke_controls.rs b/src/ui/toolbar/model/style_pill/tests/stroke_controls.rs index 0542f2e9b..979b3c014 100644 --- a/src/ui/toolbar/model/style_pill/tests/stroke_controls.rs +++ b/src/ui/toolbar/model/style_pill/tests/stroke_controls.rs @@ -2,6 +2,8 @@ //! Pen detection. The Pen feel chip is the default; meters and steppers put //! one inline control per setting in the pill. +use crate::{config, draw}; + use super::*; use crate::config::ToolbarStrokeControls; @@ -233,10 +235,10 @@ fn the_smoothing_stepper_moves_one_whole_pass_at_a_time() { "there is nothing below off" ); - snapshot.pen_smoothing = crate::draw::MAX_PEN_SMOOTHING; + snapshot.pen_smoothing = draw::MAX_PEN_SMOOTHING; assert_eq!( stepper.required_steps(&snapshot)[1].event, - ToolbarEvent::SetPenSmoothing(crate::draw::MAX_PEN_SMOOTHING) + ToolbarEvent::SetPenSmoothing(draw::MAX_PEN_SMOOTHING) ); } @@ -259,7 +261,7 @@ fn the_sensitivity_stepper_stays_in_range() { ); assert_eq!(steps[1].tooltip, "Recognize rougher strokes"); - let max = crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY; + let max = config::MAX_SHAPE_RECOGNITION_SENSITIVITY; snapshot.shape_recognition_sensitivity = 0; assert_eq!( stepper.required_steps(&snapshot)[0].event, diff --git a/src/ui/toolbar/model/style_pill/tests/tool_states.rs b/src/ui/toolbar/model/style_pill/tests/tool_states.rs index 5882859b3..7a072183d 100644 --- a/src/ui/toolbar/model/style_pill/tests/tool_states.rs +++ b/src/ui/toolbar/model/style_pill/tests/tool_states.rs @@ -1,3 +1,9 @@ +use crate::config::TopDisplayMode; +use crate::draw::{FontDescriptor, SpotlightMagnifierSource}; +use crate::input::SelectionPropertyValue; +use crate::ui::toolbar::PrecisionEntryTarget; +use crate::{config, draw}; + use super::*; use crate::config::ToolbarStrokeControls; @@ -73,7 +79,7 @@ fn the_smoothing_meter_names_its_level_and_keeps_events_on_its_bars() { assert_eq!(meter.value_text(&snapshot).as_deref(), Some("Medium")); let bars = meter.required_meter(&snapshot).segments; - assert_eq!(bars.len(), usize::from(crate::draw::MAX_PEN_SMOOTHING)); + assert_eq!(bars.len(), usize::from(draw::MAX_PEN_SMOOTHING)); assert_eq!(bars[3].event, ToolbarEvent::SetPenSmoothing(4)); // Zero passes is a state, not a quantity. @@ -139,7 +145,7 @@ fn bold_has_a_control_of_its_own() { assert_eq!(toggle.role(), StylePillRole::Toggle); assert_eq!(toggle.label(&snapshot), "Bold"); - snapshot.font = crate::draw::FontDescriptor::new( + snapshot.font = FontDescriptor::new( "Sans".to_string(), "normal".to_string(), "normal".to_string(), @@ -150,11 +156,8 @@ fn bold_has_a_control_of_its_own() { Some(ToolbarEvent::SetFontBold(true)) ); - snapshot.font = crate::draw::FontDescriptor::new( - "Sans".to_string(), - "Bold".to_string(), - "normal".to_string(), - ); + snapshot.font = + FontDescriptor::new("Sans".to_string(), "Bold".to_string(), "normal".to_string()); assert!( toggle.active(&snapshot), "the weight is compared without case, like every other font identity" @@ -173,11 +176,8 @@ fn a_numeric_weight_does_not_read_as_bold() { // The toggle writes words. A config asking for 700 is asking for something // a two-state control cannot say, so it must not claim to be showing it. let mut snapshot = snapshot(); - snapshot.font = crate::draw::FontDescriptor::new( - "Sans".to_string(), - "700".to_string(), - "normal".to_string(), - ); + snapshot.font = + FontDescriptor::new("Sans".to_string(), "700".to_string(), "normal".to_string()); assert!(!StylePillControl::FontWeightToggle.active(&snapshot)); } @@ -232,7 +232,7 @@ fn the_smoothing_meter_stops_at_both_ends_of_the_range() { ToolbarEvent::SetPenSmoothing(1) ); - snapshot.pen_smoothing = crate::draw::MAX_PEN_SMOOTHING; + snapshot.pen_smoothing = draw::MAX_PEN_SMOOTHING; assert_eq!(meter.meter_wheel_event(&snapshot, 1), None); assert!( meter @@ -312,7 +312,7 @@ fn the_shape_detection_meter_is_shape_pen_only_and_stays_in_range() { ToolbarEvent::SetShapeRecognitionSensitivity(3) ); - let max = crate::config::MAX_SHAPE_RECOGNITION_SENSITIVITY; + let max = config::MAX_SHAPE_RECOGNITION_SENSITIVITY; snapshot.shape_recognition_sensitivity = 0; assert_eq!(meter.meter_wheel_event(&snapshot, -1), None); snapshot.shape_recognition_sensitivity = max; @@ -323,7 +323,7 @@ fn the_shape_detection_meter_is_shape_pen_only_and_stays_in_range() { fn the_font_button_shows_the_family_in_use_and_opens_the_picker() { let mut snapshot = snapshot(); snapshot.text_active = true; - snapshot.font = crate::draw::FontDescriptor::new( + snapshot.font = FontDescriptor::new( "Noto Sans CJK JP Black".to_string(), "normal".to_string(), "normal".to_string(), @@ -373,8 +373,7 @@ fn a_squeezed_pill_sheds_its_extras_before_it_sheds_the_color_chip() { fn spotlight_state_exposes_an_inline_missing_source_hint() { let mut snapshot = snapshot_for_tool(Tool::Spotlight); snapshot.spotlight_magnification = 2.25; - snapshot.spotlight_magnifier_source = - Some(crate::draw::SpotlightMagnifierSource::IncompleteTransparent); + snapshot.spotlight_magnifier_source = Some(SpotlightMagnifierSource::IncompleteTransparent); let slider = StylePillControl::Slider(StylePillSlider::SpotlightMagnification); assert_eq!( @@ -382,8 +381,7 @@ fn spotlight_state_exposes_an_inline_missing_source_hint() { Some("Freeze screen to preview") ); - snapshot.spotlight_magnifier_source = - Some(crate::draw::SpotlightMagnifierSource::CompleteSolid); + snapshot.spotlight_magnifier_source = Some(SpotlightMagnifierSource::CompleteSolid); assert_eq!(slider.status_text(&snapshot), None); // No backend has answered, so the control says nothing rather than @@ -399,8 +397,7 @@ fn a_compact_strip_drops_the_pill_and_with_it_the_unavailable_status() { // has to decide what to do with a hint it cannot fit. let mut snapshot = snapshot_for_tool(Tool::Spotlight); snapshot.spotlight_magnification = 2.25; - snapshot.spotlight_magnifier_source = - Some(crate::draw::SpotlightMagnifierSource::IncompleteTransparent); + snapshot.spotlight_magnifier_source = Some(SpotlightMagnifierSource::IncompleteTransparent); let mut compact = plan(); compact.compact = true; @@ -432,7 +429,7 @@ fn minimized_and_micro_strips_hide_the_pill() { ); let mut micro = snapshot(); - micro.top_display_mode = crate::config::TopDisplayMode::Micro; + micro.top_display_mode = TopDisplayMode::Micro; assert_eq!( StylePillSpec::state_of(µ, &plan()), StylePillState::Hidden @@ -443,7 +440,7 @@ fn minimized_and_micro_strips_hide_the_pill() { #[test] fn an_unconstrained_pill_offers_the_whole_built_in_palette() { let snapshot = snapshot_for_tool(Tool::Pen); - let palette = crate::config::QuickColorPalette::default(); + let palette = QuickColorPalette::default(); assert!(palette.rendered_entries().len() <= StylePillSpec::MAX_SWATCHES); let spec = StylePillSpec::build(&snapshot, &plan()); @@ -512,7 +509,7 @@ fn stroke_state_orders_chip_swatches_slider_and_numeral() { assert_eq!( numeral.event(&snapshot), Some(ToolbarEvent::OpenPrecisionEntry( - crate::ui::toolbar::PrecisionEntryTarget::Thickness + PrecisionEntryTarget::Thickness )) ); assert_eq!( @@ -552,7 +549,7 @@ fn hiding_quick_colors_hides_the_pill_swatches_but_keeps_the_chip() { ); // A Text-state pill (also color-bearing) honours the same gate. - let mut text = snapshot.clone(); + let mut text = snapshot; text.text_active = true; let text_ids = control_ids(&StylePillSpec::build(&text, &plan())); assert_eq!( @@ -796,7 +793,7 @@ fn text_state_is_swatches_size_and_one_font_control() { assert_eq!( numeral.event(&snapshot), Some(ToolbarEvent::OpenPrecisionEntry( - crate::ui::toolbar::PrecisionEntryTarget::FontSize + PrecisionEntryTarget::FontSize )) ); assert_eq!( @@ -906,24 +903,19 @@ fn text_controls_appear_only_where_text_is_drawn_while_the_pill_adapts() { fn the_docked_selection_control_reports_on_the_selected_shape_not_the_tool_default() { let mut snapshot = snapshot_for_tool(Tool::Select); // The next Spotlight the user draws would be unmagnified... - snapshot.spotlight_magnification = crate::draw::DEFAULT_SPOTLIGHT_MAGNIFICATION; + snapshot.spotlight_magnification = draw::DEFAULT_SPOTLIGHT_MAGNIFICATION; // ...but the shape they have selected is not. snapshot.selection_spotlight_magnification = Some(3.0); - snapshot.spotlight_magnifier_source = - Some(crate::draw::SpotlightMagnifierSource::IncompleteTransparent); - snapshot - .selection_properties - .push(crate::input::SelectionPropertyEntry { - label: "Magnification".to_string(), - value: "3x".to_string(), - kind: crate::input::SelectionPropertyKind::SpotlightMagnification, - state: crate::input::SelectionPropertyValue::Number(Some(3.0)), - disabled: false, - }); - - let stepper = StylePillControl::SelectionStepper( - crate::input::SelectionPropertyKind::SpotlightMagnification, - ); + snapshot.spotlight_magnifier_source = Some(SpotlightMagnifierSource::IncompleteTransparent); + snapshot.selection_properties.push(SelectionPropertyEntry { + label: "Magnification".to_string(), + value: "3x".to_string(), + kind: SelectionPropertyKind::SpotlightMagnification, + state: SelectionPropertyValue::Number(Some(3.0)), + disabled: false, + }); + + let stepper = StylePillControl::SelectionStepper(SelectionPropertyKind::SpotlightMagnification); assert!(stepper.has_status_slot()); assert_eq!( stepper.status_text(&snapshot), @@ -938,7 +930,7 @@ fn the_docked_selection_control_reports_on_the_selected_shape_not_the_tool_defau ); // An unmagnified selection has nothing to warn about either. - snapshot.selection_spotlight_magnification = Some(crate::draw::DEFAULT_SPOTLIGHT_MAGNIFICATION); + snapshot.selection_spotlight_magnification = Some(draw::DEFAULT_SPOTLIGHT_MAGNIFICATION); assert_eq!(stepper.status_text(&snapshot), None); } diff --git a/src/ui/toolbar/model/top_spec.rs b/src/ui/toolbar/model/top_spec.rs index fdc5d10f7..1a6d27f88 100644 --- a/src/ui/toolbar/model/top_spec.rs +++ b/src/ui/toolbar/model/top_spec.rs @@ -1,21 +1,15 @@ -use std::borrow::Cow; - -use crate::config::{ - Action, ToolbarItemId, ToolbarLayoutMode, action_label, action_short_label, - toolbar_item_ids as ids, -}; +use crate::config::toolbar_item_ids as ids; +#[cfg(test)] +use crate::config::{Action, ToolbarLayoutMode}; +#[cfg(test)] use crate::input::Tool; +#[cfg(test)] use crate::label_format::format_binding_label; -use crate::ui::toolbar::bindings::{tool_label, tool_tooltip_label}; +#[cfg(test)] use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; -use super::{ - SemanticToolIcon, TopToolGroup, TopUtilityButton, active_tool_in_shape_picker, - default_drag_hint, layout_mode_label, semantic_icon_for_tool, toolbar_item_id_for_tool, - toolbar_item_visible, top_highlight_ring_visible, top_highlight_visible, - top_shape_picker_visible, top_tool_group, visible_top_tool_buttons, - visible_top_utility_buttons, -}; +#[cfg(test)] +use super::{TopUtilityButton, semantic_icon_for_tool}; mod control; mod control_meta; diff --git a/src/ui/toolbar/model/top_spec/control.rs b/src/ui/toolbar/model/top_spec/control.rs index 771282824..7e9dd0fb8 100644 --- a/src/ui/toolbar/model/top_spec/control.rs +++ b/src/ui/toolbar/model/top_spec/control.rs @@ -1,6 +1,22 @@ -use super::control_meta::*; -use super::*; +use crate::config::TopDisplayMode; + +use super::super::{ + active_tool_in_shape_picker, layout_mode_label, semantic_icon_for_tool, + toolbar_item_id_for_tool, +}; +use super::control_meta::{ + TopToolbarControlId, TopToolbarControlRole, TopToolbarIcon, TopToolbarUtility, action_tooltip, + preset_accessible_label, preset_slot, preset_tooltip, tool_tooltip, utility_accessible_label, + utility_action, utility_event, utility_short_label, +}; +use super::ids; +use crate::config::{Action, action_label, action_short_label}; use crate::i18n::{tr, trf}; +use crate::input::Tool; +use crate::label_format::format_binding_label; +use crate::ui::toolbar::bindings::{tool_label, tool_tooltip_label}; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; +use std::borrow::Cow; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum TopToolbarNode { @@ -144,7 +160,7 @@ impl TopToolbarControl { pub(crate) fn event(self, snapshot: &ToolbarSnapshot) -> ToolbarEvent { match self { Self::Restore => ToolbarEvent::SetTopMinimized(false), - Self::MicroChip => ToolbarEvent::SetTopDisplayMode(crate::config::TopDisplayMode::Full), + Self::MicroChip => ToolbarEvent::SetTopDisplayMode(TopDisplayMode::Full), Self::DragHandle => ToolbarEvent::MoveTopToolbar { x: 0.0, y: 0.0 }, Self::Tool(tool) => ToolbarEvent::SelectTool(tool), Self::ShapePicker => ToolbarEvent::ToggleShapePicker(!snapshot.shape_picker_open), diff --git a/src/ui/toolbar/model/top_spec/control_meta.rs b/src/ui/toolbar/model/top_spec/control_meta.rs index c1c51d885..2cdb896a3 100644 --- a/src/ui/toolbar/model/top_spec/control_meta.rs +++ b/src/ui/toolbar/model/top_spec/control_meta.rs @@ -1,5 +1,15 @@ -use super::*; +use crate::config::ToolbarLayoutMode; +use crate::draw::Color; +use crate::ui::toolbar::PresetSlotSnapshot; + +use super::super::{SemanticToolIcon, TopUtilityButton, default_drag_hint}; +use crate::config::{Action, ToolbarItemId, action_label, action_short_label}; use crate::i18n::{tr, trf}; +use crate::input::Tool; +use crate::label_format::format_binding_label; +use crate::ui::toolbar::bindings::tool_tooltip_label; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; +use std::borrow::Cow; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum TopToolbarControlId { @@ -99,11 +109,11 @@ impl TopToolbarIcon { /// The density glyph for a layout preset. Total on purpose: the GTK /// factory renders the layout button from the mode alone, and this /// mapping spares it unwrapping the generic `icon()` Option. - pub(crate) fn for_layout_mode(mode: crate::config::ToolbarLayoutMode) -> Self { + pub(crate) fn for_layout_mode(mode: ToolbarLayoutMode) -> Self { match mode { - crate::config::ToolbarLayoutMode::Simple => Self::LayoutSimple, - crate::config::ToolbarLayoutMode::Regular => Self::LayoutRegular, - crate::config::ToolbarLayoutMode::Advanced => Self::LayoutAdvanced, + ToolbarLayoutMode::Simple => Self::LayoutSimple, + ToolbarLayoutMode::Regular => Self::LayoutRegular, + ToolbarLayoutMode::Advanced => Self::LayoutAdvanced, } } } @@ -148,10 +158,7 @@ pub(super) fn utility_accessible_label(utility: TopToolbarUtility) -> &'static s /// The filled preset saved in slot `index` (0-based), if any. Both frontends /// read the same accessor so their per-slot rendering cannot drift. -pub(crate) fn preset_slot( - snapshot: &ToolbarSnapshot, - index: usize, -) -> Option<&crate::ui::toolbar::PresetSlotSnapshot> { +pub(crate) fn preset_slot(snapshot: &ToolbarSnapshot, index: usize) -> Option<&PresetSlotSnapshot> { snapshot.presets.get(index).and_then(Option::as_ref) } @@ -190,9 +197,9 @@ fn preset_summary(snapshot: &ToolbarSnapshot, index: usize) -> Option { }) } -fn preset_color_label(snapshot: &ToolbarSnapshot, color: crate::draw::Color) -> String { +fn preset_color_label(snapshot: &ToolbarSnapshot, color: Color) -> String { const TOLERANCE: f64 = 0.5 / 255.0; - let matches = |entry: &crate::draw::Color| { + let matches = |entry: &Color| { (entry.r - color.r).abs() <= TOLERANCE && (entry.g - color.g).abs() <= TOLERANCE && (entry.b - color.b).abs() <= TOLERANCE diff --git a/src/ui/toolbar/model/top_spec/spec.rs b/src/ui/toolbar/model/top_spec/spec.rs index 6e0d9ff35..87215ca5a 100644 --- a/src/ui/toolbar/model/top_spec/spec.rs +++ b/src/ui/toolbar/model/top_spec/spec.rs @@ -1,4 +1,14 @@ -use super::*; +use super::super::{ + TopToolGroup, TopUtilityButton, toolbar_item_visible, top_highlight_ring_visible, + top_highlight_visible, top_shape_picker_visible, top_tool_group, visible_top_tool_buttons, + visible_top_utility_buttons, +}; +use super::control::{TopToolbarControl, TopToolbarDivider, TopToolbarNode}; +use super::control_meta::TopToolbarUtility; +use super::ids; +use crate::config::ToolbarLayoutMode; +use crate::input::Tool; +use crate::ui::toolbar::ToolbarSnapshot; /// Width-degradation result shared by both top-toolbar frontends. /// diff --git a/src/ui/toolbar/model/top_spec/tests/behavior.rs b/src/ui/toolbar/model/top_spec/tests/behavior.rs index e09e81653..58e69586d 100644 --- a/src/ui/toolbar/model/top_spec/tests/behavior.rs +++ b/src/ui/toolbar/model/top_spec/tests/behavior.rs @@ -1,3 +1,6 @@ +use crate::draw::Color; +use crate::ui::toolbar::PresetSlotSnapshot; + use super::*; #[test] @@ -226,7 +229,7 @@ fn about_is_a_hideable_chrome_entry_that_opens_the_dialog() { assert!(!strip_control_ids(&spec).contains(&ids::TOP_CHROME_ABOUT.as_str().to_string())); assert!(!spec.overflow().contains(&TopToolbarControl::About)); - let mut hidden = snapshot.clone(); + let mut hidden = snapshot; let mut items = ToolbarItemsConfig::default(); items.set_hidden(ids::TOP_CHROME_ABOUT, true); hidden.resolved_toolbar_items = items.resolved(); @@ -453,7 +456,7 @@ fn presets_island_hosts_the_saved_slots() { let mut snapshot = snapshot(); snapshot.presets = vec![None; 5]; - snapshot.presets[0] = Some(crate::ui::toolbar::PresetSlotSnapshot { + snapshot.presets[0] = Some(PresetSlotSnapshot { name: Some("Red pen".to_string()), tool: Tool::Pen, color: Color::new(1.0, 0.0, 0.0, 1.0), @@ -539,12 +542,8 @@ fn presets_island_hosts_the_saved_slots() { assert!(!has_preset(&TopToolbarSpec::build(&snapshot, &dropped))); } -fn preset( - tool: Tool, - color: crate::draw::Color, - size: f64, -) -> crate::ui::toolbar::PresetSlotSnapshot { - crate::ui::toolbar::PresetSlotSnapshot { +fn preset(tool: Tool, color: Color, size: f64) -> PresetSlotSnapshot { + PresetSlotSnapshot { name: None, tool, color, @@ -572,13 +571,9 @@ fn preset_slots_say_what_they_hold_and_how_to_fill_them() { let red = snapshot.quick_colors.rendered_entries()[0].clone(); snapshot.presets = vec![None; 5]; snapshot.presets[0] = Some(preset(Tool::Pen, red.color, 4.0)); - snapshot.presets[1] = Some(crate::ui::toolbar::PresetSlotSnapshot { + snapshot.presets[1] = Some(PresetSlotSnapshot { name: Some("Lecture".to_string()), - ..preset( - Tool::Marker, - crate::draw::Color::new(0.2, 0.4, 0.6, 1.0), - 12.0, - ) + ..preset(Tool::Marker, Color::new(0.2, 0.4, 0.6, 1.0), 12.0) }); snapshot.presets[2] = Some(preset(Tool::Eraser, red.color, 18.0)); @@ -679,7 +674,7 @@ fn a_grouped_tool_lights_one_button() { } // Simple layouts move Shape Pen, Line, and Arrow into the picker. - let mut simple = full.clone(); + let mut simple = full; simple.layout_mode = ToolbarLayoutMode::Simple; for tool in [Tool::LiveShape, Tool::Line, Tool::Arrow] { simple.active_tool = tool; diff --git a/src/ui/toolbar/model/top_spec/tests/structure.rs b/src/ui/toolbar/model/top_spec/tests/structure.rs index c68155f6a..d34723fe3 100644 --- a/src/ui/toolbar/model/top_spec/tests/structure.rs +++ b/src/ui/toolbar/model/top_spec/tests/structure.rs @@ -1,3 +1,5 @@ +use crate::config::TopDisplayMode; + use super::*; #[test] @@ -154,7 +156,7 @@ fn minimized_spec_contains_only_the_non_hideable_restore_control() { #[test] fn micro_spec_contains_only_the_non_hideable_micro_chip_control() { let mut snapshot = snapshot(); - snapshot.top_display_mode = crate::config::TopDisplayMode::Micro; + snapshot.top_display_mode = TopDisplayMode::Micro; snapshot .resolved_toolbar_items .hidden @@ -171,7 +173,7 @@ fn micro_spec_contains_only_the_non_hideable_micro_chip_control() { }; assert_eq!( chip.event(&snapshot), - ToolbarEvent::SetTopDisplayMode(crate::config::TopDisplayMode::Full) + ToolbarEvent::SetTopDisplayMode(TopDisplayMode::Full) ); assert_eq!(chip.role(), TopToolbarControlRole::Restore); assert_eq!( @@ -385,7 +387,7 @@ fn assert_collapsed_strips_have_no_overflow() { .is_empty() ); let mut micro = self::snapshot(); - micro.top_display_mode = crate::config::TopDisplayMode::Micro; + micro.top_display_mode = TopDisplayMode::Micro; assert!( TopToolbarSpec::build(µ, &TopStripPlan::unconstrained()) .overflow() diff --git a/src/ui/toolbar/snapshot/build.rs b/src/ui/toolbar/snapshot/build.rs index 0a06dfc13..2d7095c37 100644 --- a/src/ui/toolbar/snapshot/build.rs +++ b/src/ui/toolbar/snapshot/build.rs @@ -1,8 +1,8 @@ use std::path::PathBuf; use std::time::Instant; -use crate::input::state::PRESET_FEEDBACK_DURATION_MS; -use crate::input::{BoardBackground, InputState}; +use crate::input::state::{PRESET_FEEDBACK_DURATION_MS, TopMenuState}; +use crate::input::{BoardBackground, DrawingState, InputState, TextInputMode, Tool}; use super::super::bindings::ToolbarBindingHints; use super::types::{PresetFeedbackSnapshot, PresetSlotSnapshot, ToolbarSnapshot}; @@ -29,10 +29,10 @@ impl ToolbarSnapshot { }; let page_count = state.boards.page_count(); let page_index = state.boards.active_page_index(); - let text_active = matches!(state.state, crate::input::DrawingState::TextInput { .. }) - && state.text_editing.mode() == crate::input::TextInputMode::Plain; - let note_active = matches!(state.state, crate::input::DrawingState::TextInput { .. }) - && state.text_editing.mode() == crate::input::TextInputMode::StickyNote; + let text_active = matches!(state.state, DrawingState::TextInput { .. }) + && state.text_editing.mode() == TextInputMode::Plain; + let note_active = matches!(state.state, DrawingState::TextInput { .. }) + && state.text_editing.mode() == TextInputMode::StickyNote; let override_tool = state.tool_override(); let thickness_targets_eraser = active_tool.uses_eraser_size() || override_tool @@ -198,22 +198,14 @@ impl ToolbarSnapshot { presets, active_preset_slot: state.preset_slots.active(), preset_feedback, - shape_picker_open: state.toolbar_top_menu() - == crate::input::state::TopMenuState::ShapePicker, - top_overflow_open: state.toolbar_top_menu() - == crate::input::state::TopMenuState::TopOverflow, - session_popover_open: state.toolbar_top_menu() - == crate::input::state::TopMenuState::SessionPopover, - settings_popover_open: state.toolbar_top_menu() - == crate::input::state::TopMenuState::SettingsPopover, - canvas_popover_open: state.toolbar_top_menu() - == crate::input::state::TopMenuState::CanvasPopover, - layout_menu_open: state.toolbar_top_menu() - == crate::input::state::TopMenuState::LayoutMenu, - pen_feel_open: state.toolbar_top_menu() - == crate::input::state::TopMenuState::PenFeelPanel, - arrow_style_menu_open: state.toolbar_top_menu() - == crate::input::state::TopMenuState::ArrowStyleMenu, + shape_picker_open: state.toolbar_top_menu() == TopMenuState::ShapePicker, + top_overflow_open: state.toolbar_top_menu() == TopMenuState::TopOverflow, + session_popover_open: state.toolbar_top_menu() == TopMenuState::SessionPopover, + settings_popover_open: state.toolbar_top_menu() == TopMenuState::SettingsPopover, + canvas_popover_open: state.toolbar_top_menu() == TopMenuState::CanvasPopover, + layout_menu_open: state.toolbar_top_menu() == TopMenuState::LayoutMenu, + pen_feel_open: state.toolbar_top_menu() == TopMenuState::PenFeelPanel, + arrow_style_menu_open: state.toolbar_top_menu() == TopMenuState::ArrowStyleMenu, exit_hides_overlay: false, top_popover_scroll: state.toolbar_top_popover_scroll(), top_minimized: state.toolbar_top_minimized(), @@ -221,7 +213,7 @@ impl ToolbarSnapshot { // Fade is owned by the backend engine; renderers see 1.0 until // the backend publishes the animated value. top_fade: 1.0, - selection_properties: if active_tool == crate::input::Tool::Select { + selection_properties: if active_tool == Tool::Select { state.build_selection_property_entries_from_resolved( state.selected_shape_ids(), &selected, @@ -254,7 +246,8 @@ mod tests { use super::*; use crate::config::{QuickColorPalette, QuickColorPaletteEntry}; use crate::draw::Color; - use crate::input::state::{TopMenuState, test_support::make_test_input_state}; + use crate::input::state::TopMenuState; + use crate::input::state::test_support::make_test_input_state; #[test] fn snapshot_carries_input_state_quick_colors() { @@ -336,19 +329,23 @@ mod tests { #[cfg(test)] mod measurement { + use std::hint; + + use crate::input::state::test_support; + use super::*; #[test] #[ignore = "release timing workload; run with --release --ignored --nocapture"] fn measure_unchanged_toolbar_snapshot() { - let state = crate::input::state::test_support::make_test_input_state(); + let state = test_support::make_test_input_state(); let start = Instant::now(); let mut changes = 0; let mut previous = ToolbarSnapshot::from_input(&state); for _ in 0..10_000 { let snapshot = ToolbarSnapshot::from_input(&state); changes += usize::from(snapshot != previous); - previous = std::hint::black_box(snapshot); + previous = hint::black_box(snapshot); } eprintln!( "P05 constructions=10000 changed={changes} mean_us={:.2}", diff --git a/src/ui/toolbar/snapshot/fade.rs b/src/ui/toolbar/snapshot/fade.rs index 52c4bce19..119c32edc 100644 --- a/src/ui/toolbar/snapshot/fade.rs +++ b/src/ui/toolbar/snapshot/fade.rs @@ -37,6 +37,7 @@ pub const TOP_STRIP_FADE_TICK: Duration = Duration::from_millis(16); /// Whether a published `top_fade` value is the fully hidden strip, which /// must not intercept pointer input. +#[must_use] pub fn top_strip_hidden(fade: f64) -> bool { fade <= TOP_STRIP_HIDDEN_LEVEL } @@ -88,6 +89,7 @@ impl Default for TopStripFade { } impl TopStripFade { + #[must_use] pub fn new() -> Self { Self { envelope: Envelope::new(TOP_STRIP_SHOWN_LEVEL), @@ -127,11 +129,13 @@ impl TopStripFade { /// Current fade value ([`TOP_STRIP_SHOWN_LEVEL`] = shown, /// [`TOP_STRIP_HIDDEN_LEVEL`] = hidden). + #[must_use] pub fn value(&self) -> f64 { self.envelope.value() } /// Whether a transition is still in flight. + #[must_use] pub fn animating(&self) -> bool { !self.envelope.settled() } @@ -143,6 +147,7 @@ impl TopStripFade { /// motion, a key, a menu), which wakes the loop on its own. The idle /// deadline applies under reduced motion too: the snap still has to /// happen on time. + #[must_use] pub fn wake_after(&self, inputs: &TopStripFadeInputs, now: Instant) -> Option { if self.animating() { return Some(TOP_STRIP_FADE_TICK); diff --git a/src/ui/toolbar/snapshot/selection_tests.rs b/src/ui/toolbar/snapshot/selection_tests.rs index 2d9c68497..7f6f9bf17 100644 --- a/src/ui/toolbar/snapshot/selection_tests.rs +++ b/src/ui/toolbar/snapshot/selection_tests.rs @@ -3,7 +3,7 @@ use super::ToolbarSnapshot; #[test] fn shared_selection_preserves_text_order_locked_notes_and_normalized_spotlights() { let mut state = make_test_input_state(); - state.apply_toolbar_event(crate::ui::toolbar::ToolbarEvent::SelectTool(Tool::Select)); + state.apply_toolbar_event(ToolbarEvent::SelectTool(Tool::Select)); let mut bold = state.style.font_descriptor.clone(); bold.weight = "bold".to_string(); let mut normal = state.style.font_descriptor.clone(); @@ -102,11 +102,11 @@ fn shared_selection_preserves_text_order_locked_notes_and_normalized_spotlights( } } -use crate::{ - draw::{Frame, Shape}, - input::state::SelectionPropertyKind, - input::{InputState, Tool, state::test_support::make_test_input_state}, -}; +use crate::draw::{Frame, Shape}; +use crate::input::state::SelectionPropertyKind; +use crate::input::state::test_support::make_test_input_state; +use crate::input::{InputState, Tool}; +use crate::ui::toolbar::ToolbarEvent; #[test] fn complete_snapshot_resolves_selection_once_without_per_id_lookups() { @@ -119,7 +119,7 @@ fn complete_snapshot_resolves_selection_once_without_per_id_lookups() { for case in [Fixture::Rectangles, Fixture::TextLast, Fixture::LockedText] { let mut state = make_test_input_state(); - state.apply_toolbar_event(crate::ui::toolbar::ToolbarEvent::SelectTool(Tool::Select)); + state.apply_toolbar_event(ToolbarEvent::SelectTool(Tool::Select)); let mut font = state.style.font_descriptor.clone(); font.weight = "bold".to_string(); let color = state.style.current_color; diff --git a/src/ui/toolbar/snapshot/types.rs b/src/ui/toolbar/snapshot/types.rs index bb9690fbb..037325b98 100644 --- a/src/ui/toolbar/snapshot/types.rs +++ b/src/ui/toolbar/snapshot/types.rs @@ -1,10 +1,12 @@ -use crate::config::QuickColorPalette; -use crate::config::{ResolvedToolbarItems, ToolbarItemId, ToolbarLayoutMode, TopDisplayMode}; -use crate::draw::{ArrowStyle, Color, EraserKind, FontDescriptor}; +use crate::config::{ + QuickColorPalette, ResolvedToolbarItems, StatusBarItem, ToolbarItemId, ToolbarLayoutMode, + ToolbarStrokeControls, TopDisplayMode, +}; +use crate::draw::{ArrowStyle, Color, EraserKind, FontDescriptor, SpotlightMagnifierSource}; use crate::i18n::tr; use crate::input::state::PresetFeedbackKind; use crate::input::tool::{ToolControlGroup, ToolProfile}; -use crate::input::{EraserMode, Tool}; +use crate::input::{EraserMode, SelectionPropertyEntry, Tool}; use std::path::PathBuf; use super::super::bindings::ToolbarBindingHints; @@ -92,6 +94,7 @@ pub struct ToolContext { impl ToolContext { /// Compute the tool context from the current toolbar state. + #[must_use] pub fn from_snapshot(snapshot: &ToolbarSnapshot) -> Self { // If context-aware UI is disabled, show all sections (classic behavior) if !snapshot.context_aware_ui { @@ -298,7 +301,7 @@ pub struct ToolbarSnapshot { /// Only the backend that owns the canvas can decide this, so a snapshot /// built from `InputState` alone leaves it unset rather than guessing; an /// unset source simply shows no inline status. - pub spotlight_magnifier_source: Option, + pub spotlight_magnifier_source: Option, /// Highest magnification among selected Spotlights, when the selection /// holds any. Drives the docked selection control's inline status, /// which reports on the selected shape rather than the tool default. @@ -358,7 +361,7 @@ pub struct ToolbarSnapshot { /// Current toolbar layout mode pub layout_mode: ToolbarLayoutMode, /// How the style pill shows pen smoothing and Shape Pen sensitivity - pub stroke_controls: crate::config::ToolbarStrokeControls, + pub stroke_controls: ToolbarStrokeControls, /// Resolved known item-level toolbar visibility config. pub resolved_toolbar_items: ResolvedToolbarItems, /// Whether to show extended color palette @@ -486,7 +489,7 @@ pub struct ToolbarSnapshot { /// Selection-property entries docked into the style pill while the /// select tool has an active selection (mirrors the overlay properties /// popup's entry list; empty when nothing is selected). - pub selection_properties: Vec, + pub selection_properties: Vec, } impl ToolbarSnapshot { @@ -499,35 +502,39 @@ impl ToolbarSnapshot { } } - pub fn status_bar_item_visible(&self, item: crate::config::StatusBarItem) -> bool { + #[must_use] + pub fn status_bar_item_visible(&self, item: StatusBarItem) -> bool { match item { - crate::config::StatusBarItem::ActiveOutput => self.show_active_output_badge, - crate::config::StatusBarItem::SelectionInfo => self.show_status_selection_info, - crate::config::StatusBarItem::Board => self.show_status_board_badge, - crate::config::StatusBarItem::Page => self.show_status_page_badge, - crate::config::StatusBarItem::Color => self.show_status_color, - crate::config::StatusBarItem::Tool => self.show_status_tool, - crate::config::StatusBarItem::Size => self.show_status_size, - crate::config::StatusBarItem::ContextIndicators => self.show_status_context_indicators, - crate::config::StatusBarItem::ToolbarHint => self.show_toolbar_hint, - crate::config::StatusBarItem::Help => self.show_status_help, - crate::config::StatusBarItem::About => self.show_status_about, + StatusBarItem::ActiveOutput => self.show_active_output_badge, + StatusBarItem::SelectionInfo => self.show_status_selection_info, + StatusBarItem::Board => self.show_status_board_badge, + StatusBarItem::Page => self.show_status_page_badge, + StatusBarItem::Color => self.show_status_color, + StatusBarItem::Tool => self.show_status_tool, + StatusBarItem::Size => self.show_status_size, + StatusBarItem::ContextIndicators => self.show_status_context_indicators, + StatusBarItem::ToolbarHint => self.show_toolbar_hint, + StatusBarItem::Help => self.show_status_help, + StatusBarItem::About => self.show_status_about, } } /// Whether the top strip renders as the micro chip. Minimized wins when /// both states are somehow set (e.g. a hand-edited config): the restore /// tab is the more explicit "bring me back" affordance. + #[must_use] pub fn top_micro_active(&self) -> bool { self.top_display_mode == TopDisplayMode::Micro && !self.top_minimized } /// Whether the idle fade has the strip fully hidden. Every frontend keeps /// the surface mapped but lets pointer input through to the canvas. + #[must_use] pub fn top_strip_hidden(&self) -> bool { super::fade::top_strip_hidden(self.top_fade) } + #[must_use] pub fn toolbar_item_hidden(&self, item: ToolbarItemId) -> bool { self.resolved_toolbar_items.is_hidden(item) } diff --git a/src/ui_text.rs b/src/ui_text.rs index c38087009..1fc45b9c6 100644 --- a/src/ui_text.rs +++ b/src/ui_text.rs @@ -1,9 +1,11 @@ use std::cell::RefCell; use std::collections::HashMap; +use pangocairo::functions; + #[derive(Clone, Copy, Debug)] -pub(crate) struct UiTextStyle<'a> { - pub family: &'a str, +pub(crate) struct UiTextStyle<'font> { + pub family: &'font str, pub slant: cairo::FontSlant, pub weight: cairo::FontWeight, pub size: f64, @@ -66,7 +68,7 @@ impl UiTextLayout { pub(crate) fn show_at_baseline(&self, ctx: &cairo::Context, x: f64, y: f64) { ctx.move_to(x, y - self.baseline); - pangocairo::functions::show_layout(ctx, &self.layout); + functions::show_layout(ctx, &self.layout); } } @@ -209,7 +211,7 @@ impl UiTextEngine { if let Some(layout) = cached { // Re-bind the cached layout to the current Cairo context (font options, // resolution, transformation) without re-shaping the text. - pangocairo::functions::update_layout(ctx, &layout); + functions::update_layout(ctx, &layout); let (ink_rect, logical_rect, baseline) = layout_metrics(&layout); return UiTextLayout { layout, @@ -219,7 +221,7 @@ impl UiTextEngine { }; } - let layout = pangocairo::functions::create_layout(ctx); + let layout = functions::create_layout(ctx); let font_desc = font_description(style); layout.set_font_description(Some(&font_desc)); layout.set_text(text); diff --git a/src/ui_text/tests.rs b/src/ui_text/tests.rs index 500d7c017..8cb3f8c2e 100644 --- a/src/ui_text/tests.rs +++ b/src/ui_text/tests.rs @@ -15,7 +15,7 @@ fn uncached_text_extents( text: &str, wrap_width: Option, ) -> UiTextExtents { - let layout = pangocairo::functions::create_layout(ctx); + let layout = functions::create_layout(ctx); let font_desc = font_description(style); layout.set_font_description(Some(&font_desc)); layout.set_text(text); diff --git a/src/unix_signals.rs b/src/unix_signals.rs index ad48fd91e..37e19bcc1 100644 --- a/src/unix_signals.rs +++ b/src/unix_signals.rs @@ -1,6 +1,9 @@ -use std::io; use std::os::fd::{FromRawFd, OwnedFd, RawFd}; -use std::sync::atomic::{AtomicI32, AtomicU64, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, AtomicUsize, Ordering}; +use std::{io, mem, ptr, thread}; + +#[cfg(test)] +use crate::test_env::{Lease, LeaseGuard}; mod listener; @@ -31,8 +34,8 @@ static ACTIVE_HANDLERS: AtomicUsize = AtomicUsize::new(0); static SIGNAL_WRITE_FD: AtomicI32 = AtomicI32::new(-1); static REGISTERED_SIGNALS: [AtomicI32; MAX_SIGNAL_SLOTS] = [const { AtomicI32::new(0) }; MAX_SIGNAL_SLOTS]; -static PENDING_SIGNALS: [std::sync::atomic::AtomicBool; MAX_SIGNAL_SLOTS] = - [const { std::sync::atomic::AtomicBool::new(false) }; MAX_SIGNAL_SLOTS]; +static PENDING_SIGNALS: [AtomicBool; MAX_SIGNAL_SLOTS] = + [const { AtomicBool::new(false) }; MAX_SIGNAL_SLOTS]; fn gate_token(epoch: u64, state: u64) -> u64 { (epoch << GATE_STATE_BITS) | state @@ -137,7 +140,7 @@ fn wait_for_admitted_handlers() { // a handler that is concurrently rechecking the closed gate on weakly // ordered CPUs and then close a descriptor that handler is about to use. while ACTIVE_HANDLERS.load(Ordering::SeqCst) != 0 { - std::thread::yield_now(); + thread::yield_now(); } } @@ -191,7 +194,7 @@ fn set_status_flag(fd: RawFd, flag: libc::c_int) -> io::Result<()> { fn install_handler(signal: libc::c_int) -> io::Result { // SAFETY: Zeroed sigaction is immediately initialized before use. - let mut action = unsafe { std::mem::zeroed::() }; + let mut action = unsafe { mem::zeroed::() }; action.sa_sigaction = signal_handler as *const () as usize; action.sa_flags = signal_action_flags(); @@ -201,7 +204,7 @@ fn install_handler(signal: libc::c_int) -> io::Result { } // SAFETY: Zeroed sigaction is filled by libc when installation succeeds. - let mut previous = unsafe { std::mem::zeroed::() }; + let mut previous = unsafe { mem::zeroed::() }; // SAFETY: Installs a process-wide handler for the requested signal. if unsafe { libc::sigaction(signal, &action, &mut previous) } != 0 { return Err(io::Error::last_os_error()); @@ -212,7 +215,7 @@ fn install_handler(signal: libc::c_int) -> io::Result { fn restore_handlers(handlers: &[(libc::c_int, libc::sigaction)]) { for (signal, previous) in handlers.iter().rev() { // SAFETY: `previous` was returned by `sigaction` for the same signal. - let _ = unsafe { libc::sigaction(*signal, previous, std::ptr::null_mut()) }; + let _ = unsafe { libc::sigaction(*signal, previous, ptr::null_mut()) }; } } @@ -253,7 +256,7 @@ extern "C" fn signal_handler(signal: libc::c_int) { libc::write( fd, (&wakeup as *const u8).cast::(), - std::mem::size_of::(), + mem::size_of::(), ) }; } @@ -382,7 +385,7 @@ fn write_pipe_hint(fd: RawFd) -> io::Result<()> { libc::write( fd, (&wakeup as *const u8).cast::(), - std::mem::size_of::(), + mem::size_of::(), ) }; if result == 1 { @@ -413,6 +416,7 @@ fn close_fd(fd: RawFd) { #[cfg(test)] mod test_hooks { use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering}; + use std::{hint, thread}; pub(super) const PAUSE_AFTER_TOKEN_READ: u8 = 1; pub(super) const PAUSE_AFTER_ADMISSION: u8 = 2; @@ -428,19 +432,16 @@ mod test_hooks { } HANDLER_PAUSED.store(true, Ordering::Release); while !RESUME_HANDLER.load(Ordering::Acquire) { - std::hint::spin_loop(); - std::thread::yield_now(); + hint::spin_loop(); + thread::yield_now(); } } } #[cfg(test)] -pub(crate) fn test_signal_lock() -> std::sync::MutexGuard<'static, ()> { - use std::sync::{Mutex, OnceLock}; - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) +pub(crate) fn test_signal_lock() -> LeaseGuard<'static> { + static SIGNAL_LEASE: Lease = Lease::new(); + SIGNAL_LEASE.acquire() } #[cfg(test)] diff --git a/src/unix_signals/listener.rs b/src/unix_signals/listener.rs index a66da3fe9..a0328dd4f 100644 --- a/src/unix_signals/listener.rs +++ b/src/unix_signals/listener.rs @@ -1,10 +1,11 @@ -use std::fmt; -use std::io; use std::os::fd::{AsRawFd, OwnedFd, RawFd}; use std::panic::{AssertUnwindSafe, catch_unwind}; +#[cfg(test)] +use std::sync::atomic::AtomicI32; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; +use std::{fmt, io}; use super::{ SIGNAL_WRITE_FD, begin_listener_teardown, clear_registered_signals, create_pipe, @@ -54,9 +55,9 @@ struct ListenerShared { on_signal: Box, owner_wake: Arc, #[cfg(test)] - injected_read_error: std::sync::atomic::AtomicI32, + injected_read_error: AtomicI32, #[cfg(test)] - thread_tid: std::sync::atomic::AtomicI32, + thread_tid: AtomicI32, } pub(crate) struct SignalListener { @@ -118,9 +119,9 @@ where on_signal: Box::new(on_signal), owner_wake: Arc::new(owner_wake), #[cfg(test)] - injected_read_error: std::sync::atomic::AtomicI32::new(0), + injected_read_error: AtomicI32::new(0), #[cfg(test)] - thread_tid: std::sync::atomic::AtomicI32::new(0), + thread_tid: AtomicI32::new(0), }); let thread_shared = Arc::clone(&shared); let listener_read_fd = read_fd.as_raw_fd(); @@ -165,7 +166,7 @@ impl SignalListener { self.shared .health .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) + .expect("signal listener health is never locked across a panic") .clone() } @@ -185,7 +186,7 @@ impl SignalListener { .shared .health .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("signal listener health is never locked across a panic"); if matches!(*health, SignalListenerHealth::Running) { *health = SignalListenerHealth::Stopping; } @@ -204,7 +205,8 @@ impl SignalListener { .shared .health .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) = SignalListenerHealth::Stopped; + .expect("signal listener health is never locked across a panic") = + SignalListenerHealth::Stopped; self.teardown_complete = true; match join_result { @@ -250,6 +252,10 @@ impl Drop for SignalListener { } } +#[expect( + clippy::disallowed_methods, + reason = "a panicking listener must surface as a reported listener failure instead of silently ending signal handling" +)] fn listener_thread(read_fd: RawFd, shared: &ListenerShared) { #[cfg(test)] { @@ -270,6 +276,10 @@ fn listener_thread(read_fd: RawFd, shared: &ListenerShared) { } } +#[expect( + clippy::disallowed_methods, + reason = "a panicking signal callback must surface as a reported listener failure instead of silently ending signal handling" +)] fn read_signal_loop(read_fd: RawFd, shared: &ListenerShared) -> Result<(), SignalListenerFailure> { let mut buffer = [0_u8; MAX_SIGNAL_PIPE_BYTES_PER_PASS]; loop { @@ -282,7 +292,7 @@ fn read_signal_loop(read_fd: RawFd, shared: &ListenerShared) -> Result<(), Signa let injected = shared.injected_read_error.swap(0, Ordering::AcqRel); if injected != 0 { let err = io::Error::from_raw_os_error(injected); - return Err(read_failure(err)); + return Err(read_failure(&err)); } } @@ -303,7 +313,7 @@ fn read_signal_loop(read_fd: RawFd, shared: &ListenerShared) -> Result<(), Signa if err.kind() == io::ErrorKind::Interrupted { continue; } - return Err(read_failure(err)); + return Err(read_failure(&err)); } if should_stop(shared) { return Ok(()); @@ -319,7 +329,7 @@ fn read_signal_loop(read_fd: RawFd, shared: &ListenerShared) -> Result<(), Signa } } -fn read_failure(err: io::Error) -> SignalListenerFailure { +fn read_failure(err: &io::Error) -> SignalListenerFailure { SignalListenerFailure::ReadFailed { kind: err.kind(), raw_os_error: err.raw_os_error(), @@ -334,7 +344,7 @@ fn should_stop(shared: &ListenerShared) -> bool { *shared .health .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()), + .expect("signal listener health is never locked across a panic"), SignalListenerHealth::Running ) } @@ -344,7 +354,7 @@ fn publish_failure(shared: &ListenerShared, failure: SignalListenerFailure) { let mut health = shared .health .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + .expect("signal listener health is never locked across a panic"); if matches!(*health, SignalListenerHealth::Running) { *health = SignalListenerHealth::Failed(failure); true @@ -357,6 +367,10 @@ fn publish_failure(shared: &ListenerShared, failure: SignalListenerFailure) { } } +#[expect( + clippy::disallowed_methods, + reason = "a panicking owner wake must not end the listener thread that delivers later signals" +)] fn wake_owner(shared: &ListenerShared) { let wake = Arc::clone(&shared.owner_wake); let _ = catch_unwind(AssertUnwindSafe(move || wake())); diff --git a/src/unix_signals/tests.rs b/src/unix_signals/tests.rs index 4418b4877..e1de81ad3 100644 --- a/src/unix_signals/tests.rs +++ b/src/unix_signals/tests.rs @@ -1,11 +1,14 @@ +use std::fs; + +use crate::backend::wayland::RuntimeWakeSource; + use super::listener::MAX_SIGNAL_PIPE_BYTES_PER_PASS; use super::*; -use std::io; use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Barrier, mpsc}; -use std::thread; use std::time::{Duration, Instant}; +use std::{io, thread}; const TEST_SIGNAL: libc::c_int = libc::SIGWINCH; @@ -115,7 +118,7 @@ fn pending_state_survives_an_unavailable_self_pipe() { #[test] fn wake_before_listener_wait_is_retained_and_callback_precedes_owner_wake() { let _guard = test_signal_lock(); - let owner_wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let owner_wake = RuntimeWakeSource::try_new().unwrap(); let wake_handle = owner_wake.handle(); let callback_published = Arc::new(AtomicBool::new(false)); let callback_state = Arc::clone(&callback_published); @@ -137,7 +140,7 @@ fn wake_before_listener_wait_is_retained_and_callback_precedes_owner_wake() { #[test] fn signal_delivered_while_listener_is_blocked_wakes_owner() { let _guard = test_signal_lock(); - let owner_wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let owner_wake = RuntimeWakeSource::try_new().unwrap(); let wake_handle = owner_wake.handle(); let callbacks = Arc::new(AtomicUsize::new(0)); let callback_count = Arc::clone(&callbacks); @@ -195,7 +198,7 @@ fn signal_burst_coalesces_while_callback_is_in_flight() { #[test] fn read_error_publishes_failure_before_owner_wake_and_retains_endpoints() { let _guard = test_signal_lock(); - let owner_wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let owner_wake = RuntimeWakeSource::try_new().unwrap(); let wake_handle = owner_wake.handle(); let mut listener = spawn_listener(&[TEST_SIGNAL], |_| {}, move || wake_handle.wake().unwrap()).unwrap(); @@ -221,7 +224,7 @@ fn read_error_publishes_failure_before_owner_wake_and_retains_endpoints() { #[test] fn callback_panic_publishes_failure_before_independent_owner_wake() { let _guard = test_signal_lock(); - let owner_wake = crate::backend::wayland::RuntimeWakeSource::new().unwrap(); + let owner_wake = RuntimeWakeSource::try_new().unwrap(); let wake_handle = owner_wake.handle(); let mut listener = spawn_listener( &[TEST_SIGNAL], @@ -387,7 +390,7 @@ fn listener_pass_is_explicitly_bounded() { assert_eq!(MAX_SIGNAL_PIPE_BYTES_PER_PASS, 4096); } -fn wait_for_owner_wake(source: &crate::backend::wayland::RuntimeWakeSource) { +fn wait_for_owner_wake(source: &RuntimeWakeSource) { let mut pollfd = libc::pollfd { fd: source.poll_fd().as_raw_fd(), events: libc::POLLIN, @@ -407,7 +410,7 @@ fn wait_until_listener_blocks(listener: &SignalListener) { if tid == 0 { return false; } - std::fs::read_to_string(format!("/proc/self/task/{tid}/stat")) + fs::read_to_string(format!("/proc/self/task/{tid}/stat")) .ok() .and_then(|stat| { stat.rsplit_once(") ") @@ -473,9 +476,9 @@ fn set_errno(value: libc::c_int) { fn current_sigaction(signal: libc::c_int) -> io::Result { // SAFETY: Zeroed sigaction is filled by libc when the query succeeds. - let mut action = unsafe { std::mem::zeroed::() }; + let mut action = unsafe { mem::zeroed::() }; // SAFETY: Null new action queries the current handler for `signal`. - if unsafe { libc::sigaction(signal, std::ptr::null(), &mut action) } != 0 { + if unsafe { libc::sigaction(signal, ptr::null(), &mut action) } != 0 { return Err(io::Error::last_os_error()); } Ok(action) diff --git a/src/update_check/cache.rs b/src/update_check/cache.rs index d2dbc29d5..790cb06f8 100644 --- a/src/update_check/cache.rs +++ b/src/update_check/cache.rs @@ -6,13 +6,16 @@ //! already been told about (so a notification fires once per release, not once //! per login). +use std::fs::{File, OpenOptions}; use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::{fs, thread}; use log::debug; use serde::{Deserialize, Serialize}; use crate::durable_io::{AtomicWriteOptions, OverwriteMode, PermissionPolicy, SymlinkPolicy}; +use crate::{durable_io, paths, session}; /// Schema version, so a future format change can be detected rather than /// misread. v2 split the single "checked" timestamp in two; v3 records the @@ -64,7 +67,7 @@ pub(crate) struct UpdateCache { /// Cache file path (`$XDG_CACHE_HOME/wayscriber/update-check.json`). pub(crate) fn cache_path() -> PathBuf { - crate::paths::update_check_cache_file() + paths::update_check_cache_file() } /// Advisory lock guarding read-modify-write cycles. @@ -77,7 +80,7 @@ fn lock_path(cache_path: &Path) -> PathBuf { /// or are on the daemon's watcher thread. On timeout the mutation is skipped; /// it must never fall back to an unlocked read-modify-write. const LOCK_ATTEMPTS: u32 = 25; -const LOCK_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(20); +const LOCK_RETRY_DELAY: Duration = Duration::from_millis(20); /// Read-modify-write the cache under an advisory lock. /// @@ -101,18 +104,18 @@ pub(crate) fn update(mutate: impl FnOnce(&mut UpdateCache) -> T) -> Option } /// Holds the advisory lock for as long as it is in scope. -struct CacheLock(std::fs::File); +struct CacheLock(File); impl CacheLock { fn acquire(cache_path: &Path) -> Option { let parent = cache_path.parent()?; - if let Err(err) = std::fs::create_dir_all(parent) { + if let Err(err) = fs::create_dir_all(parent) { debug!("Failed to create update cache dir for locking: {err}"); return None; } let path = lock_path(cache_path); - let file = match std::fs::OpenOptions::new() + let file = match OpenOptions::new() .read(true) .write(true) .create(true) @@ -127,11 +130,11 @@ impl CacheLock { }; for attempt in 0..LOCK_ATTEMPTS { - if crate::session::try_lock_exclusive(&file).is_ok() { + if session::try_lock_exclusive(&file).is_ok() { return Some(Self(file)); } if attempt + 1 < LOCK_ATTEMPTS { - std::thread::sleep(LOCK_RETRY_DELAY); + thread::sleep(LOCK_RETRY_DELAY); } } @@ -142,7 +145,7 @@ impl CacheLock { impl Drop for CacheLock { fn drop(&mut self) { - if let Err(err) = crate::session::unlock(&self.0) { + if let Err(err) = session::unlock(&self.0) { debug!("Failed to release the update cache lock: {err}"); } } @@ -155,7 +158,7 @@ pub(crate) fn load() -> UpdateCache { } pub(crate) fn load_from(path: &Path) -> UpdateCache { - let Ok(raw) = std::fs::read_to_string(path) else { + let Ok(raw) = fs::read_to_string(path) else { return UpdateCache::default(); }; match serde_json::from_str::(&raw) { @@ -209,7 +212,7 @@ fn store_at(path: &Path, cache: &UpdateCache) -> bool { }; if let Some(parent) = path.parent() - && let Err(err) = std::fs::create_dir_all(parent) + && let Err(err) = fs::create_dir_all(parent) { debug!( "Failed to create update cache dir {}: {err}", @@ -218,7 +221,7 @@ fn store_at(path: &Path, cache: &UpdateCache) -> bool { return false; } - if let Err(err) = crate::durable_io::write_text_atomic( + if let Err(err) = durable_io::write_text_atomic( path, &contents, AtomicWriteOptions { @@ -254,6 +257,12 @@ pub(crate) fn seconds_since(then: u64) -> Option { #[cfg(test)] mod tests { + use std::env; + use std::os::unix; + use std::sync::{Arc, Barrier}; + + use crate::{env_vars, test_env}; + use super::*; use crate::test_temp::tempdir; @@ -291,7 +300,7 @@ mod tests { assert_eq!(load_from(&missing), UpdateCache::default()); let corrupt = dir.path().join("corrupt.json"); - std::fs::write(&corrupt, "{not json").unwrap(); + fs::write(&corrupt, "{not json").unwrap(); assert_eq!(load_from(&corrupt), UpdateCache::default()); } @@ -299,7 +308,7 @@ mod tests { fn a_v1_cache_keeps_its_throttle_but_not_a_claim_of_freshness() { let dir = tempdir().unwrap(); let path = dir.path().join("v1.json"); - std::fs::write( + fs::write( &path, r#"{"version": 1, "last_checked_unix": 1750000000, "latest_version": "0.9.23"}"#, ) @@ -315,7 +324,7 @@ mod tests { // Rewriting drops the legacy key entirely. store_at(&path, &cache); - let raw = std::fs::read_to_string(&path).unwrap(); + let raw = fs::read_to_string(&path).unwrap(); assert!(!raw.contains("last_checked_unix"), "{raw}"); assert!(raw.contains("last_attempt_unix"), "{raw}"); } @@ -324,7 +333,7 @@ mod tests { fn a_v2_cache_does_not_guess_an_attempt_outcome_from_timestamps() { let dir = tempdir().unwrap(); let path = dir.path().join("v2.json"); - std::fs::write( + fs::write( &path, r#"{ "version": 2, @@ -348,10 +357,11 @@ mod tests { /// restored stale result. #[test] fn concurrent_updates_do_not_erase_each_other() { - let _lock = crate::test_env::lock(); + let _lock = test_env::lock(); let dir = tempdir().unwrap(); - let previous = std::env::var_os(crate::env_vars::XDG_CACHE_HOME_ENV); - unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; + let previous = env::var_os(env_vars::XDG_CACHE_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_lock`. + unsafe { env::set_var(env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; let mut initial = sample(); initial.notified_version = None; @@ -361,13 +371,13 @@ mod tests { // The slow writer holds the lock across a wide load→store window; the // fast one starts inside that window. Unlocked, the slow store would // land last and revert the fast writer's field. - let entered = std::sync::Arc::new(std::sync::Barrier::new(2)); - let slow_entered = std::sync::Arc::clone(&entered); - let slow = std::thread::spawn(move || { + let entered = Arc::new(Barrier::new(2)); + let slow_entered = Arc::clone(&entered); + let slow = thread::spawn(move || { let _ = update(|cache| { cache.notified_version = Some("0.9.23".to_string()); slow_entered.wait(); - std::thread::sleep(std::time::Duration::from_millis(60)); + thread::sleep(Duration::from_millis(60)); }); }); entered.wait(); @@ -387,18 +397,22 @@ mod tests { // Fields neither writer touched survive both cycles. assert_eq!(final_cache.latest_version.as_deref(), Some("0.9.23")); - match previous { - Some(value) => unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, value) }, - None => unsafe { std::env::remove_var(crate::env_vars::XDG_CACHE_HOME_ENV) }, + // SAFETY: this test still holds the environment mutex in `_lock`. + unsafe { + match previous { + Some(value) => env::set_var(env_vars::XDG_CACHE_HOME_ENV, value), + None => env::remove_var(env_vars::XDG_CACHE_HOME_ENV), + } } } #[test] fn a_timed_out_lock_does_not_fall_back_to_an_unlocked_write() { - let _env_lock = crate::test_env::lock(); + let _env_lock = test_env::lock(); let dir = tempdir().unwrap(); - let previous = std::env::var_os(crate::env_vars::XDG_CACHE_HOME_ENV); - unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; + let previous = env::var_os(env_vars::XDG_CACHE_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_env_lock`. + unsafe { env::set_var(env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; let initial = sample(); store(&initial); @@ -420,23 +434,27 @@ mod tests { ); drop(held); - match previous { - Some(value) => unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, value) }, - None => unsafe { std::env::remove_var(crate::env_vars::XDG_CACHE_HOME_ENV) }, + // SAFETY: this test still holds the environment mutex in `_env_lock`. + unsafe { + match previous { + Some(value) => env::set_var(env_vars::XDG_CACHE_HOME_ENV, value), + None => env::remove_var(env_vars::XDG_CACHE_HOME_ENV), + } } } #[cfg(unix)] #[test] fn a_rejected_cache_write_is_not_reported_as_persisted() { - let _env_lock = crate::test_env::lock(); + let _env_lock = test_env::lock(); let dir = tempdir().unwrap(); - let previous = std::env::var_os(crate::env_vars::XDG_CACHE_HOME_ENV); - unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; + let previous = env::var_os(env_vars::XDG_CACHE_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_env_lock`. + unsafe { env::set_var(env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; let path = cache_path(); - std::fs::create_dir_all(path.parent().unwrap()).unwrap(); - std::os::unix::fs::symlink(dir.path().join("redirected.json"), &path).unwrap(); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + unix::fs::symlink(dir.path().join("redirected.json"), &path).unwrap(); let update_result = update(|cache| { cache.last_attempt_unix = 1_800_000_000; @@ -445,9 +463,12 @@ mod tests { assert_eq!(update_result, None, "a rejected write was not persisted"); assert!(!dir.path().join("redirected.json").exists()); - match previous { - Some(value) => unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, value) }, - None => unsafe { std::env::remove_var(crate::env_vars::XDG_CACHE_HOME_ENV) }, + // SAFETY: this test still holds the environment mutex in `_env_lock`. + unsafe { + match previous { + Some(value) => env::set_var(env_vars::XDG_CACHE_HOME_ENV, value), + None => env::remove_var(env_vars::XDG_CACHE_HOME_ENV), + } } } @@ -455,7 +476,7 @@ mod tests { fn cache_from_a_newer_schema_is_ignored() { let dir = tempdir().unwrap(); let path = dir.path().join("future.json"); - std::fs::write(&path, r#"{"version": 99, "latest_version": "9.9.9"}"#).unwrap(); + fs::write(&path, r#"{"version": 99, "latest_version": "9.9.9"}"#).unwrap(); assert_eq!(load_from(&path), UpdateCache::default()); } diff --git a/src/update_check/fetch.rs b/src/update_check/fetch.rs index f89e8bb6a..c4b6746fc 100644 --- a/src/update_check/fetch.rs +++ b/src/update_check/fetch.rs @@ -13,8 +13,11 @@ use std::path::{Path, PathBuf}; use std::time::Duration; +use std::{env, fs}; use crate::i18n::{tr, trf}; +use crate::process_broker; +use crate::process_broker::HelperKind; use super::manifest::MAX_MANIFEST_BYTES; @@ -62,10 +65,10 @@ fn run_fetcher(program: &str, url: &str, timeout: Duration) -> Result Result Option { - let path = std::env::var_os("PATH").unwrap_or_else(|| "/bin:/usr/bin".into()); - std::env::split_paths(&path) + let path = env::var_os("PATH").unwrap_or_else(|| "/bin:/usr/bin".into()); + env::split_paths(&path) .map(|directory| directory.join(program)) .find(|candidate| is_executable_file(candidate)) } @@ -110,7 +113,7 @@ fn find_in_path(program: &str) -> Option { fn is_executable_file(path: &Path) -> bool { use std::os::unix::fs::PermissionsExt; - std::fs::metadata(path) + fs::metadata(path) .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) } diff --git a/src/update_check/mod.rs b/src/update_check/mod.rs index 33569ed92..79a02c7cc 100644 --- a/src/update_check/mod.rs +++ b/src/update_check/mod.rs @@ -17,13 +17,15 @@ mod fetch; mod manifest; mod version; -use std::time::Duration; +use std::env; +use std::time::{Duration, Instant}; use log::debug; pub(crate) use manifest::is_trusted_url; pub use manifest::{DEFAULT_NOTES_URL, DEFAULT_UPDATE_URL, MANIFEST_URL, install_source}; +use crate::build_info; use crate::env_vars::DISABLE_UPDATE_CHECK_ENV; /// Network timeout for one manifest request. @@ -78,7 +80,7 @@ pub enum CachedStatus { /// The running build's version. pub fn current_version() -> &'static str { - crate::build_info::version() + build_info::version() } /// Whether this build can check at all. @@ -109,7 +111,7 @@ pub fn background_checks_enabled(config_enabled: bool) -> bool { /// switch whose only job is to stop network access, an unrecognized value is /// honored rather than ignored, so a typo cannot silently re-enable the check. fn env_opt_out() -> bool { - std::env::var_os(DISABLE_UPDATE_CHECK_ENV) + env::var_os(DISABLE_UPDATE_CHECK_ENV) .map(|value| { let value = value.to_string_lossy().trim().to_ascii_lowercase(); !matches!( @@ -234,7 +236,7 @@ fn outcome_for(manifest: &manifest::ReleaseManifest) -> CheckOutcome { /// intentionally bypass the interval and therefore do not need it. #[derive(Debug, Default)] pub(crate) struct CheckThrottle { - last_attempt: Option, + last_attempt: Option, } impl CheckThrottle { @@ -267,7 +269,7 @@ impl CheckThrottle { } fn note_attempt(&mut self) { - self.last_attempt = Some(std::time::Instant::now()); + self.last_attempt = Some(Instant::now()); } fn attempted_within(&self, interval: Duration) -> bool { @@ -317,37 +319,52 @@ pub fn update_instructions_url() -> String { #[cfg(test)] mod tests { + use std::ffi::OsString; + + use crate::{env_vars, test_env, test_temp}; + use super::*; /// Restores the opt-out variable when the test ends, however it ends. - struct OptOutEnv(Option); + /// + /// Create it only while holding `test_env::lock()`, and drop it before + /// that guard; its environment writes rely on that lock. + struct OptOutEnv(Option); impl OptOutEnv { fn capture() -> Self { - Self(std::env::var_os(DISABLE_UPDATE_CHECK_ENV)) + Self(env::var_os(DISABLE_UPDATE_CHECK_ENV)) } fn set(&self, value: &str) { - unsafe { std::env::set_var(DISABLE_UPDATE_CHECK_ENV, value) }; + // SAFETY: the caller holds the test environment mutex for this + // value's lifetime (see the type's documentation). + unsafe { env::set_var(DISABLE_UPDATE_CHECK_ENV, value) }; } fn clear(&self) { - unsafe { std::env::remove_var(DISABLE_UPDATE_CHECK_ENV) }; + // SAFETY: the caller holds the test environment mutex for this + // value's lifetime (see the type's documentation). + unsafe { env::remove_var(DISABLE_UPDATE_CHECK_ENV) }; } } impl Drop for OptOutEnv { fn drop(&mut self) { - match self.0.take() { - Some(value) => unsafe { std::env::set_var(DISABLE_UPDATE_CHECK_ENV, value) }, - None => unsafe { std::env::remove_var(DISABLE_UPDATE_CHECK_ENV) }, + // SAFETY: the caller holds the test environment mutex until after + // this value drops (see the type's documentation). + unsafe { + match self.0.take() { + Some(value) => env::set_var(DISABLE_UPDATE_CHECK_ENV, value), + None => env::remove_var(DISABLE_UPDATE_CHECK_ENV), + } } } } #[test] fn env_opt_out_recognizes_falsey_values() { - let _lock = crate::test_env::lock(); + let _lock = test_env::lock(); let env = OptOutEnv::capture(); env.set("1"); @@ -399,10 +416,11 @@ mod tests { /// turning a daily check into one request per daemon wakeup. #[test] fn each_watcher_owns_its_in_process_attempt_throttle() { - let _lock = crate::test_env::lock(); - let dir = crate::test_temp::tempdir().unwrap(); - let previous = std::env::var_os(crate::env_vars::XDG_CACHE_HOME_ENV); - unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; + let _lock = test_env::lock(); + let dir = test_temp::tempdir().unwrap(); + let previous = env::var_os(env_vars::XDG_CACHE_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_lock`. + unsafe { env::set_var(env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; let mut attempted = CheckThrottle::default(); attempted.note_attempt(); @@ -416,9 +434,12 @@ mod tests { assert!(!attempted.is_due_now(Duration::from_secs(86_400))); assert!(untouched.is_due_now(Duration::from_secs(86_400))); - match previous { - Some(value) => unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, value) }, - None => unsafe { std::env::remove_var(crate::env_vars::XDG_CACHE_HOME_ENV) }, + // SAFETY: this test still holds the environment mutex in `_lock`. + unsafe { + match previous { + Some(value) => env::set_var(env_vars::XDG_CACHE_HOME_ENV, value), + None => env::remove_var(env_vars::XDG_CACHE_HOME_ENV), + } } } @@ -451,10 +472,11 @@ mod tests { /// the dialog reads back, with the install-source anchor applied. #[test] fn cached_status_reports_what_the_last_check_stored() { - let _lock = crate::test_env::lock(); - let dir = crate::test_temp::tempdir().unwrap(); - let previous = std::env::var_os(crate::env_vars::XDG_CACHE_HOME_ENV); - unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; + let _lock = test_env::lock(); + let dir = test_temp::tempdir().unwrap(); + let previous = env::var_os(env_vars::XDG_CACHE_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_lock`. + unsafe { env::set_var(env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; assert_eq!(cached_status(), CachedStatus::Never(Freshness::default())); @@ -535,18 +557,22 @@ mod tests { }) ); - match previous { - Some(value) => unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, value) }, - None => unsafe { std::env::remove_var(crate::env_vars::XDG_CACHE_HOME_ENV) }, + // SAFETY: this test still holds the environment mutex in `_lock`. + unsafe { + match previous { + Some(value) => env::set_var(env_vars::XDG_CACHE_HOME_ENV, value), + None => env::remove_var(env_vars::XDG_CACHE_HOME_ENV), + } } } #[test] fn a_first_persisted_failure_remains_visible_after_reopening_about() { - let _lock = crate::test_env::lock(); - let dir = crate::test_temp::tempdir().unwrap(); - let previous = std::env::var_os(crate::env_vars::XDG_CACHE_HOME_ENV); - unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; + let _lock = test_env::lock(); + let dir = test_temp::tempdir().unwrap(); + let previous = env::var_os(env_vars::XDG_CACHE_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_lock`. + unsafe { env::set_var(env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; record_attempt(None); @@ -558,18 +584,22 @@ mod tests { }) ); - match previous { - Some(value) => unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, value) }, - None => unsafe { std::env::remove_var(crate::env_vars::XDG_CACHE_HOME_ENV) }, + // SAFETY: this test still holds the environment mutex in `_lock`. + unsafe { + match previous { + Some(value) => env::set_var(env_vars::XDG_CACHE_HOME_ENV, value), + None => env::remove_var(env_vars::XDG_CACHE_HOME_ENV), + } } } #[test] fn a_same_second_failure_is_not_mistaken_for_the_previous_success() { - let _lock = crate::test_env::lock(); - let dir = crate::test_temp::tempdir().unwrap(); - let previous = std::env::var_os(crate::env_vars::XDG_CACHE_HOME_ENV); - unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; + let _lock = test_env::lock(); + let dir = test_temp::tempdir().unwrap(); + let previous = env::var_os(env_vars::XDG_CACHE_HOME_ENV); + // SAFETY: serialized by the test environment mutex held in `_lock`. + unsafe { env::set_var(env_vars::XDG_CACHE_HOME_ENV, dir.path()) }; let stamp = cache::now_unix(); let mut seeded = cache::load(); @@ -587,9 +617,12 @@ mod tests { }) )); - match previous { - Some(value) => unsafe { std::env::set_var(crate::env_vars::XDG_CACHE_HOME_ENV, value) }, - None => unsafe { std::env::remove_var(crate::env_vars::XDG_CACHE_HOME_ENV) }, + // SAFETY: this test still holds the environment mutex in `_lock`. + unsafe { + match previous { + Some(value) => env::set_var(env_vars::XDG_CACHE_HOME_ENV, value), + None => env::remove_var(env_vars::XDG_CACHE_HOME_ENV), + } } } diff --git a/src/util/arrow.rs b/src/util/arrow.rs index ffe2226a9..02fcb3962 100644 --- a/src/util/arrow.rs +++ b/src/util/arrow.rs @@ -650,7 +650,8 @@ pub(crate) fn calculate_arrow_outline_styled( let mut left = Vec::with_capacity(samples.len() + 5); let mut right = Vec::with_capacity(samples.len()); - for sample in samples.iter().take_while(|s| s.arc <= join_arc) { + let shaft_samples = samples.iter().take_while(|s| s.arc <= join_arc); + for sample in shaft_samples { let progress = (sample.arc / join_arc).clamp(0.0, 1.0); let half_width = tail_half + (shoulder_half - tail_half) * progress; let normal = left_normal(sample.tangent); diff --git a/src/util/colors.rs b/src/util/colors.rs index 26db81193..02aa9e858 100644 --- a/src/util/colors.rs +++ b/src/util/colors.rs @@ -1,4 +1,8 @@ -use crate::draw::{Color, color::*}; +use crate::draw::Color; +use crate::draw::color::{ + PALETTE_BLACK, PALETTE_BLUE, PALETTE_GREEN, PALETTE_ORANGE, PALETTE_PINK, PALETTE_RED, + PALETTE_WHITE, PALETTE_YELLOW, +}; use crate::i18n::tr; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -24,6 +28,7 @@ pub enum ConfigHexColorError { /// # Returns /// - `Some(Color)` if the name matches a predefined color /// - `None` if the name is not recognized +#[must_use] pub fn name_to_color(name: &str) -> Option { match name.to_lowercase().as_str() { "red" => Some(PALETTE_RED), @@ -82,6 +87,7 @@ pub fn parse_config_hex_color(value: &str) -> Result /// # Returns /// A static string with the color name, or "Custom" if the color doesn't /// match any predefined color. +#[must_use] pub fn color_to_name(color: &Color) -> &'static str { // Match the tuned palette first so named colors and quick color defaults // report their names instead of "Custom". diff --git a/src/util/geometry.rs b/src/util/geometry.rs index 7f98a5672..40fb6ab2e 100644 --- a/src/util/geometry.rs +++ b/src/util/geometry.rs @@ -32,6 +32,7 @@ pub struct Rect { impl Rect { /// Creates a new rectangle. Width/height must be non-negative. + #[must_use] pub fn new(x: i32, y: i32, width: i32, height: i32) -> Option { if width <= 0 || height <= 0 { None @@ -46,6 +47,7 @@ impl Rect { } /// Builds a rectangle from min/max bounds (inclusive min, exclusive max). + #[must_use] pub fn from_min_max(min_x: i32, min_y: i32, max_x: i32, max_y: i32) -> Option { let width = i32::try_from(i64::from(max_x) - i64::from(min_x)).ok()?; let height = i32::try_from(i64::from(max_y) - i64::from(min_y)).ok()?; @@ -53,6 +55,7 @@ impl Rect { } /// Returns true if rectangle has a positive area. + #[must_use] pub fn is_valid(&self) -> bool { self.width > 0 && self.height > 0 } @@ -73,6 +76,7 @@ impl Rect { } /// Returns true if the point lies within the rectangle (inclusive of min, exclusive of max). + #[must_use] pub fn contains(&self, x: i32, y: i32) -> bool { let x = i64::from(x); let y = i64::from(y); @@ -85,6 +89,7 @@ impl Rect { } /// Returns a new rectangle inflated by `amount` in all directions. + #[must_use] pub fn inflated(&self, amount: i32) -> Option { if !self.is_valid() { return None; @@ -117,6 +122,7 @@ impl Rect { /// - `cx`, `cy` = center point coordinates /// - `rx` = horizontal radius (half width) /// - `ry` = vertical radius (half height) +#[must_use] pub fn ellipse_bounds(x1: i32, y1: i32, x2: i32, y2: i32) -> (i32, i32, i32, i32) { let (cx, rx) = ellipse_axis_bounds(x1, x2); let (cy, ry) = ellipse_axis_bounds(y1, y2); diff --git a/src/util/text.rs b/src/util/text.rs index 5d086b6c4..7e7209483 100644 --- a/src/util/text.rs +++ b/src/util/text.rs @@ -1,6 +1,7 @@ //! Text utility functions. /// Truncate a string to a maximum length, adding ellipsis if truncated. +#[must_use] pub fn truncate_with_ellipsis(text: &str, max_len: usize) -> String { if max_len == 0 { return text.to_string(); diff --git a/src/zbus_stream.rs b/src/zbus_stream.rs index 5c756f50c..c5c962f77 100644 --- a/src/zbus_stream.rs +++ b/src/zbus_stream.rs @@ -1,4 +1,5 @@ -use std::{future::poll_fn, pin::Pin}; +use std::future::poll_fn; +use std::pin::Pin; use zbus::export::futures_core::Stream; diff --git a/tests/AGENTS.md b/tests/AGENTS.md index 35f7b6674..dcedd5c31 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -6,7 +6,7 @@ ## Architecture - `tests/cli.rs` covers CLI behavior. - `tests/ui.rs` covers UI smoke/integration behavior that can run without an ungated visible overlay. -- `tests/repository_guards/` holds the source guards (process sites, config writers, shared dependencies, no Python). They read the checkout, not compiled items, and each guard keeps regression cases showing its forbidden escapes fail: most edit a copy of the checked-out `source::Tree`, the shared-dependency corpus builds small trees from `shared_dependency_fixtures.json`, and `no_python.rs` audits small in-memory file sets. +- `tests/repository_guards/` holds the source guards (process sites, config writers, shared dependencies, use statements, no Python). They read the checkout, not compiled items, and each guard keeps regression cases showing its forbidden escapes fail: most edit a copy of the checked-out `source::Tree`, the shared-dependency corpus builds small trees from `shared_dependency_fixtures.json`, and `no_python.rs` audits small in-memory file sets. ## Invariants - Tests should not launch visible Wayland overlays, steal focus, or interact with foreground/fullscreen apps by default. diff --git a/tests/cli.rs b/tests/cli.rs index b3e674681..00838a2ef 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,4 +1,4 @@ -use std::fs; +use std::ffi::OsString; #[cfg(unix)] use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; @@ -9,6 +9,7 @@ use std::process::{Command, Output}; use std::thread; #[cfg(unix)] use std::time::{Duration, Instant}; +use std::{env, fs}; use wayscriber::env_vars::{ HOME_ENV, NO_DETACH_ENV, WAYLAND_DISPLAY_ENV, XDG_CONFIG_HOME_ENV, XDG_CURRENT_DESKTOP_ENV, XDG_DATA_HOME_ENV, XDG_RUNTIME_DIR_ENV, XDG_SESSION_DESKTOP_ENV, @@ -20,6 +21,13 @@ use wayscriber::env_vars::{ use wayscriber::runtime_capabilities::RUNTIME_CAPABILITIES_FLAG; use tempfile::TempDir; +use wayscriber::config::Config; +use wayscriber::draw::{Color, EraserKind, FontDescriptor, Frame, Shape}; +use wayscriber::input::EraserMode; +use wayscriber::session::{ + BoardPagesSnapshot, BoardSnapshot, SessionOptions, SessionSnapshot, ToolStateSnapshot, +}; +use wayscriber::{build_info, draw, runtime_capabilities, session, shortcut_hint}; struct CommandOutput { output: Output, @@ -160,7 +168,7 @@ fn run_command_with_timeout( fn write_session_config(temp: &TempDir, custom_dir: &Path) { let config_dir = temp.path().join("wayscriber"); - fs::create_dir_all(&config_dir).unwrap(); + fs::create_dir_all(&config_dir).expect("create test config directory"); let config_contents = format!( r#" [session] @@ -178,12 +186,12 @@ backup_retention = 1 "#, custom_dir.display() ); - fs::write(config_dir.join("config.toml"), config_contents).unwrap(); + fs::write(config_dir.join("config.toml"), config_contents).expect("write test session config"); } fn write_disabled_session_config(temp: &TempDir, custom_dir: &Path) { let config_dir = temp.path().join("wayscriber"); - fs::create_dir_all(&config_dir).unwrap(); + fs::create_dir_all(&config_dir).expect("create test config directory"); let config_contents = format!( r#" [session] @@ -202,12 +210,12 @@ backup_retention = 1 "#, custom_dir.display() ); - fs::write(config_dir.join("config.toml"), config_contents).unwrap(); + fs::write(config_dir.join("config.toml"), config_contents).expect("write test session config"); } -fn saved_tool_state() -> wayscriber::session::ToolStateSnapshot { - wayscriber::session::ToolStateSnapshot { - current_color: wayscriber::draw::Color { +fn saved_tool_state() -> ToolStateSnapshot { + ToolStateSnapshot { + current_color: Color { r: 0.0, g: 1.0, b: 0.0, @@ -215,8 +223,8 @@ fn saved_tool_state() -> wayscriber::session::ToolStateSnapshot { }, current_thickness: 3.0, eraser_size: 12.0, - eraser_kind: wayscriber::draw::EraserKind::Circle, - eraser_mode: wayscriber::input::EraserMode::Brush, + eraser_kind: EraserKind::Circle, + eraser_mode: EraserMode::Brush, blur_style: Default::default(), recent_colors: Vec::new(), pen_smoothing: None, @@ -226,27 +234,27 @@ fn saved_tool_state() -> wayscriber::session::ToolStateSnapshot { fill_enabled: Some(false), tool_override: None, current_font_size: 24.0, - font_descriptor: Some(wayscriber::draw::FontDescriptor::default()), + font_descriptor: Some(FontDescriptor::default()), text_background_enabled: false, arrow_length: 20.0, arrow_angle: 30.0, arrow_head_at_end: Some(false), arrow_style: None, arrow_label_enabled: Some(false), - polygon_sides: wayscriber::draw::REGULAR_POLYGON_DEFAULT_SIDES, + polygon_sides: draw::REGULAR_POLYGON_DEFAULT_SIDES, board_previous_color: None, tool_settings: None, } } -fn saved_line_snapshot(with_tool_state: bool) -> wayscriber::session::SessionSnapshot { - let mut frame = wayscriber::draw::Frame::new(); - frame.add_shape(wayscriber::draw::Shape::Line { +fn saved_line_snapshot(with_tool_state: bool) -> SessionSnapshot { + let mut frame = Frame::new(); + frame.add_shape(Shape::Line { x1: 0, y1: 0, x2: 10, y2: 10, - color: wayscriber::draw::Color { + color: Color { r: 1.0, g: 0.0, b: 0.0, @@ -255,12 +263,12 @@ fn saved_line_snapshot(with_tool_state: bool) -> wayscriber::session::SessionSna thick: 2.0, }); - wayscriber::session::SessionSnapshot { + SessionSnapshot { active_board_id: "transparent".to_string(), - boards: vec![wayscriber::session::BoardSnapshot { + boards: vec![BoardSnapshot { appearance: None, id: "transparent".to_string(), - pages: wayscriber::session::BoardPagesSnapshot { + pages: BoardPagesSnapshot { pages: vec![frame], active: 0, }, @@ -292,19 +300,19 @@ fn wayscriber_version_prints_binary_name() { .success() .code(0) .stdout_starts_with("wayscriber ") - .stdout_contains(wayscriber::build_info::version()); + .stdout_contains(build_info::version()); } } #[test] fn wayscriber_runtime_capabilities_reports_portal_feature() { - let expected = wayscriber::runtime_capabilities::render_runtime_capabilities( - wayscriber::runtime_capabilities::current_runtime_capabilities(), + let expected = runtime_capabilities::render_runtime_capabilities( + runtime_capabilities::current_runtime_capabilities(), ); assert!( expected.starts_with(&format!( "portal={}\n", - wayscriber::shortcut_hint::portal_runtime_supported() + shortcut_hint::portal_runtime_supported() )), "the rendered report must lead with the portal capability" ); @@ -405,7 +413,7 @@ fn daemon_lock_conflict_exits_seventy_five_without_starting_runtime_services() { .truncate(false) .open(&lock_path) .unwrap(); - wayscriber::session::try_lock_exclusive(&lock_file).unwrap(); + session::try_lock_exclusive(&lock_file).unwrap(); let mut command = wayscriber_cmd(); command @@ -585,20 +593,18 @@ fn clear_tool_state_named_session_targets_only_requested_file() { let selected = temp.path().join("lecture-04.wayscriber-session"); let sibling = temp.path().join("lecture-05.wayscriber-session"); - let mut selected_options = - wayscriber::session::SessionOptions::new(temp.path().to_path_buf(), "display"); + let mut selected_options = SessionOptions::new(temp.path().to_path_buf(), "display"); selected_options.set_named_file_target(selected.clone()); selected_options.persist_transparent = true; selected_options.restore_tool_state = true; - let mut sibling_options = - wayscriber::session::SessionOptions::new(temp.path().to_path_buf(), "display"); + let mut sibling_options = SessionOptions::new(temp.path().to_path_buf(), "display"); sibling_options.set_named_file_target(sibling); sibling_options.persist_transparent = true; sibling_options.restore_tool_state = true; - wayscriber::session::save_snapshot(&saved_line_snapshot(true), &selected_options).unwrap(); - wayscriber::session::save_snapshot(&saved_line_snapshot(true), &sibling_options).unwrap(); + session::save_snapshot(&saved_line_snapshot(true), &selected_options).unwrap(); + session::save_snapshot(&saved_line_snapshot(true), &sibling_options).unwrap(); run_command( wayscriber_cmd() @@ -613,10 +619,10 @@ fn clear_tool_state_named_session_targets_only_requested_file() { .stdout_contains("Cleared saved tool state") .stdout_contains("Preserved saved boards and history"); - let selected_loaded = wayscriber::session::load_snapshot(&selected_options) + let selected_loaded = session::load_snapshot(&selected_options) .unwrap() .expect("selected session should remain"); - let sibling_loaded = wayscriber::session::load_snapshot(&sibling_options) + let sibling_loaded = session::load_snapshot(&sibling_options) .unwrap() .expect("sibling session should remain"); @@ -651,7 +657,7 @@ fn named_session_clear_rejects_symlink_primary_without_removing_artifacts() { let temp = TempDir::new().unwrap(); let target = temp.path().join("real-session.wayscriber-session"); let link = temp.path().join("linked-session.wayscriber-session"); - let mut backup_raw = std::ffi::OsString::from(link.as_os_str()); + let mut backup_raw = OsString::from(link.as_os_str()); backup_raw.push(".bak"); let backup = PathBuf::from(backup_raw); fs::write(&target, b"target").unwrap(); @@ -804,43 +810,46 @@ fn session_info_reports_saved_snapshot() { write_session_config(&temp, &session_dir); let display = "test-session"; - let original_config = std::env::var_os(XDG_CONFIG_HOME_ENV); + let original_config = env::var_os(XDG_CONFIG_HOME_ENV); + // SAFETY: no lock guards this write. This is the only test in this binary + // that changes the environment, and the tests that may run alongside it + // read the environment only through std (`Command` spawns), which + // serializes those reads with this call. unsafe { - std::env::set_var(XDG_CONFIG_HOME_ENV, temp.path()); + env::set_var(XDG_CONFIG_HOME_ENV, temp.path()); } - let original_display = std::env::var_os(WAYLAND_DISPLAY_ENV); + let original_display = env::var_os(WAYLAND_DISPLAY_ENV); + // SAFETY: as for `XDG_CONFIG_HOME` above. unsafe { - std::env::set_var(WAYLAND_DISPLAY_ENV, display); + env::set_var(WAYLAND_DISPLAY_ENV, display); } - let loaded = wayscriber::config::Config::load().unwrap(); - let config_dir = - wayscriber::config::Config::config_directory_from_source(&loaded.source).unwrap(); - let mut options = wayscriber::session::options_from_config( - &loaded.config.session, - &config_dir, - Some(display), - ) - .unwrap(); + let loaded = Config::load().unwrap(); + let config_dir = Config::config_directory_from_source(&loaded.source).unwrap(); + let mut options = + session::options_from_config(&loaded.config.session, &config_dir, Some(display)).unwrap(); options.set_output_identity(Some("DP-1")); - match original_config { - Some(value) => unsafe { std::env::set_var(XDG_CONFIG_HOME_ENV, value) }, - None => unsafe { std::env::remove_var(XDG_CONFIG_HOME_ENV) }, - } + // SAFETY: as for the writes above; the restore runs on the same thread. + unsafe { + match original_config { + Some(value) => env::set_var(XDG_CONFIG_HOME_ENV, value), + None => env::remove_var(XDG_CONFIG_HOME_ENV), + } - match original_display { - Some(value) => unsafe { std::env::set_var(WAYLAND_DISPLAY_ENV, value) }, - None => unsafe { std::env::remove_var(WAYLAND_DISPLAY_ENV) }, + match original_display { + Some(value) => env::set_var(WAYLAND_DISPLAY_ENV, value), + None => env::remove_var(WAYLAND_DISPLAY_ENV), + } } - let mut frame = wayscriber::draw::Frame::new(); - frame.add_shape(wayscriber::draw::Shape::Line { + let mut frame = Frame::new(); + frame.add_shape(Shape::Line { x1: 0, y1: 0, x2: 10, y2: 10, - color: wayscriber::draw::Color { + color: Color { r: 1.0, g: 0.0, b: 0.0, @@ -849,12 +858,12 @@ fn session_info_reports_saved_snapshot() { thick: 2.0, }); - let snapshot = wayscriber::session::SessionSnapshot { + let snapshot = SessionSnapshot { active_board_id: "transparent".to_string(), - boards: vec![wayscriber::session::BoardSnapshot { + boards: vec![BoardSnapshot { appearance: None, id: "transparent".to_string(), - pages: wayscriber::session::BoardPagesSnapshot { + pages: BoardPagesSnapshot { pages: vec![frame], active: 0, }, @@ -862,7 +871,7 @@ fn session_info_reports_saved_snapshot() { tool_state: None, }; - wayscriber::session::save_snapshot(&snapshot, &options).unwrap(); + session::save_snapshot(&snapshot, &options).unwrap(); run_command( wayscriber_cmd() diff --git a/tests/daemon_v1_fixture.rs b/tests/daemon_v1_fixture.rs index 53810141f..4f59f516e 100644 --- a/tests/daemon_v1_fixture.rs +++ b/tests/daemon_v1_fixture.rs @@ -1,7 +1,7 @@ -use std::fs; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Output}; use std::time::{Duration, Instant}; +use std::{env, fs, thread}; const FROZEN_V1_SOURCE: &str = include_str!("fixtures/frozen_daemon_v1.rs"); use tempfile::TempDir; @@ -24,8 +24,8 @@ impl Drop for ChildGuard { fn compile_fixture(root: &Path) -> PathBuf { let source = root.join("frozen_daemon_v1.rs"); let binary = root.join("frozen-daemon-v1"); - fs::write(&source, FROZEN_V1_SOURCE).unwrap(); - let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); + fs::write(&source, FROZEN_V1_SOURCE).expect("write frozen v1 fixture source"); + let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); let output = Command::new(rustc) .arg("--edition=2024") .arg("-C") @@ -51,20 +51,20 @@ fn wait_for(path: &Path) { "timed out waiting for {}", path.display() ); - std::thread::sleep(Duration::from_millis(2)); + thread::sleep(Duration::from_millis(2)); } } fn read_report(root: &Path) -> (usize, usize) { wait_for(&root.join("fixture-report")); - let report = fs::read_to_string(root.join("fixture-report")).unwrap(); + let report = fs::read_to_string(root.join("fixture-report")).expect("read fixture report"); let value = |name: &str| { report .lines() .find_map(|line| line.strip_prefix(&format!("{name}="))) - .unwrap() + .expect("fixture report lists every counter") .parse::() - .unwrap() + .expect("fixture report counters are integers") }; (value("signals"), value("typed")) } @@ -79,7 +79,7 @@ fn wait_for_report(root: &Path, expected_signals: usize, expected_typed: usize) Instant::now() < deadline, "fixture report did not reach expected counts" ); - std::thread::sleep(Duration::from_millis(2)); + thread::sleep(Duration::from_millis(2)); } } @@ -92,8 +92,8 @@ fn run_client(binary: &Path, mode: &str, root: &Path) -> Output { } fn stop_daemon(daemon: &mut Child) { - daemon.kill().unwrap(); - daemon.wait().unwrap(); + daemon.kill().expect("kill fixture daemon"); + daemon.wait().expect("reap fixture daemon"); } #[test] diff --git a/tests/i18n.rs b/tests/i18n.rs index 148e79248..1bebeefdd 100644 --- a/tests/i18n.rs +++ b/tests/i18n.rs @@ -12,12 +12,14 @@ const OVERRIDE: &str = "WAYSCRIBER_LANGUAGE"; const LOCALE_DIR: &str = "WAYSCRIBER_LOCALE_DIR"; fn set_env(name: &str, value: &str) { - // Safe here: this test binary runs one test at a time and owns the - // variables it touches. + // SAFETY: this test binary holds a single test, which spawns no threads, + // so nothing else in the process reads or writes the environment. unsafe { env::set_var(name, value) }; } fn unset_env(name: &str) { + // SAFETY: this test binary holds a single test, which spawns no threads, + // so nothing else in the process reads or writes the environment. unsafe { env::remove_var(name) }; } diff --git a/tests/repository_guards/config_writers.rs b/tests/repository_guards/config_writers.rs index 095aac25b..79f83a99e 100644 --- a/tests/repository_guards/config_writers.rs +++ b/tests/repository_guards/config_writers.rs @@ -450,15 +450,15 @@ fn trait_methods(body: &str) -> BTreeSet { .collect() } -struct Source<'a> { - path: &'a str, - text: &'a str, +struct Source<'source> { + path: &'source str, + text: &'source str, masked: String, test_spans: Spans, } -impl<'a> Source<'a> { - fn read(tree: &'a Tree, path: &'a str) -> (Self, Vec) { +impl<'source> Source<'source> { + fn read(tree: &'source Tree, path: &'source str) -> (Self, Vec) { let text = tree .read(Path::new(path)) .unwrap_or_else(|| panic!("{path} is missing")); @@ -485,13 +485,13 @@ impl<'a> Source<'a> { } } -struct Audit<'a> { - tree: &'a Tree, +struct Audit<'tree> { + tree: &'tree Tree, failures: Vec, test_sources: TestSources, } -impl<'a> Audit<'a> { +impl<'tree> Audit<'tree> { fn fail(&mut self, failure: String) { self.failures.push(failure); } @@ -1042,8 +1042,8 @@ fn checkout() -> Tree { Tree::checkout(&["src", "configurator/src"]) } -fn assert_fails(tree: Tree, expected: &str) { - let failures = audit(&tree); +fn assert_fails(tree: &Tree, expected: &str) { + let failures = audit(tree); assert!( failures.iter().any(|failure| failure.contains(expected)), @@ -1061,7 +1061,7 @@ fn only_the_reviewed_writers_can_write_config_toml() { #[test] fn a_primitive_outside_the_reviewed_writers_fails() { assert_fails( - checkout().appending( + &checkout().appending( "src/daemon/core.rs", "\nfn leak(document: &ConfigDocument) { document.save_with_backup(config()); }\n", ), @@ -1082,7 +1082,7 @@ fn only_a_cfg_test_module_makes_a_tests_directory_test_code() { }; assert_fails( - tree("mod tests;\n"), + &tree("mod tests;\n"), "src/probe/tests/leak.rs:1: config write capability `create_config_backup`", ); let failures = audit(&tree("#[cfg(test)]\nmod tests;\n")); @@ -1092,7 +1092,7 @@ fn only_a_cfg_test_module_makes_a_tests_directory_test_code() { #[test] fn editor_calls_are_pinned_to_their_reviewed_callers() { assert_fails( - checkout().appending( + &checkout().appending( "src/backend/wayland/state.rs", "\nfn sneak() { crate::config::persist_quick_color(0, color()); }\n", ), @@ -1100,7 +1100,7 @@ fn editor_calls_are_pinned_to_their_reviewed_callers() { `persist_quick_color`", ); assert_fails( - checkout() + &checkout() .replacing(EDIT_WORKER, "\n persist_quick_color,\n", "\n") .replacing( EDIT_WORKER, @@ -1114,14 +1114,14 @@ fn editor_calls_are_pinned_to_their_reviewed_callers() { #[test] fn path_taking_twins_stay_out_of_production() { assert_fails( - checkout().appending( + &checkout().appending( "src/daemon/core.rs", "\nfn f() { persist_preset_slot_at(path, 0, None); }\n", ), "src/daemon/core.rs: production code names `persist_preset_slot_at`", ); assert_fails( - checkout().replacing( + &checkout().replacing( IO_SOURCE, "#[cfg(test)]\npub(crate) fn persist_quick_color_at(", "pub(crate) fn persist_quick_color_at(", @@ -1133,7 +1133,7 @@ fn path_taking_twins_stay_out_of_production() { #[test] fn editors_are_not_wrapped_elsewhere_in_the_config_module() { assert_fails( - checkout().appending( + &checkout().appending( "src/config/types/mod.rs", "\npub fn wrapped() { super::io::persist_keybinding_edit(action(), &[]); }\n", ), @@ -1145,7 +1145,7 @@ fn editors_are_not_wrapped_elsewhere_in_the_config_module() { fn casting_a_writer_to_a_value_fails() { // The edit worker may name the editor, but not hand it on as a value. assert_fails( - checkout().appending( + &checkout().appending( EDIT_WORKER, "\nfn leak() -> fn() { persist_quick_color as fn() }\n", ), @@ -1156,14 +1156,14 @@ fn casting_a_writer_to_a_value_fails() { #[test] fn renaming_a_writer_on_import_or_reexport_fails() { assert_fails( - checkout().appending( + &checkout().appending( EDIT_WORKER, "\nuse crate::config::persist_preset_slot as save;\n", ), "imports the config writer `persist_preset_slot` as `save`", ); assert_fails( - checkout().appending( + &checkout().appending( CONFIG_MODULE, "\npub use io::persist_keybinding_edit as concealed_edit;\n", ), @@ -1174,7 +1174,7 @@ fn renaming_a_writer_on_import_or_reexport_fails() { #[test] fn a_new_document_entry_that_reaches_the_write_fails() { assert_fails( - checkout().replacing( + &checkout().replacing( DOCUMENT_SOURCE, " fn merge_and_write(", " pub fn persist_any_config(&self, config: &Config) {\n \ @@ -1188,7 +1188,7 @@ fn a_new_document_entry_that_reaches_the_write_fails() { #[test] fn a_new_io_entry_that_reaches_a_primitive_fails() { assert_fails( - checkout().appending( + &checkout().appending( IO_SOURCE, "\nfn helper(path: &Path) -> Result<()> { prepare_config_parent(path) }\n\ pub fn persist_everything(path: &Path) -> Result<()> { helper(path) }\n", @@ -1200,7 +1200,7 @@ fn a_new_io_entry_that_reaches_a_primitive_fails() { #[test] fn a_write_reachable_through_a_trait_fails() { assert_fails( - checkout().appending( + &checkout().appending( DOCUMENT_SOURCE, "\nimpl Persist for ConfigDocument {\n fn persist(&self, config: &Config) {\n \ self.merge_and_write(&self.config, config, &mut || {});\n }\n}\n", @@ -1208,7 +1208,7 @@ fn a_write_reachable_through_a_trait_fails() { "`fn persist` can write config.toml from inside a trait", ); assert_fails( - checkout().appending( + &checkout().appending( IO_SOURCE, "\npub trait Concealed {\n fn persist_quick_color(&self);\n}\n", ), @@ -1219,7 +1219,7 @@ fn a_write_reachable_through_a_trait_fails() { #[test] fn a_writer_stored_as_a_value_fails_on_both_halves() { assert_fails( - checkout().appending( + &checkout().appending( DOCUMENT_SOURCE, "\npub const PERSIST_ANY_CONFIG: fn(&ConfigDocument, Config) -> \ Result = ConfigDocument::save_with_backup;\n", @@ -1227,7 +1227,7 @@ fn a_writer_stored_as_a_value_fails_on_both_halves() { "`const PERSIST_ANY_CONFIG` names `save_with_backup`", ); assert_fails( - checkout() + &checkout() .appending(IO_SOURCE, "\npub static WRITE_CONFIG: () = ();\n") .appending(CONFIG_MODULE, "\npub use io::WRITE_CONFIG;\n"), "re-exports `WRITE_CONFIG`, a `static` declared in src/config/io.rs", @@ -1237,14 +1237,14 @@ fn a_writer_stored_as_a_value_fails_on_both_halves() { #[test] fn the_config_module_stays_a_reexport_list() { assert_fails( - checkout().appending( + &checkout().appending( CONFIG_MODULE, "\npub fn concealed_edit() { io::persist_keybinding_edit(action(), &[]); }\n", ), "names the config writer `persist_keybinding_edit` inside `fn concealed_edit`", ); assert_fails( - checkout().appending( + &checkout().appending( CONFIG_MODULE, "\npub fn indirect() { concealed(); }\n\ fn concealed() { io::persist_quick_color(0, color()); }\n", @@ -1256,11 +1256,11 @@ fn the_config_module_stays_a_reexport_list() { #[test] fn primitives_never_leave_the_config_module() { assert_fails( - checkout().appending(CONFIG_MODULE, "\npub use io::create_config_backup;\n"), + &checkout().appending(CONFIG_MODULE, "\npub use io::create_config_backup;\n"), "re-exports the write primitive `create_config_backup`", ); assert_fails( - checkout().replacing( + &checkout().replacing( IO_SOURCE, "pub(super) fn prepare_config_parent(", "pub(crate) fn prepare_config_parent(", @@ -1272,7 +1272,7 @@ fn primitives_never_leave_the_config_module() { #[test] fn editors_build_on_the_loaded_config() { assert_fails( - checkout().appending( + &checkout().appending( IO_SOURCE, "\nfn base(document: &ConfigDocument) -> &Config { document.authored_config() }\n", ), @@ -1283,7 +1283,7 @@ fn editors_build_on_the_loaded_config() { #[test] fn the_walk_must_reach_every_former_writer() { assert_fails( - checkout().without("src/daemon/tray/runtime.rs"), + &checkout().without("src/daemon/tray/runtime.rs"), "the walk missed expected sources, so it proves nothing: src/daemon/tray/runtime.rs", ); } diff --git a/tests/repository_guards/main.rs b/tests/repository_guards/main.rs index c07db9226..db22496a4 100644 --- a/tests/repository_guards/main.rs +++ b/tests/repository_guards/main.rs @@ -11,3 +11,4 @@ mod process_sites; mod shared_dependencies; mod source; mod translations; +mod use_statements; diff --git a/tests/repository_guards/no_python.rs b/tests/repository_guards/no_python.rs index ed6b3ce80..caa8e43c0 100644 --- a/tests/repository_guards/no_python.rs +++ b/tests/repository_guards/no_python.rs @@ -96,7 +96,8 @@ impl LocalFiles { fn parse(gitignore: &str) -> Self { let mut local = Self::default(); - for line in gitignore.lines().map(str::trim) { + let trimmed_lines = gitignore.lines().map(str::trim); + for line in trimmed_lines { if let Some(included) = line.strip_prefix('!') { let path = included.trim_start_matches('/'); if !path.is_empty() && !path.ends_with('/') && !path.contains(['*', '?', '[', '\\']) @@ -233,7 +234,7 @@ fn audit(entries: &BTreeMap) -> Vec { continue; } - let Ok(text) = std::str::from_utf8(bytes) else { + let Ok(text) = str::from_utf8(bytes) else { // An extensionless file may be binary; a source file must be text. if path.extension().is_some() { failures.push(format!("{shown}: not UTF-8")); diff --git a/tests/repository_guards/shared_dependencies.rs b/tests/repository_guards/shared_dependencies.rs index 8d82c3dd3..5560ed620 100644 --- a/tests/repository_guards/shared_dependencies.rs +++ b/tests/repository_guards/shared_dependencies.rs @@ -41,15 +41,7 @@ const CONFIG_DRAW_VALUES: &[(&str, &str)] = &[ ), ( "src/config/validate/spotlight.rs", - "crate::draw::MIN_SPOTLIGHT_MAGNIFICATION", - ), - ( - "src/config/validate/spotlight.rs", - "crate::draw::MAX_SPOTLIGHT_MAGNIFICATION", - ), - ( - "src/config/validate/spotlight.rs", - "crate::draw::normalize_spotlight_magnification", + "crate::draw::{MAX_SPOTLIGHT_MAGNIFICATION, MIN_SPOTLIGHT_MAGNIFICATION, normalize_spotlight_magnification,}", ), ]; diff --git a/tests/repository_guards/source.rs b/tests/repository_guards/source.rs index a11a1998c..877f4f58c 100644 --- a/tests/repository_guards/source.rs +++ b/tests/repository_guards/source.rs @@ -6,9 +6,9 @@ //! of small sources can also be checked as its own tree. use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::fs; use std::path::{Path, PathBuf}; -use std::sync::{LazyLock, Mutex, PoisonError}; +use std::sync::{LazyLock, Mutex}; +use std::{fs, iter}; /// Rust sources keyed by repository-relative path. #[derive(Clone)] @@ -27,11 +27,22 @@ impl Tree { .iter() .map(|directory| (*directory).to_owned()) .collect(); - let mut checkouts = CHECKOUTS.lock().unwrap_or_else(PoisonError::into_inner); + let cached = CHECKOUTS + .lock() + .expect("the checkout cache is never locked across a panic") + .get(&key) + .cloned(); + if let Some(tree) = cached { + return tree; + } - checkouts + // Reading can panic on an unreadable file, so it runs outside the lock. + let tree = Self::read_checkout(directories); + CHECKOUTS + .lock() + .expect("the checkout cache is never locked across a panic") .entry(key) - .or_insert_with(|| Self::read_checkout(directories)) + .or_insert(tree) .clone() } @@ -39,10 +50,10 @@ impl Tree { let root = repository_root(); let mut files = BTreeMap::new(); - for path in directories + let listed_paths = directories .iter() - .flat_map(|directory| files_under(directory)) - { + .flat_map(|directory| files_under(directory)); + for path in listed_paths { if path.extension().is_some_and(|extension| extension == "rs") { let source = fs::read_to_string(root.join(&path)) .unwrap_or_else(|error| panic!("read {}: {error}", path.display())); @@ -388,17 +399,17 @@ pub fn within(spans: &[(usize, usize)], offset: usize) -> bool { } /// A cursor over masked source for recognising item headers. -pub struct Cursor<'a> { - pub text: &'a str, +pub struct Cursor<'text> { + pub text: &'text str, pub at: usize, } -impl<'a> Cursor<'a> { - pub fn new(text: &'a str, at: usize) -> Self { +impl<'text> Cursor<'text> { + pub fn new(text: &'text str, at: usize) -> Self { Self { text, at } } - fn rest(&self) -> &'a str { + fn rest(&self) -> &'text str { &self.text[self.at..] } @@ -448,7 +459,7 @@ impl<'a> Cursor<'a> { } /// A run of word characters, like `\w+`. - pub fn word(&mut self) -> Option<&'a str> { + pub fn word(&mut self) -> Option<&'text str> { let rest = self.rest(); let length = rest .char_indices() @@ -462,7 +473,7 @@ impl<'a> Cursor<'a> { /// `pub`, optionally restricted, then whitespace, like /// `pub(?:\s*\([^)]*\))?\s+`. Returns the visibility text. - pub fn visibility(&mut self) -> Option<&'a str> { + pub fn visibility(&mut self) -> Option<&'text str> { let start = self.at; if !self.keyword("pub") { return None; @@ -515,7 +526,7 @@ impl<'a> Cursor<'a> { /// Offsets of every line start: 0 and each offset after a newline. pub fn line_starts(text: &str) -> impl Iterator + '_ { - std::iter::once(0).chain(text.match_indices('\n').map(|(index, _)| index + 1)) + iter::once(0).chain(text.match_indices('\n').map(|(index, _)| index + 1)) } /// The attribute that compiles an item only for tests. diff --git a/tests/repository_guards/translations.rs b/tests/repository_guards/translations.rs index 9d6d3c286..a955b2357 100644 --- a/tests/repository_guards/translations.rs +++ b/tests/repository_guards/translations.rs @@ -35,6 +35,9 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::{Path, PathBuf}; +use wayscriber::domain::Language; +use wayscriber::i18n; + use crate::source::{Tree, files_under, is_word_char, line_of, mask_non_code, repository_root}; use wayscriber::config::{action_display_label, action_meta_iter}; @@ -536,8 +539,8 @@ fn check_entry(failures: &mut Vec, catalogs: &Catalogs, source: &str, wh /// draws English source text. fn action_meta_strings() -> BTreeMap> { assert_eq!( - wayscriber::i18n::language(), - wayscriber::domain::Language::English, + i18n::language(), + Language::English, "the guard reads source text; nothing in this process may select a language" ); let mut strings: BTreeMap> = BTreeMap::new(); diff --git a/tests/repository_guards/use_statements.rs b/tests/repository_guards/use_statements.rs new file mode 100644 index 000000000..9e0705310 --- /dev/null +++ b/tests/repository_guards/use_statements.rs @@ -0,0 +1,146 @@ +//! Each `use` statement imports from one module. +//! +//! `use foo::bar::{Bar, Baz};` and `use foo::{self, Foo};` are fine; +//! `use foo::{Foo, bar::Bar};` is not, because a `::` inside the braces names a +//! second module. rustfmt can enforce this with `imports_granularity = +//! "Module"`, but that option is not available on the stable toolchain, so this +//! guard reads the spelling instead. It sees every `use` item in the Rust +//! sources, including function-local ones, but not imports that a macro +//! expands to. + +use std::path::Path; + +use crate::source::{Tree, mask_non_code}; + +/// Every directory whose Rust sources the workspace compiles. +const SOURCE_DIRECTORIES: &[&str] = &["src", "configurator/src", "tests", "examples"]; + +fn check(tree: &Tree) -> Vec { + let mut errors = Vec::new(); + + for path in tree.paths() { + let source = tree.read(path).expect("listed path"); + let code = mask_non_code(source); + for (start, statement) in use_statements(&code) { + if names_a_second_module(statement) { + errors.push(location(path, &code, start)); + } + } + } + + errors +} + +fn location(path: &Path, code: &str, start: usize) -> String { + let line = code[..start].matches('\n').count() + 1; + + format!( + "{}:{line}: a `use` statement imports from more than one module", + path.display() + ) +} + +/// The `use` statements in masked code, as (offset, text up to the `;`). +fn use_statements(code: &str) -> Vec<(usize, &str)> { + let mut statements = Vec::new(); + let mut search_from = 0; + + while let Some(found) = code[search_from..].find("use") { + let start = search_from + found; + search_from = start + "use".len(); + if !starts_use_item(code, start) { + continue; + } + + let Some(length) = code[start..].find(';') else { + break; + }; + statements.push((start, &code[start..start + length])); + search_from = start + length; + } + + statements +} + +/// Whether the `use` at `start` is the keyword that begins a `use` item. +fn starts_use_item(code: &str, start: usize) -> bool { + let after = &code[start + "use".len()..]; + if !after.starts_with(char::is_whitespace) { + return false; + } + + // An item begins after another item or block, an attribute, or a + // visibility such as `pub` or `pub(crate)`; `reuse` begins with none. + let before = code[..start].trim_end(); + before.is_empty() + || before.ends_with([';', '{', '}', ']', ')']) + || before + .strip_suffix("pub") + .is_some_and(|rest| !rest.ends_with(is_word_char)) +} + +fn is_word_char(character: char) -> bool { + character.is_alphanumeric() || character == '_' +} + +fn names_a_second_module(statement: &str) -> bool { + let mut depth = 0_usize; + let mut characters = statement.chars().peekable(); + + while let Some(character) = characters.next() { + match character { + '{' => depth += 1, + '}' => depth = depth.saturating_sub(1), + ':' if depth > 0 && characters.peek() == Some(&':') => return true, + _ => {} + } + } + + false +} + +#[test] +fn every_use_statement_imports_from_one_module() { + let errors = check(&Tree::checkout(SOURCE_DIRECTORIES)); + + assert!(errors.is_empty(), "{}", errors.join("\n")); +} + +#[test] +fn a_module_path_inside_braces_is_rejected() { + let escapes = [ + "use foo::{Foo, bar::Bar};\n", + "use foo::{bar::{Baz, Qux}};\n", + "pub(crate) use foo::{bar::Bar, Foo};\n", + "#[cfg(test)]\nuse foo::{\n Foo,\n bar::Bar,\n};\n", + "fn body() {\n use std::{fs, io::Read};\n}\n", + "use {std::fs, crate::paths};\n", + ]; + + for escape in escapes { + let tree = Tree::empty().with("src/escape.rs", escape); + + assert_eq!(check(&tree).len(), 1, "accepted: {escape}"); + } +} + +#[test] +fn single_module_imports_and_text_that_only_looks_like_one_are_accepted() { + let accepted = [ + "use foo::bar::{Bar, Baz};\n", + "use foo::{self, Foo};\n", + "pub use foo::bar::Bar;\n", + "use super::*;\n", + "use std::io::{self, Read as _};\n", + "// use foo::{Foo, bar::Bar};\n", + "const DOC: &str = \"use foo::{Foo, bar::Bar};\";\n", + "fn reuse() -> usize { let used = 1; used }\n", + "let pause = ::std::time::Duration::ZERO;\n", + ]; + + for source in accepted { + let tree = Tree::empty().with("src/accepted.rs", source); + + assert!(check(&tree).is_empty(), "rejected: {source}"); + } +} diff --git a/tests/ui.rs b/tests/ui.rs index 80eb79bf7..ce305e77e 100644 --- a/tests/ui.rs +++ b/tests/ui.rs @@ -1,15 +1,30 @@ use cairo::{Context, ImageSurface}; use wayscriber::config::{Action, Config, HelpOverlayStyle, StatusBarStyle, StatusPosition}; use wayscriber::draw::{Color, Shape}; +use wayscriber::input::state::HelpOverlayClick; use wayscriber::input::{BOARD_ID_BLACKBOARD, BOARD_ID_WHITEBOARD, InputState}; +use wayscriber::ui::{ + HelpContentOptions, HelpOverlayBindings, OnboardingCard, OnboardingCardAction, + OnboardingCardButton, OnboardingChecklistItem, +}; +use wayscriber::{draw, ui}; + +/// Every optional help section available and none filtered out. +const FULL_HELP: HelpContentOptions = HelpContentOptions { + frozen_enabled: true, + context_filter: false, + board_enabled: true, + capture_enabled: true, +}; fn make_input_state() -> InputState { InputState::from_config(&Config::default()) } fn surface_with_context(width: i32, height: i32) -> (ImageSurface, Context) { - let surface = ImageSurface::create(cairo::Format::ARgb32, width, height).unwrap(); - let ctx = Context::new(&surface).unwrap(); + let surface = ImageSurface::create(cairo::Format::ARgb32, width, height) + .expect("create test image surface"); + let ctx = Context::new(&surface).expect("create Cairo context for test surface"); (surface, ctx) } @@ -23,7 +38,7 @@ fn surface_has_pixels(surface: &mut ImageSurface) -> bool { fn alpha_at(surface: &mut ImageSurface, x: i32, y: i32) -> u8 { let stride = surface.stride() as usize; let offset = y as usize * stride + x as usize * 4 + 3; - surface.data().unwrap()[offset] + surface.data().expect("borrow test surface pixel data")[offset] } fn alpha_bounds(surface: &mut ImageSurface) -> Option<(i32, i32, i32, i32)> { @@ -67,7 +82,7 @@ fn render_status_bar_draws_for_all_positions() { for position in positions { let (mut surface, ctx) = surface_with_context(400, 200); input.update_status_hud_layout(position, &style, 400, 200); - wayscriber::ui::render_status_bar(&ctx, &input, &style, 400, 200); + ui::render_status_bar(&ctx, &input, &style, 400, 200); drop(ctx); assert!( surface_has_pixels(&mut surface), @@ -81,9 +96,9 @@ fn render_help_overlay_draws_content() { let style = HelpOverlayStyle::default(); let (mut surface, ctx) = surface_with_context(800, 600); let input = make_input_state(); - let bindings = wayscriber::ui::HelpOverlayBindings::from_input_state(&input); - wayscriber::ui::render_help_overlay( - &ctx, &style, 800, 600, true, 0, &bindings, "", false, true, true, 0.0, false, + let bindings = HelpOverlayBindings::from_input_state(&input); + ui::render_help_overlay( + &ctx, &style, 800, 600, FULL_HELP, 0, &bindings, "", 0.0, false, ); drop(ctx); assert!(surface_has_pixels(&mut surface)); @@ -101,7 +116,7 @@ fn render_status_bar_draws_in_board_modes() { input.switch_board(board_id); let (mut surface, ctx) = surface_with_context(400, 200); input.update_status_hud_layout(StatusPosition::BottomLeft, &style, 400, 200); - wayscriber::ui::render_status_bar(&ctx, &input, &style, 400, 200); + ui::render_status_bar(&ctx, &input, &style, 400, 200); drop(ctx); assert!( surface_has_pixels(&mut surface), @@ -115,9 +130,21 @@ fn render_help_overlay_without_frozen_shortcuts_draws_content() { let style = HelpOverlayStyle::default(); let (mut surface, ctx) = surface_with_context(800, 600); let input = make_input_state(); - let bindings = wayscriber::ui::HelpOverlayBindings::from_input_state(&input); - wayscriber::ui::render_help_overlay( - &ctx, &style, 800, 600, false, 0, &bindings, "", false, true, true, 0.0, false, + let bindings = HelpOverlayBindings::from_input_state(&input); + ui::render_help_overlay( + &ctx, + &style, + 800, + 600, + HelpContentOptions { + frozen_enabled: false, + ..FULL_HELP + }, + 0, + &bindings, + "", + 0.0, + false, ); drop(ctx); assert!(surface_has_pixels(&mut surface)); @@ -132,7 +159,7 @@ fn render_command_palette_with_query_draws_content() { input.command_palette.open(); input.command_palette.set_query("tool".to_string()); - wayscriber::ui::render_command_palette(&ctx, &input, 900, 700); + ui::render_command_palette(&ctx, &input, 900, 700); drop(ctx); assert!(surface_has_pixels(&mut surface)); @@ -144,7 +171,7 @@ fn render_keybinding_capture_draws_content() { let mut input = make_input_state(); assert!(input.begin_keybinding_capture(Action::SelectPenTool)); - wayscriber::ui::render_command_palette(&ctx, &input, 800, 600); + ui::render_command_palette(&ctx, &input, 800, 600); drop(ctx); assert!(surface_has_pixels(&mut surface)); @@ -153,7 +180,7 @@ fn render_keybinding_capture_draws_content() { #[test] fn render_frozen_badge_draws_pixels() { let (mut surface, ctx) = surface_with_context(400, 200); - wayscriber::ui::render_frozen_badge(&ctx, 400, 200); + ui::render_frozen_badge(&ctx, 400, 200); drop(ctx); assert!(surface_has_pixels(&mut surface)); } @@ -169,7 +196,7 @@ fn render_shape_ellipse_does_not_connect_to_existing_current_path() { }; ctx.move_to(10.0, 90.0); - wayscriber::draw::render_shape( + draw::render_shape( &ctx, &Shape::Ellipse { cx: 80, @@ -198,24 +225,24 @@ fn render_shape_ellipse_does_not_connect_to_existing_current_path() { #[test] fn render_onboarding_card_tiny_surface_does_not_panic() { let (mut surface, ctx) = surface_with_context(200, 40); - let card = wayscriber::ui::OnboardingCard { + let card = OnboardingCard { eyebrow: "First-run onboarding".to_string(), title: "Draw one mark".to_string(), body: "Make one quick stroke to start.".to_string(), - items: vec![wayscriber::ui::OnboardingChecklistItem { + items: vec![OnboardingChecklistItem { label: "Draw a stroke".to_string(), done: false, }], - buttons: vec![wayscriber::ui::OnboardingCardButton { + buttons: vec![OnboardingCardButton { label: "Skip tour".to_string(), key_hint: Some("Shift+Esc".to_string()), - action: wayscriber::ui::OnboardingCardAction::SkipTour, + action: OnboardingCardAction::SkipTour, primary: false, }], footer: "Shift+Escape to skip".to_string(), }; - wayscriber::ui::render_onboarding_card(&ctx, 200, 40, &card); + ui::render_onboarding_card(&ctx, 200, 40, &card); drop(ctx); assert!(surface_has_pixels(&mut surface)); } @@ -223,7 +250,7 @@ fn render_onboarding_card_tiny_surface_does_not_panic() { #[test] fn render_onboarding_card_without_checklist_stays_compact() { let (mut surface, ctx) = surface_with_context(700, 500); - let compact_card = wayscriber::ui::OnboardingCard { + let compact_card = OnboardingCard { eyebrow: "Step 1 / 5".to_string(), title: "Enable background mode?".to_string(), body: "Keeps Wayscriber ready in the background for quick overlay access.".to_string(), @@ -232,21 +259,21 @@ fn render_onboarding_card_without_checklist_stays_compact() { footer: "Y = set up now • N = skip • Shift+Escape = skip onboarding".to_string(), }; - wayscriber::ui::render_onboarding_card(&ctx, 700, 500, &compact_card); + ui::render_onboarding_card(&ctx, 700, 500, &compact_card); drop(ctx); let (_, compact_min_y, _, compact_max_y) = alpha_bounds(&mut surface).expect("rendered compact onboarding card"); let compact_height = compact_max_y - compact_min_y; let (mut checklist_surface, checklist_ctx) = surface_with_context(700, 500); - let checklist_card = wayscriber::ui::OnboardingCard { - items: vec![wayscriber::ui::OnboardingChecklistItem { + let checklist_card = OnboardingCard { + items: vec![OnboardingChecklistItem { label: "Open the configurator".to_string(), done: false, }], ..compact_card }; - wayscriber::ui::render_onboarding_card(&checklist_ctx, 700, 500, &checklist_card); + ui::render_onboarding_card(&checklist_ctx, 700, 500, &checklist_card); drop(checklist_ctx); let (_, checklist_min_y, _, checklist_max_y) = alpha_bounds(&mut checklist_surface).expect("rendered checklist onboarding card"); @@ -272,9 +299,9 @@ fn help_overlay_footer_offers_clickable_replay_and_about() { let style = HelpOverlayStyle::default(); let (_surface, ctx) = surface_with_context(1400, 1000); let input = make_input_state(); - let bindings = wayscriber::ui::HelpOverlayBindings::from_input_state(&input); - let result = wayscriber::ui::render_help_overlay_result( - &ctx, &style, 1400, 1000, true, 0, &bindings, "", false, true, true, 0.0, false, + let bindings = HelpOverlayBindings::from_input_state(&input); + let result = ui::render_help_overlay_result( + &ctx, &style, 1400, 1000, FULL_HELP, 0, &bindings, "", 0.0, false, ); drop(ctx); @@ -309,9 +336,9 @@ fn help_overlay_footer_offers_the_unbound_actions_toggle() { let style = HelpOverlayStyle::default(); let (_surface, ctx) = surface_with_context(1400, 1000); let input = make_input_state(); - let bindings = wayscriber::ui::HelpOverlayBindings::from_input_state(&input); - let result = wayscriber::ui::render_help_overlay_result( - &ctx, &style, 1400, 1000, true, 0, &bindings, "", false, true, true, 0.0, false, + let bindings = HelpOverlayBindings::from_input_state(&input); + let result = ui::render_help_overlay_result( + &ctx, &style, 1400, 1000, FULL_HELP, 0, &bindings, "", 0.0, false, ); drop(ctx); @@ -326,7 +353,7 @@ fn help_overlay_footer_offers_the_unbound_actions_toggle() { interactive.install_help_overlay_render_result(result); assert_eq!( interactive.help_overlay_click_at(toggle.0, toggle.1), - wayscriber::input::state::HelpOverlayClick::ToggleUnbound, + HelpOverlayClick::ToggleUnbound, ); } @@ -336,10 +363,10 @@ fn help_result_owns_rendered_footer_hits_and_matches_drawing_only_pixels() { let style = HelpOverlayStyle::default(); let input = make_input_state(); - let bindings = wayscriber::ui::HelpOverlayBindings::from_input_state(&input); + let bindings = HelpOverlayBindings::from_input_state(&input); let (mut owned_surface, ctx) = surface_with_context(1400, 1000); - let result = wayscriber::ui::render_help_overlay_result( - &ctx, &style, 1400, 1000, true, 0, &bindings, "", false, true, true, 0.0, false, + let result = ui::render_help_overlay_result( + &ctx, &style, 1400, 1000, FULL_HELP, 0, &bindings, "", 0.0, false, ); drop(ctx); @@ -362,12 +389,12 @@ fn help_result_owns_rendered_footer_hits_and_matches_drawing_only_pixels() { let (about_x, about_y) = about.unwrap(); assert_eq!( interactive.help_overlay_click_at(about_x, about_y), - wayscriber::input::state::HelpOverlayClick::Run(Action::OpenAbout), + HelpOverlayClick::Run(Action::OpenAbout), ); let (mut drawing_surface, ctx) = surface_with_context(1400, 1000); - let scroll = wayscriber::ui::render_help_overlay( - &ctx, &style, 1400, 1000, true, 0, &bindings, "", false, true, true, 0.0, false, + let scroll = ui::render_help_overlay( + &ctx, &style, 1400, 1000, FULL_HELP, 0, &bindings, "", 0.0, false, ); drop(ctx); assert_eq!(result.scroll_max, scroll); @@ -380,24 +407,21 @@ fn help_result_owns_rendered_footer_hits_and_matches_drawing_only_pixels() { "result and drawing-only paths must paint identical pixels" ); let (_surface, ctx) = surface_with_context(320, 240); - wayscriber::ui::render_help_overlay( + ui::render_help_overlay( &ctx, &style, 320, 240, - false, + HelpContentOptions::default(), 0, &bindings, "no matching action", - false, - false, - false, 0.0, true, ); assert_eq!( interactive.help_overlay_click_at(about_x, about_y), - wayscriber::input::state::HelpOverlayClick::Run(Action::OpenAbout), + HelpOverlayClick::Run(Action::OpenAbout), "painting another overlay must not replace the installed owner's map", ); let (x, y) = about.unwrap(); @@ -417,12 +441,12 @@ fn small_palette_surface_and_shadow_fit_the_screen() { input.update_screen_dimensions(width, height); input.command_palette.open(); let (x, y, panel_width, panel_height) = - wayscriber::ui::command_palette_visual_geometry(&input, width, height).unwrap(); + ui::command_palette_visual_geometry(&input, width, height).unwrap(); assert!(x >= 0.0 && y >= 0.0); assert!(x + panel_width <= width as f64); assert!(y + panel_height <= height as f64); let (mut surface, ctx) = surface_with_context(width as i32, height as i32); - wayscriber::ui::render_command_palette(&ctx, &input, width, height); + ui::render_command_palette(&ctx, &input, width, height); drop(ctx); let dimmer = alpha_at(&mut surface, 0, 0); let panel = alpha_at(&mut surface, (x + 20.0) as i32, (y + 20.0) as i32); From d57ad6094e2850404bc35aae499b5219790e9742 Mon Sep 17 00:00:00 2001 From: devmobasa <4170275+devmobasa@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:04:07 +0200 Subject: [PATCH 2/2] test(gtk): wait for stable capture reference pixels Several identical frames can precede a popup's final layout, leaving the capture restoration test with a premature reference image. Require a quiet interval before recording that image while retaining the existing settling deadline and exact restoration assertions. --- .../top_bar/tests/capture_render/support.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/toolbar_gtk/view/top_bar/tests/capture_render/support.rs b/src/toolbar_gtk/view/top_bar/tests/capture_render/support.rs index 65ab7bdb4..c60c5485b 100644 --- a/src/toolbar_gtk/view/top_bar/tests/capture_render/support.rs +++ b/src/toolbar_gtk/view/top_bar/tests/capture_render/support.rs @@ -96,19 +96,20 @@ pub(super) fn assert_restored(widget: &impl IsA, expected: &[[u8; pub(super) fn settled_pixels(widget: &impl IsA) -> Vec<[u8; 4]> { let deadline = Instant::now() + Duration::from_secs(2); let mut previous = pixels(widget); - let mut stable = 0; + let mut stable_since = Instant::now(); + + // Popup layout can change after several identical frames. Require a + // quiet interval before saving the normal pixels used after capture. loop { wait_for_frame(widget); let current = pixels(widget); - if current == previous { - stable += 1; - } else { - stable = 0; - } - if stable == 3 { + let now = Instant::now(); + if current != previous { + stable_since = now; + } else if now.duration_since(stable_since) >= Duration::from_millis(200) { return current; } - assert!(Instant::now() < deadline, "normal fixture did not settle"); + assert!(now < deadline, "normal fixture did not settle"); previous = current; } }