Skip to main content

fret_runtime/
effect.rs

1use std::time::Duration;
2
3use crate::window_chrome::WindowResizeDirection;
4use crate::window_style::{WindowRole, WindowStyleRequest};
5use crate::{
6    ClipboardToken, ExternalDropToken, FileDialogToken, ImageUpdateToken, ImageUploadToken,
7    IncomingOpenToken, ShareSheetToken, TimerToken,
8};
9use fret_core::{
10    AlphaMode, AppWindowId, CursorIcon, Edges, Event, ExternalDropReadLimits, FileDialogOptions,
11    ImageColorInfo, ImageId, Rect, RectPx, WindowAnchor,
12};
13
14use crate::{CommandId, MenuBar};
15
16#[derive(Debug, Clone, PartialEq)]
17pub enum DiagIncomingOpenItem {
18    File {
19        name: String,
20        bytes: Vec<u8>,
21        media_type: Option<String>,
22    },
23    Text {
24        text: String,
25        media_type: Option<String>,
26    },
27}
28
29#[derive(Debug, Clone, PartialEq)]
30/// Effects emitted by the portable runtime surface.
31///
32/// Effects are collected by the host (e.g. `fret-app::App`) and are expected to be handled by a
33/// runner/backend integration layer (native or web).
34///
35/// ## Completion events (runner contract)
36///
37/// Many effects represent an *asynchronous* request to the platform and are completed later by a
38/// corresponding [`fret_core::Event`]. Runners/backends should treat these as best-effort.
39///
40/// Common mappings:
41///
42/// - `ClipboardReadText { token, .. }` → `fret_core::Event::ClipboardReadText { token, .. }` or
43///   `fret_core::Event::ClipboardReadFailed { token, .. }`
44/// - `PrimarySelectionGetText { token, .. }` → `fret_core::Event::PrimarySelectionText { token, .. }`
45///   or `fret_core::Event::PrimarySelectionTextUnavailable { token, .. }`
46/// - `ShareSheetShow { token, .. }` → `fret_core::Event::ShareSheetCompleted { token, .. }`
47/// - `FileDialogOpen { .. }` → `fret_core::Event::FileDialogSelection(..)` or
48///   `fret_core::Event::FileDialogCanceled`
49/// - `FileDialogReadAll { token, .. }` → `fret_core::Event::FileDialogData(..)`
50/// - `IncomingOpenReadAll { token, .. }` → `fret_core::Event::IncomingOpenData(..)` or
51///   `fret_core::Event::IncomingOpenUnavailable { token, .. }`
52/// - `SetTimer { token, .. }` → `fret_core::Event::Timer { token }`
53/// - `ImageRegister* { token, .. }` → `fret_core::Event::ImageRegistered { token, .. }` or
54///   `fret_core::Event::ImageRegisterFailed { token, .. }`
55/// - `ImageUpdate* { token, .. }` → optionally `fret_core::Event::ImageUpdateApplied { token, .. }`
56///   or `fret_core::Event::ImageUpdateDropped { token, .. }` when the runner supports these acks
57///   (capability-gated to avoid flooding the event loop).
58pub enum Effect {
59    /// Request a window redraw (one-shot).
60    ///
61    /// This is the lowest-level redraw primitive. Higher-level UI code typically calls
62    /// `App::request_redraw` (or `Cx::request_redraw` / `Cx::request_frame`), which eventually
63    /// results in this effect being handled by the runner/backend.
64    ///
65    /// Semantics:
66    /// - This is a one-shot request and may be coalesced by the runner or platform compositor.
67    /// - This does **not** imply continuous frame progression. If you need to keep repainting
68    ///   without input events (animations, progressive rendering), use
69    ///   [`Effect::RequestAnimationFrame`] and re-issue it each frame while active.
70    Redraw(AppWindowId),
71    Window(WindowRequest),
72    Command {
73        window: Option<AppWindowId>,
74        command: CommandId,
75    },
76    /// Request the application to quit (native runners may exit their event loop).
77    ///
78    /// Web runners may ignore this request.
79    QuitApp,
80    /// Show the standard native "About" panel when available.
81    ///
82    /// Platform mapping:
83    /// - macOS: `NSApplication orderFrontStandardAboutPanel:`
84    /// - Other platforms: runners may ignore this request.
85    ShowAboutPanel,
86    /// Hide the application (macOS: `NSApplication hide:`).
87    ///
88    /// Other platforms may ignore this request.
89    HideApp,
90    /// Hide all other applications (macOS: `NSApplication hideOtherApplications:`).
91    ///
92    /// Other platforms may ignore this request.
93    HideOtherApps,
94    /// Unhide all applications (macOS: `NSApplication unhideAllApplications:`).
95    ///
96    /// Other platforms may ignore this request.
97    UnhideAllApps,
98    /// Set the application/window menu bar (native runners may map this to an OS menubar).
99    ///
100    /// Notes:
101    /// - This is a platform integration seam; web runners may ignore it.
102    /// - The menu model is data-only (`MenuBar`) and is typically derived from command metadata
103    ///   (ADR 0023).
104    SetMenuBar {
105        window: Option<AppWindowId>,
106        menu_bar: MenuBar,
107    },
108    /// Requests writing platform clipboard text (best-effort).
109    ClipboardWriteText {
110        window: AppWindowId,
111        token: ClipboardToken,
112        text: String,
113    },
114    /// Requests reading platform clipboard text (best-effort).
115    ///
116    /// Runners/backends should eventually complete this request by emitting a corresponding event
117    /// carrying `token` (see `ClipboardToken` contract in `fret-core`).
118    ClipboardReadText {
119        window: AppWindowId,
120        token: ClipboardToken,
121    },
122    /// Set Linux primary selection text (copy-on-select).
123    ///
124    /// This is intentionally separate from `ClipboardWriteText` so selecting text does not
125    /// overwrite the explicit clipboard used by `Ctrl+C` / `edit.copy`.
126    PrimarySelectionSetText {
127        text: String,
128    },
129    /// Read Linux primary selection text (middle-click paste).
130    PrimarySelectionGetText {
131        window: AppWindowId,
132        token: ClipboardToken,
133    },
134    ExternalDropReadAll {
135        window: AppWindowId,
136        token: ExternalDropToken,
137    },
138    ExternalDropReadAllWithLimits {
139        window: AppWindowId,
140        token: ExternalDropToken,
141        limits: ExternalDropReadLimits,
142    },
143    ExternalDropRelease {
144        token: ExternalDropToken,
145    },
146    /// Requests opening a URL using the platform's default handler (best-effort).
147    ///
148    /// Callers should ensure the URL is safe/expected. Component-layer helpers may apply
149    /// additional policies (e.g. `rel="noreferrer"`).
150    OpenUrl {
151        url: String,
152        target: Option<String>,
153        rel: Option<String>,
154    },
155    /// Show the platform-native share sheet (best-effort).
156    ShareSheetShow {
157        window: AppWindowId,
158        token: ShareSheetToken,
159        items: Vec<fret_core::ShareItem>,
160    },
161    /// Opens a platform-native file dialog (best-effort).
162    ///
163    /// Runners/backends typically respond by delivering one of:
164    /// - `fret_core::Event::FileDialogSelection` (token + names), followed by
165    ///   `Effect::FileDialogReadAll` to obtain bytes, or
166    /// - `fret_core::Event::FileDialogCanceled` if the user cancels.
167    FileDialogOpen {
168        window: AppWindowId,
169        options: FileDialogOptions,
170    },
171    /// Requests reading all selected file bytes for a previously opened file dialog.
172    FileDialogReadAll {
173        window: AppWindowId,
174        token: FileDialogToken,
175    },
176    FileDialogReadAllWithLimits {
177        window: AppWindowId,
178        token: FileDialogToken,
179        limits: ExternalDropReadLimits,
180    },
181    /// Releases runner-owned resources associated with a file dialog token (best-effort).
182    FileDialogRelease {
183        token: FileDialogToken,
184    },
185    /// Read all data associated with an incoming-open token (best-effort).
186    IncomingOpenReadAll {
187        window: AppWindowId,
188        token: IncomingOpenToken,
189    },
190    IncomingOpenReadAllWithLimits {
191        window: AppWindowId,
192        token: IncomingOpenToken,
193        limits: ExternalDropReadLimits,
194    },
195    /// Releases runner-owned resources associated with an incoming-open token (best-effort).
196    IncomingOpenRelease {
197        token: IncomingOpenToken,
198    },
199    /// Diagnostics-only “incoming open” injection (best-effort).
200    ///
201    /// This simulates mobile-style share-target / open-in flows in CI by injecting an
202    /// `Event::IncomingOpenRequest` carrying tokenized items.
203    ///
204    /// Runners SHOULD:
205    ///
206    /// - allocate an `IncomingOpenToken`,
207    /// - enqueue/deliver `Event::IncomingOpenRequest { token, items }`,
208    /// - and retain the injected payload behind the token so subsequent reads can succeed.
209    ///
210    /// Notes:
211    ///
212    /// - This is intended for diagnostics/scripts only; real incoming-open requests originate from
213    ///   the OS.
214    /// - Payload bytes are diagnostic fixtures; they are not intended to model platform handles.
215    DiagIncomingOpenInject {
216        window: AppWindowId,
217        items: Vec<DiagIncomingOpenItem>,
218    },
219    /// Diagnostics-only synthetic event injection (best-effort).
220    ///
221    /// This lets tooling deliver an already-constructed runtime event to a specific window even
222    /// when that window is not the one currently producing render callbacks.
223    DiagInjectEvent {
224        window: AppWindowId,
225        event: Event,
226    },
227    /// Diagnostics-only clipboard override to simulate mobile privacy/user-activation denial paths.
228    ///
229    /// Notes:
230    /// - Runners SHOULD treat this as a best-effort toggle and default to `enabled=false`.
231    /// - When enabled, clipboard reads (`ClipboardReadText`, `PrimarySelectionGetText`) SHOULD
232    ///   complete as unavailable rather than attempting platform access.
233    /// - Clipboard writes (`ClipboardWriteText`) SHOULD complete with a failed outcome rather than
234    ///   attempting platform access.
235    DiagClipboardForceUnavailable {
236        window: AppWindowId,
237        enabled: bool,
238    },
239    /// Resolve logical font assets through the shared runtime asset resolver and add the resulting
240    /// bytes to the renderer text system.
241    ///
242    /// This is the runtime font-loading lane for callers that want resolver overrides,
243    /// diagnostics, and packaged mounts to participate before byte injection.
244    ///
245    /// The runner/backend is responsible for resolving each request, applying any successful
246    /// results to the renderer, and triggering any required invalidation/redraw.
247    TextAddFontAssets {
248        requests: Vec<fret_assets::AssetRequest>,
249    },
250    /// Request a best-effort rescan of system-installed fonts (native-only).
251    ///
252    /// Web/WASM runners should ignore this effect, as they cannot access system font databases.
253    ///
254    /// Semantics:
255    /// - This is an explicit, user-initiated refresh hook (ADR 0258).
256    /// - Runners should re-enumerate the font catalog and republish `FontCatalogMetadata` if
257    ///   changes are observed.
258    /// - Runners should also bump renderer text invalidation keys (e.g. `TextFontStackKey`) so
259    ///   cached shaping/rasterization results cannot be reused after a rescan attempt.
260    TextRescanSystemFonts,
261    ViewportInput(fret_core::ViewportInputEvent),
262    Dock(fret_core::DockOp),
263    ImeAllow {
264        window: AppWindowId,
265        enabled: bool,
266    },
267    /// Best-effort request to show/hide the platform virtual keyboard.
268    ///
269    /// Notes:
270    /// - This does not replace `Effect::ImeAllow`, which remains the source of truth for whether
271    ///   the focused widget is a text input.
272    /// - Some platforms (notably Android) may require this request to be issued within a
273    ///   user-activation turn (direct input event handling), otherwise it may be ignored.
274    ImeRequestVirtualKeyboard {
275        window: AppWindowId,
276        visible: bool,
277    },
278    ImeSetCursorArea {
279        window: AppWindowId,
280        rect: Rect,
281    },
282    /// Override window insets in `WindowMetricsService` (safe area / occlusion).
283    ///
284    /// This is primarily used by diagnostics/scripted repros to simulate keyboard occlusion on
285    /// platforms where the real OS insets are not available in CI.
286    ///
287    /// Semantics:
288    /// - `None` means "no change".
289    /// - `Some(None)` clears the insets but still marks them as "known".
290    /// - `Some(Some(v))` sets the insets to `v`.
291    WindowMetricsSetInsets {
292        window: AppWindowId,
293        safe_area_insets: Option<Option<Edges>>,
294        occlusion_insets: Option<Option<Edges>>,
295    },
296    CursorSetIcon {
297        window: AppWindowId,
298        icon: CursorIcon,
299    },
300    ImageRegisterRgba8 {
301        window: AppWindowId,
302        token: ImageUploadToken,
303        width: u32,
304        height: u32,
305        bytes: Vec<u8>,
306        color_info: ImageColorInfo,
307        alpha_mode: AlphaMode,
308    },
309    ImageUpdateRgba8 {
310        window: Option<AppWindowId>,
311        token: ImageUpdateToken,
312        image: ImageId,
313        stream_generation: u64,
314        width: u32,
315        height: u32,
316        update_rect_px: Option<RectPx>,
317        bytes_per_row: u32,
318        bytes: Vec<u8>,
319        color_info: ImageColorInfo,
320        alpha_mode: AlphaMode,
321    },
322    ImageUpdateNv12 {
323        window: Option<AppWindowId>,
324        token: ImageUpdateToken,
325        image: ImageId,
326        stream_generation: u64,
327        width: u32,
328        height: u32,
329        update_rect_px: Option<RectPx>,
330        y_bytes_per_row: u32,
331        y_plane: Vec<u8>,
332        uv_bytes_per_row: u32,
333        uv_plane: Vec<u8>,
334        color_info: ImageColorInfo,
335        alpha_mode: AlphaMode,
336    },
337    ImageUpdateI420 {
338        window: Option<AppWindowId>,
339        token: ImageUpdateToken,
340        image: ImageId,
341        stream_generation: u64,
342        width: u32,
343        height: u32,
344        update_rect_px: Option<RectPx>,
345        y_bytes_per_row: u32,
346        y_plane: Vec<u8>,
347        u_bytes_per_row: u32,
348        u_plane: Vec<u8>,
349        v_bytes_per_row: u32,
350        v_plane: Vec<u8>,
351        color_info: ImageColorInfo,
352        alpha_mode: AlphaMode,
353    },
354    ImageUnregister {
355        image: fret_core::ImageId,
356    },
357    /// Request the next animation frame for a window.
358    ///
359    /// Use this for frame-driven updates (animations, progressive rendering) where the UI must
360    /// keep repainting even if there are no new input events.
361    ///
362    /// This is a one-shot request. Runners/backends should schedule a redraw and keep advancing
363    /// the frame counter while these requests are being issued.
364    ///
365    /// Platform mapping:
366    /// - Web backends typically map this to `requestAnimationFrame`.
367    /// - Desktop backends typically translate this into a "redraw on the next event-loop turn"
368    ///   request (and may coalesce multiple requests).
369    RequestAnimationFrame(AppWindowId),
370    /// Requests a timer callback to be delivered as `fret_core::Event::Timer` (best-effort).
371    SetTimer {
372        window: Option<AppWindowId>,
373        token: TimerToken,
374        after: Duration,
375        repeat: Option<Duration>,
376    },
377    /// Cancels a previously requested timer (best-effort).
378    CancelTimer {
379        token: TimerToken,
380    },
381}
382
383#[derive(Debug, Clone, PartialEq)]
384pub enum WindowRequest {
385    Create(CreateWindowRequest),
386    Close(AppWindowId),
387    /// Request showing or hiding an OS window without destroying it (best-effort).
388    SetVisible {
389        window: AppWindowId,
390        visible: bool,
391    },
392    SetInnerSize {
393        window: AppWindowId,
394        size: fret_core::Size,
395    },
396    /// Request moving the OS window to a screen-space logical position (ADR 0017).
397    ///
398    /// Runners should treat this as best-effort and may clamp/deny the request based on platform
399    /// constraints and user settings.
400    SetOuterPosition {
401        window: AppWindowId,
402        position: fret_core::WindowLogicalPosition,
403    },
404    Raise {
405        window: AppWindowId,
406        sender: Option<AppWindowId>,
407    },
408    /// Begin an OS-native interactive window drag (best-effort).
409    BeginDrag {
410        window: AppWindowId,
411    },
412    /// Begin an OS-native interactive window resize (best-effort).
413    BeginResize {
414        window: AppWindowId,
415        direction: WindowResizeDirection,
416    },
417    /// Best-effort request to update OS window style facets at runtime.
418    ///
419    /// Semantics:
420    /// - This is a patch request: each `Some(...)` field updates that facet, `None` leaves it
421    ///   unchanged.
422    /// - Runners may ignore unsupported facets based on platform constraints.
423    SetStyle {
424        window: AppWindowId,
425        style: WindowStyleRequest,
426    },
427}
428
429#[derive(Debug, Clone, PartialEq)]
430pub struct CreateWindowRequest {
431    pub kind: CreateWindowKind,
432    pub anchor: Option<WindowAnchor>,
433    pub role: WindowRole,
434    pub style: WindowStyleRequest,
435}
436
437#[derive(Debug, Clone, PartialEq)]
438pub enum CreateWindowKind {
439    DockFloating {
440        source_window: AppWindowId,
441        panel: fret_core::PanelKey,
442    },
443    DockRestore {
444        logical_window_id: String,
445    },
446}