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}