Skip to main content

fission_core/
context.rs

1//! Reducer context and effect builder.
2//!
3//! When a reducer needs to emit side-effects or inspect the [`ActionInput`]
4//! that triggered it, it receives a [`ReducerContext`]. The context provides
5//! an [`Effects`] builder for issuing capabilities, jobs, services, and
6//! runtime-control effects plus binding callback actions.
7
8use crate::action::{Action, ActionEnvelope, GlobalState};
9use crate::async_runtime::{
10    JobRef, JobRequestPayload, JobSpec, ServiceBindings, ServiceCommandPayload, ServiceSlot,
11    ServiceSpec, ServiceStartPayload, ServiceStopPayload,
12};
13use crate::capability::{
14    CapabilityInvocationPayload, CapabilityType, OperationCapability, OperationCapabilityInvocation,
15};
16use crate::effect::{ActionInput, Effect, EffectEnvelope, RuntimeEffect, ScrollIntoViewRequest};
17use crate::platform::{
18    CancelNotificationRequest, NotificationPermissionRequest, NotificationRequest,
19    PushRegistrationRequest, SetBadgeCountRequest, CANCEL_ALL_NOTIFICATIONS, CANCEL_NOTIFICATION,
20    GET_NOTIFICATION_SETTINGS, REGISTER_PUSH_NOTIFICATIONS, REQUEST_NOTIFICATION_PERMISSION,
21    SCHEDULE_NOTIFICATION, SET_BADGE_COUNT, SHOW_NOTIFICATION, UNREGISTER_PUSH_NOTIFICATIONS,
22};
23use crate::platform_barcode::{
24    BarcodeImageDecodeRequest, BarcodeScanRequest, CANCEL_BARCODE_SCAN, DECODE_BARCODE_IMAGE,
25    SCAN_BARCODE,
26};
27use crate::platform_biometric::{
28    BiometricAuthenticateRequest, AUTHENTICATE_BIOMETRIC, CANCEL_BIOMETRIC_AUTHENTICATION,
29    GET_BIOMETRIC_AVAILABILITY,
30};
31use crate::platform_bluetooth::{
32    BluetoothAdvertiseRequest, BluetoothConnectRequest, BluetoothDisconnectRequest,
33    BluetoothPermissionRequest, BluetoothReadRequest, BluetoothScanRequest,
34    BluetoothStopAdvertiseRequest, BluetoothWriteRequest, CONNECT_BLUETOOTH_DEVICE,
35    DISCONNECT_BLUETOOTH_DEVICE, GET_BLUETOOTH_AVAILABILITY, READ_BLUETOOTH_CHARACTERISTIC,
36    REQUEST_BLUETOOTH_PERMISSION, SCAN_BLUETOOTH_DEVICES, START_BLUETOOTH_ADVERTISING,
37    STOP_BLUETOOTH_ADVERTISING, WRITE_BLUETOOTH_CHARACTERISTIC,
38};
39use crate::platform_camera::{
40    CameraCaptureRequest, CameraFlashlightRequest, CameraPermissionRequest, CANCEL_CAMERA_CAPTURE,
41    CAPTURE_PHOTO, GET_CAMERA_AVAILABILITY, REQUEST_CAMERA_PERMISSION, SET_CAMERA_FLASHLIGHT,
42};
43use crate::platform_clipboard::{
44    ClipboardContent, ClipboardWriteTextRequest, CLEAR_CLIPBOARD, READ_CLIPBOARD_CONTENT,
45    READ_CLIPBOARD_TEXT, WRITE_CLIPBOARD_CONTENT, WRITE_CLIPBOARD_TEXT,
46};
47use crate::platform_geolocation::{
48    GeolocationPermissionRequest, GeolocationPositionRequest, GET_CURRENT_POSITION,
49    GET_GEOLOCATION_PERMISSION, REQUEST_GEOLOCATION_PERMISSION,
50};
51use crate::platform_haptics::{
52    HapticImpactRequest, HapticNotificationRequest, HapticPatternRequest, HAPTIC_IMPACT,
53    HAPTIC_NOTIFICATION, HAPTIC_PATTERN, HAPTIC_SELECTION,
54};
55use crate::platform_microphone::{
56    MicrophoneCaptureRequest, MicrophonePermissionRequest, CANCEL_MICROPHONE_CAPTURE,
57    CAPTURE_MICROPHONE_AUDIO, GET_MICROPHONE_AVAILABILITY, REQUEST_MICROPHONE_PERMISSION,
58};
59use crate::platform_nfc::{
60    NfcEmulationRequest, NfcScanRequest, NfcWriteRequest, CANCEL_NFC_SESSION, EMULATE_NFC_TAG,
61    GET_NFC_AVAILABILITY, SCAN_NFC_TAG, WRITE_NFC_TAG,
62};
63use crate::platform_passkey::{
64    PasskeyAuthenticationRequest, PasskeyRegistrationRequest, AUTHENTICATE_PASSKEY,
65    CANCEL_PASSKEY_OPERATION, GET_PASSKEY_AVAILABILITY, REGISTER_PASSKEY,
66};
67use crate::platform_volume::{
68    VolumeAdjustRequest, VolumeSetRequest, VolumeStream, ADJUST_VOLUME_LEVEL, GET_VOLUME_LEVEL,
69    SET_VOLUME_LEVEL,
70};
71use crate::platform_wifi::{
72    WifiConnectRequest, WifiDisconnectRequest, WifiPermissionRequest, WifiScanRequest,
73    CONNECT_WIFI_NETWORK, DISCONNECT_WIFI_NETWORK, GET_WIFI_AVAILABILITY, REQUEST_WIFI_PERMISSION,
74    SCAN_WIFI_NETWORKS,
75};
76use crate::registry::{ActionRegistry, IntoHandler};
77use std::marker::PhantomData;
78
79/// The context passed to modern 3-argument reducer handlers.
80///
81/// Provides access to the [`Effects`] builder (for emitting side-effects) and
82/// the [`ActionInput`] that accompanied the dispatch (e.g. effect results,
83/// pointer coordinates, drop payloads).
84///
85/// # Example
86///
87/// ```rust,ignore
88/// fn handle_click(
89///     state: &mut GlobalState,
90///     action: ClickAction,
91///     ctx: &mut ReducerContext<GlobalState>,
92/// ) {
93///     // Read pointer position from the input
94///     if let Some((x, y, _, _)) = ctx.input.as_pointer() {
95///         state.last_click = (x, y);
96///     }
97///     // Issue a capability effect
98///     ctx.effects.capability(MY_CAPABILITY, request);
99/// }
100/// ```
101pub struct ReducerContext<'a, 'b, 'c, S: GlobalState> {
102    /// Mutable reference to the effects builder.
103    pub effects: &'a mut Effects<'b, S>,
104    /// The input data that accompanied this action dispatch.
105    pub input: &'c ActionInput,
106}
107
108/// Builder for emitting side-effects from within a reducer.
109///
110/// `Effects` accumulates [`EffectEnvelope`] values that the runtime collects
111/// after the reducer returns. Each effect can carry optional `on_ok` and
112/// `on_err` callbacks.
113///
114/// # Example
115///
116/// ```rust,ignore
117/// fn handle_save(
118///     state: &mut MyState,
119///     _action: Save,
120///     ctx: &mut ReducerContext<MyState>,
121/// ) {
122///     ctx.effects.capability(MY_CAPABILITY, request)
123///         .on_ok(ctx.effects.bind(SaveOk, handle_save_ok as fn(&mut MyState, SaveOk)))
124///         .on_err(ctx.effects.bind(SaveErr, handle_save_err as fn(&mut MyState, SaveErr)));
125/// }
126/// ```
127pub struct Effects<'a, S: GlobalState> {
128    /// Accumulated effect envelopes, drained by the runtime after the reducer.
129    pub out: Vec<EffectEnvelope>,
130    next_req_id: u64,
131    pub(crate) registry: Option<&'a mut ActionRegistry<S>>,
132    _phantom: PhantomData<S>,
133}
134
135impl<'a, S: GlobalState> Effects<'a, S> {
136    pub fn new(next_req_id: u64, registry: &'a mut ActionRegistry<S>) -> Self {
137        Self {
138            out: Vec::new(),
139            next_req_id,
140            registry: Some(registry),
141            _phantom: PhantomData,
142        }
143    }
144
145    pub fn new_headless(next_req_id: u64) -> Self {
146        Self {
147            out: Vec::new(),
148            next_req_id,
149            registry: None,
150            _phantom: PhantomData,
151        }
152    }
153
154    pub fn bind<A: Action, H>(&mut self, action: A, handler: H) -> ActionEnvelope
155    where
156        H: IntoHandler<S, A> + Send + Sync + 'static,
157    {
158        if let Some(registry) = &mut self.registry {
159            registry.register(handler);
160        }
161        ActionEnvelope {
162            id: A::static_id(),
163            payload: action.encode(),
164        }
165    }
166
167    pub fn add(&mut self, effect: Effect) -> u64 {
168        let req_id = self.next_req_id;
169        self.next_req_id += 1;
170
171        self.out.push(EffectEnvelope {
172            req_id,
173            effect,
174            on_ok: None,
175            on_err: None,
176            service_bindings: None,
177            resource: None,
178        });
179        req_id
180    }
181
182    pub fn capability<C: OperationCapability>(
183        &mut self,
184        capability: CapabilityType<C>,
185        request: C::Request,
186    ) -> EffectBuilder<'_, 'a, S> {
187        let req_id = self.next_req_id;
188        self.next_req_id += 1;
189        let request =
190            serde_json::to_vec(&request).expect("capability request serialization must succeed");
191
192        let index = self.out.len();
193        self.out.push(EffectEnvelope {
194            req_id,
195            effect: Effect::Capability(CapabilityInvocationPayload::Operation(
196                OperationCapabilityInvocation {
197                    capability_name: capability.name.to_string(),
198                    request,
199                },
200            )),
201            on_ok: None,
202            on_err: None,
203            service_bindings: None,
204            resource: None,
205        });
206
207        EffectBuilder {
208            effects: self,
209            index,
210        }
211    }
212
213    /// Starts a typed notification capability request.
214    ///
215    /// Use this from reducers when the app needs the host to request
216    /// notification permission, show or schedule a notification, update a badge,
217    /// or register for push delivery. The returned builder records a capability
218    /// effect; it does not display anything until the reducer has returned and
219    /// the active shell processes queued effects.
220    pub fn notifications(&mut self) -> NotificationEffects<'_, 'a, S> {
221        NotificationEffects { effects: self }
222    }
223
224    /// Starts a typed NFC capability request.
225    ///
226    /// Use this when the app needs the host to read, write, emulate, or cancel
227    /// an NFC session. The helper keeps NFC prompts, tag records, and timeout
228    /// choices in typed request values so reducers do not call platform NFC APIs
229    /// directly.
230    pub fn nfc(&mut self) -> NfcEffects<'_, 'a, S> {
231        NfcEffects { effects: self }
232    }
233
234    /// Starts a typed biometric authentication capability request.
235    ///
236    /// Use this for host-owned local user verification such as fingerprint,
237    /// face, or device credential fallback. The result reports whether the host
238    /// verified the local user; it is not a network identity assertion.
239    pub fn biometrics(&mut self) -> BiometricEffects<'_, 'a, S> {
240        BiometricEffects { effects: self }
241    }
242
243    /// Starts a typed passkey/WebAuthn credential capability request.
244    ///
245    /// Use this for account sign-in, re-authentication, or credential
246    /// registration flows where the server verifies WebAuthn data. This is
247    /// intentionally separate from `biometrics()`: the host may use biometrics
248    /// to unlock a passkey, but the app receives credential data, not raw face
249    /// or fingerprint state.
250    pub fn passkeys(&mut self) -> PasskeyEffects<'_, 'a, S> {
251        PasskeyEffects { effects: self }
252    }
253
254    /// Starts a typed Bluetooth capability request.
255    ///
256    /// Use this for nearby-device workflows such as adapter availability,
257    /// permission requests, scanning, connecting, characteristic reads and
258    /// writes, and advertising. Scans and connections are host-owned operations
259    /// because permission and hardware behavior differ sharply by platform.
260    pub fn bluetooth(&mut self) -> BluetoothEffects<'_, 'a, S> {
261        BluetoothEffects { effects: self }
262    }
263
264    /// Starts a typed barcode scanner capability request.
265    ///
266    /// Use this when the host should run a live scanner session or decode image
267    /// bytes into barcode results. Live scanning normally depends on camera
268    /// permission; image decoding can be tested without camera hardware.
269    pub fn barcode_scanner(&mut self) -> BarcodeScannerEffects<'_, 'a, S> {
270        BarcodeScannerEffects { effects: self }
271    }
272
273    /// Starts a typed camera and flashlight capability request.
274    ///
275    /// Use this for camera availability, permission, still photo capture, and
276    /// torch control. The returned helper emits requests to the shell host so
277    /// the app state does not depend on a particular camera API.
278    pub fn camera(&mut self) -> CameraEffects<'_, 'a, S> {
279        CameraEffects { effects: self }
280    }
281
282    /// Starts a typed clipboard capability request.
283    ///
284    /// Use this for user-visible copy and paste flows. Platforms may restrict
285    /// clipboard access to focused windows, secure browser contexts, or direct
286    /// user gestures, so reducers should handle errors as normal outcomes.
287    pub fn clipboard(&mut self) -> ClipboardEffects<'_, 'a, S> {
288        ClipboardEffects { effects: self }
289    }
290
291    /// Starts a typed geolocation capability request.
292    ///
293    /// Use this when the app needs permission state or a current location. The
294    /// request controls accuracy, timeout, and cache age so the host can choose
295    /// an appropriate platform location call.
296    pub fn geolocation(&mut self) -> GeolocationEffects<'_, 'a, S> {
297        GeolocationEffects { effects: self }
298    }
299
300    /// Starts a typed haptic feedback capability request.
301    ///
302    /// Use this for tactile feedback tied to meaningful interactions such as
303    /// impact, notification, selection, or a bounded pattern. Unsupported hosts
304    /// should return a typed error rather than pretending vibration occurred.
305    pub fn haptics(&mut self) -> HapticEffects<'_, 'a, S> {
306        HapticEffects { effects: self }
307    }
308
309    /// Starts a typed microphone capability request.
310    ///
311    /// Use this for microphone availability, permission, bounded audio capture,
312    /// and cancellation. Captures should be explicit and time-bounded because
313    /// recording is a sensitive host-owned operation.
314    pub fn microphone(&mut self) -> MicrophoneEffects<'_, 'a, S> {
315        MicrophoneEffects { effects: self }
316    }
317
318    /// Starts a typed Wi-Fi capability request.
319    ///
320    /// Use this for adapter availability, permission, scanning, connecting, and
321    /// disconnecting where the platform allows app-level Wi-Fi management.
322    /// Many platforms treat Wi-Fi information as location-sensitive, so reducers
323    /// should handle permission and unsupported errors explicitly.
324    pub fn wifi(&mut self) -> WifiEffects<'_, 'a, S> {
325        WifiEffects { effects: self }
326    }
327
328    /// Starts a typed host volume-control capability request.
329    ///
330    /// Use this for app-approved media, notification, alarm, call, or system
331    /// stream adjustments. Some platforms expose only media-element volume or no
332    /// system-volume control, so callers should treat unsupported errors as
333    /// normal platform outcomes.
334    pub fn volume(&mut self) -> VolumeEffects<'_, 'a, S> {
335        VolumeEffects { effects: self }
336    }
337
338    pub fn app<J: JobSpec>(
339        &mut self,
340        job: JobRef<J>,
341        request: J::Request,
342    ) -> EffectBuilder<'_, 'a, S> {
343        let req_id = self.next_req_id;
344        self.next_req_id += 1;
345        let payload = serde_json::to_vec(&request).expect("job request serialization must succeed");
346        let index = self.out.len();
347        self.out.push(EffectEnvelope {
348            req_id,
349            effect: Effect::Job(JobRequestPayload {
350                job_name: job.name.to_string(),
351                payload,
352            }),
353            on_ok: None,
354            on_err: None,
355            service_bindings: None,
356            resource: None,
357        });
358        EffectBuilder {
359            effects: self,
360            index,
361        }
362    }
363
364    pub fn start_service<Svc: ServiceSpec>(
365        &mut self,
366        slot: ServiceSlot<Svc>,
367        config: Svc::Config,
368    ) -> ServiceStartBuilder<'_, 'a, S> {
369        let req_id = self.next_req_id;
370        self.next_req_id += 1;
371        let index = self.out.len();
372        let config =
373            serde_json::to_vec(&config).expect("service config serialization must succeed");
374        self.out.push(EffectEnvelope {
375            req_id,
376            effect: Effect::StartService(ServiceStartPayload {
377                service_name: slot.ty.name.to_string(),
378                slot_key: slot.slot_key().to_string(),
379                config,
380            }),
381            on_ok: None,
382            on_err: None,
383            service_bindings: Some(ServiceBindings::default()),
384            resource: None,
385        });
386        ServiceStartBuilder {
387            effects: self,
388            index,
389        }
390    }
391
392    pub fn command<Svc: ServiceSpec>(
393        &mut self,
394        slot: ServiceSlot<Svc>,
395        command: Svc::Command,
396    ) -> EffectBuilder<'_, 'a, S> {
397        let req_id = self.next_req_id;
398        self.next_req_id += 1;
399        let index = self.out.len();
400        let payload =
401            serde_json::to_vec(&command).expect("service command serialization must succeed");
402        self.out.push(EffectEnvelope {
403            req_id,
404            effect: Effect::ServiceCommand(ServiceCommandPayload {
405                service_name: slot.ty.name.to_string(),
406                slot_key: slot.slot_key().to_string(),
407                payload,
408            }),
409            on_ok: None,
410            on_err: None,
411            service_bindings: None,
412            resource: None,
413        });
414        EffectBuilder {
415            effects: self,
416            index,
417        }
418    }
419
420    pub fn stop_service<Svc: ServiceSpec>(
421        &mut self,
422        slot: ServiceSlot<Svc>,
423    ) -> EffectBuilder<'_, 'a, S> {
424        let req_id = self.next_req_id;
425        self.next_req_id += 1;
426        let index = self.out.len();
427        self.out.push(EffectEnvelope {
428            req_id,
429            effect: Effect::StopService(ServiceStopPayload {
430                service_name: slot.ty.name.to_string(),
431                slot_key: slot.slot_key().to_string(),
432            }),
433            on_ok: None,
434            on_err: None,
435            service_bindings: None,
436            resource: None,
437        });
438        EffectBuilder {
439            effects: self,
440            index,
441        }
442    }
443
444    pub fn cancel(&mut self, req_id: u64) {
445        self.add(Effect::Runtime(RuntimeEffect::Cancel { req_id }));
446    }
447
448    pub fn release_resource(&mut self, resource_id: u64) {
449        self.add(Effect::Runtime(RuntimeEffect::ReleaseResource {
450            resource_id,
451        }));
452    }
453
454    /// Reveals a widget inside a scroll container after the next layout pass.
455    ///
456    /// This is safe to emit from reducers because the runtime resolves widget
457    /// rectangles later, after layout has produced stable geometry.
458    pub fn scroll_into_view(&mut self, request: ScrollIntoViewRequest) -> u64 {
459        self.add(Effect::Runtime(RuntimeEffect::ScrollIntoView(request)))
460    }
461
462    /// Alias for [`Effects::scroll_into_view`] when the caller cares about
463    /// visibility rather than a specific scroll operation.
464    pub fn ensure_visible(&mut self, request: ScrollIntoViewRequest) -> u64 {
465        self.scroll_into_view(request)
466    }
467}
468
469/// Convenience builder for the standard notification host capabilities.
470pub struct NotificationEffects<'a, 'b, S: GlobalState> {
471    effects: &'a mut Effects<'b, S>,
472}
473
474impl<'a, 'b, S: GlobalState> NotificationEffects<'a, 'b, S> {
475    /// Requests notification permission from the active host.
476    ///
477    /// `request` declares which notification features the app wants, such as
478    /// alerts, badges, sounds, or provisional delivery. The returned
479    /// `EffectBuilder` should normally bind success and error actions so the
480    /// reducer can update state after the user responds to the platform prompt.
481    pub fn request_permission(
482        self,
483        request: NotificationPermissionRequest,
484    ) -> EffectBuilder<'a, 'b, S> {
485        self.effects
486            .capability(REQUEST_NOTIFICATION_PERMISSION, request)
487    }
488
489    /// Queries the host's current notification settings without showing a prompt.
490    ///
491    /// Use this before rendering notification-dependent controls or when a
492    /// settings screen needs to explain why delivery is unavailable. The success
493    /// action receives `NotificationSettings`.
494    pub fn settings(self) -> EffectBuilder<'a, 'b, S> {
495        self.effects.capability(GET_NOTIFICATION_SETTINGS, ())
496    }
497
498    /// Shows an immediate local notification through the host.
499    ///
500    /// `request` supplies the stable notification id, visible title/body, badge,
501    /// sound policy, optional deep link, and action buttons. Use `schedule`
502    /// instead when delivery should happen at a future time.
503    pub fn show(self, request: NotificationRequest) -> EffectBuilder<'a, 'b, S> {
504        self.effects.capability(SHOW_NOTIFICATION, request)
505    }
506
507    /// Schedules a local notification for future delivery.
508    ///
509    /// The `schedule` field on `request` controls the delivery time. Hosts may
510    /// reject schedules they cannot persist, cannot deliver in the background, or
511    /// cannot map to the platform notification model.
512    pub fn schedule(self, request: NotificationRequest) -> EffectBuilder<'a, 'b, S> {
513        self.effects.capability(SCHEDULE_NOTIFICATION, request)
514    }
515
516    /// Cancels one pending or displayed notification by id.
517    ///
518    /// Use the same `NotificationId` that was used for `show` or `schedule`. A
519    /// host may treat cancelling an unknown id as success if the desired final
520    /// state is already true.
521    pub fn cancel(self, request: CancelNotificationRequest) -> EffectBuilder<'a, 'b, S> {
522        self.effects.capability(CANCEL_NOTIFICATION, request)
523    }
524
525    /// Cancels all notifications owned by this app where the host supports it.
526    ///
527    /// Use this for sign-out, workspace switching, or clearing a notification
528    /// center state that no longer matches app state. Hosts that cannot bulk
529    /// cancel should return `NotificationError`.
530    pub fn cancel_all(self) -> EffectBuilder<'a, 'b, S> {
531        self.effects.capability(CANCEL_ALL_NOTIFICATIONS, ())
532    }
533
534    /// Sets or clears the app badge count.
535    ///
536    /// `request.count = Some(n)` asks the host to show a badge count.
537    /// `request.count = None` clears the badge. Badge support varies by desktop
538    /// shell, launcher, browser, and mobile platform.
539    pub fn set_badge_count(self, request: SetBadgeCountRequest) -> EffectBuilder<'a, 'b, S> {
540        self.effects.capability(SET_BADGE_COUNT, request)
541    }
542
543    /// Registers the app for remote or push notifications.
544    ///
545    /// `request` carries provider-specific public registration inputs such as a
546    /// web push application-server key, Android sender id, or requested topics.
547    /// Secrets and store credentials belong in host configuration, not in app
548    /// state.
549    pub fn register_push(self, request: PushRegistrationRequest) -> EffectBuilder<'a, 'b, S> {
550        self.effects
551            .capability(REGISTER_PUSH_NOTIFICATIONS, request)
552    }
553
554    /// Unregisters the app from remote or push notification delivery.
555    ///
556    /// Use this during sign-out, account removal, or when a user disables remote
557    /// notifications. The host should invalidate or delete its platform token
558    /// where the provider supports that operation.
559    pub fn unregister_push(self) -> EffectBuilder<'a, 'b, S> {
560        self.effects.capability(UNREGISTER_PUSH_NOTIFICATIONS, ())
561    }
562}
563
564/// Convenience builder for standard NFC host capabilities.
565pub struct NfcEffects<'a, 'b, S: GlobalState> {
566    effects: &'a mut Effects<'b, S>,
567}
568
569impl<'a, 'b, S: GlobalState> NfcEffects<'a, 'b, S> {
570    /// Queries whether NFC is supported, enabled, and which NFC modes are available.
571    ///
572    /// Use this before showing scan/write controls so the UI can distinguish a
573    /// missing NFC chip from a disabled adapter or unsupported operation.
574    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
575        self.effects.capability(GET_NFC_AVAILABILITY, ())
576    }
577
578    /// Starts a one-shot NFC scan session.
579    ///
580    /// `request` declares allowed technologies, optional user-facing prompt text,
581    /// timeout, and whether multiple records should be collected. The success
582    /// action receives an `NfcTag` when the host reads a compatible tag.
583    pub fn scan_tag(self, request: NfcScanRequest) -> EffectBuilder<'a, 'b, S> {
584        self.effects.capability(SCAN_NFC_TAG, request)
585    }
586
587    /// Starts an NFC tag write session.
588    ///
589    /// `request.records` contains the portable NDEF-like records to write. Hosts
590    /// may require the user to tap a writable tag after the operation starts and
591    /// may reject read-only or incompatible tags.
592    pub fn write_tag(self, request: NfcWriteRequest) -> EffectBuilder<'a, 'b, S> {
593        self.effects.capability(WRITE_NFC_TAG, request)
594    }
595
596    /// Requests host NFC card emulation for the supplied records.
597    ///
598    /// Use this only on platforms and devices that support card emulation for
599    /// the product scenario. Many hosts support scanning but not emulation.
600    pub fn emulate_tag(self, request: NfcEmulationRequest) -> EffectBuilder<'a, 'b, S> {
601        self.effects.capability(EMULATE_NFC_TAG, request)
602    }
603
604    /// Cancels the active NFC session, if one is running.
605    ///
606    /// Use this when the user dismisses the screen that started scanning, writing,
607    /// or emulation. Hosts may return success when no session is active.
608    pub fn cancel_session(self) -> EffectBuilder<'a, 'b, S> {
609        self.effects.capability(CANCEL_NFC_SESSION, ())
610    }
611}
612
613/// Convenience builder for standard biometric host capabilities.
614pub struct BiometricEffects<'a, 'b, S: GlobalState> {
615    effects: &'a mut Effects<'b, S>,
616}
617
618impl<'a, 'b, S: GlobalState> BiometricEffects<'a, 'b, S> {
619    /// Queries local biometric support and enrollment state.
620    ///
621    /// Use this before presenting a biometric-only path. The result tells the app
622    /// whether the host supports biometric verification, whether credentials are
623    /// enrolled, which modalities may be available, and whether device credential
624    /// fallback is possible.
625    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
626        self.effects.capability(GET_BIOMETRIC_AVAILABILITY, ())
627    }
628
629    /// Asks the host to authenticate the current local user.
630    ///
631    /// `request.reason` should explain why verification is needed before the
632    /// platform prompt appears. The success action receives
633    /// `BiometricAuthenticateResult`, which reports the modality and whether a
634    /// device credential fallback was used.
635    pub fn authenticate(self, request: BiometricAuthenticateRequest) -> EffectBuilder<'a, 'b, S> {
636        self.effects.capability(AUTHENTICATE_BIOMETRIC, request)
637    }
638
639    /// Cancels an active biometric authentication prompt where the host permits it.
640    ///
641    /// Use this when the screen that requested verification is closed or the app
642    /// changes state before the user responds. Some platform prompts cannot be
643    /// cancelled programmatically after display.
644    pub fn cancel_authentication(self) -> EffectBuilder<'a, 'b, S> {
645        self.effects.capability(CANCEL_BIOMETRIC_AUTHENTICATION, ())
646    }
647}
648
649/// Convenience builder for standard passkey/WebAuthn host capabilities.
650pub struct PasskeyEffects<'a, 'b, S: GlobalState> {
651    effects: &'a mut Effects<'b, S>,
652}
653
654impl<'a, 'b, S: GlobalState> PasskeyEffects<'a, 'b, S> {
655    /// Queries passkey support for the active host and origin.
656    ///
657    /// Use this before showing passkey-specific registration or sign-in controls.
658    /// The result tells the app whether the host supports passkeys, whether the
659    /// current context is secure enough for credential APIs, and whether platform
660    /// or conditional UI authenticators may be available.
661    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
662        self.effects.capability(GET_PASSKEY_AVAILABILITY, ())
663    }
664
665    /// Requests creation of a new passkey credential.
666    ///
667    /// `request.challenge` must come from the relying-party server and must be
668    /// verified by that server when the success action receives
669    /// `PasskeyRegistrationResult`. Do not generate production challenges in the
670    /// UI reducer or trust registration data until the backend verifies it.
671    pub fn register(self, request: PasskeyRegistrationRequest) -> EffectBuilder<'a, 'b, S> {
672        self.effects.capability(REGISTER_PASSKEY, request)
673    }
674
675    /// Requests authentication with an existing passkey credential.
676    ///
677    /// `request.challenge` must come from the server, and the returned
678    /// `PasskeyAuthenticationResult` must be verified by the server before the
679    /// app treats the user as signed in. The host only gathers credential data.
680    pub fn authenticate(self, request: PasskeyAuthenticationRequest) -> EffectBuilder<'a, 'b, S> {
681        self.effects.capability(AUTHENTICATE_PASSKEY, request)
682    }
683
684    /// Cancels an active passkey prompt where the host permits cancellation.
685    ///
686    /// Use this when the sign-in or registration screen disappears before the
687    /// host credential picker completes. Some browser or operating-system
688    /// prompts cannot be cancelled once shown.
689    pub fn cancel(self) -> EffectBuilder<'a, 'b, S> {
690        self.effects.capability(CANCEL_PASSKEY_OPERATION, ())
691    }
692}
693
694/// Convenience builder for standard Bluetooth host capabilities.
695pub struct BluetoothEffects<'a, 'b, S: GlobalState> {
696    effects: &'a mut Effects<'b, S>,
697}
698
699impl<'a, 'b, S: GlobalState> BluetoothEffects<'a, 'b, S> {
700    /// Queries Bluetooth adapter, permission, and mode availability.
701    ///
702    /// Use this before showing scan, connect, or advertise controls. The result
703    /// lets the UI distinguish missing hardware, disabled Bluetooth, permission
704    /// denial, and hosts that support only classic or Low Energy Bluetooth.
705    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
706        self.effects.capability(GET_BLUETOOTH_AVAILABILITY, ())
707    }
708
709    /// Requests Bluetooth or nearby-device permission from the host.
710    ///
711    /// `request.reason` should explain the product feature that needs nearby
712    /// devices. Hosts map the request to the platform permission model, which may
713    /// include Bluetooth, location, or nearby-device prompts depending on target.
714    pub fn request_permission(
715        self,
716        request: BluetoothPermissionRequest,
717    ) -> EffectBuilder<'a, 'b, S> {
718        self.effects
719            .capability(REQUEST_BLUETOOTH_PERMISSION, request)
720    }
721
722    /// Scans for Bluetooth devices matching the request filters.
723    ///
724    /// `request.service_uuids` narrows discovery to product-relevant services.
725    /// `timeout_ms` should be set for user-driven scans so the host does not keep
726    /// nearby-device discovery running indefinitely.
727    pub fn scan_devices(self, request: BluetoothScanRequest) -> EffectBuilder<'a, 'b, S> {
728        self.effects.capability(SCAN_BLUETOOTH_DEVICES, request)
729    }
730
731    /// Connects to a discovered or previously known Bluetooth device.
732    ///
733    /// `request.device_id` must come from a trusted host result or stored pairing
734    /// flow. The success action receives a `BluetoothConnection` whose
735    /// `connection_id` is used for later read, write, and disconnect requests.
736    pub fn connect_device(self, request: BluetoothConnectRequest) -> EffectBuilder<'a, 'b, S> {
737        self.effects.capability(CONNECT_BLUETOOTH_DEVICE, request)
738    }
739
740    /// Disconnects a previously opened Bluetooth connection.
741    ///
742    /// `request.connection_id` should be the id returned by `connect_device`.
743    /// Use this when the user leaves the device workflow or when the app no
744    /// longer needs the peripheral.
745    pub fn disconnect_device(
746        self,
747        request: BluetoothDisconnectRequest,
748    ) -> EffectBuilder<'a, 'b, S> {
749        self.effects
750            .capability(DISCONNECT_BLUETOOTH_DEVICE, request)
751    }
752
753    /// Reads one Bluetooth characteristic from an active connection.
754    ///
755    /// `request` names the connection, service UUID, and characteristic UUID.
756    /// Hosts should return `BluetoothError` when the connection is gone or the
757    /// characteristic is unavailable.
758    pub fn read_characteristic(self, request: BluetoothReadRequest) -> EffectBuilder<'a, 'b, S> {
759        self.effects
760            .capability(READ_BLUETOOTH_CHARACTERISTIC, request)
761    }
762
763    /// Writes bytes to one Bluetooth characteristic.
764    ///
765    /// `request.with_response` lets the app choose between acknowledged and
766    /// unacknowledged writes where the platform supports both. Reducers should
767    /// still handle connection loss and permission errors as normal outcomes.
768    pub fn write_characteristic(self, request: BluetoothWriteRequest) -> EffectBuilder<'a, 'b, S> {
769        self.effects
770            .capability(WRITE_BLUETOOTH_CHARACTERISTIC, request)
771    }
772
773    /// Starts Bluetooth advertising for hosts that allow apps to advertise.
774    ///
775    /// `request` supplies the service UUID, optional service data, display name,
776    /// and timeout. Mobile and browser platforms often restrict advertising more
777    /// heavily than scanning or connecting.
778    pub fn start_advertising(self, request: BluetoothAdvertiseRequest) -> EffectBuilder<'a, 'b, S> {
779        self.effects
780            .capability(START_BLUETOOTH_ADVERTISING, request)
781    }
782
783    /// Stops a Bluetooth advertising session.
784    ///
785    /// `request.advertisement_id` should be the id returned by
786    /// `start_advertising`. Hosts may also stop advertisements automatically when
787    /// their timeout expires or the app moves to a background state.
788    pub fn stop_advertising(
789        self,
790        request: BluetoothStopAdvertiseRequest,
791    ) -> EffectBuilder<'a, 'b, S> {
792        self.effects.capability(STOP_BLUETOOTH_ADVERTISING, request)
793    }
794}
795
796/// Convenience builder for standard barcode scanner host capabilities.
797pub struct BarcodeScannerEffects<'a, 'b, S: GlobalState> {
798    effects: &'a mut Effects<'b, S>,
799}
800
801impl<'a, 'b, S: GlobalState> BarcodeScannerEffects<'a, 'b, S> {
802    /// Starts a live barcode scanning session.
803    ///
804    /// `request.formats` should list only formats the product accepts. The host
805    /// may open a camera UI, display `prompt`, and return one or more decoded
806    /// barcode values depending on `allow_multiple`.
807    pub fn scan(self, request: BarcodeScanRequest) -> EffectBuilder<'a, 'b, S> {
808        self.effects.capability(SCAN_BARCODE, request)
809    }
810
811    /// Decodes barcode data from an image stream supplied by the app.
812    ///
813    /// Use this when the image already exists, such as a file import or camera
814    /// frame captured elsewhere. The host should not request camera permission
815    /// for this operation unless its decoder specifically requires it.
816    pub fn decode_image(self, request: BarcodeImageDecodeRequest) -> EffectBuilder<'a, 'b, S> {
817        self.effects.capability(DECODE_BARCODE_IMAGE, request)
818    }
819
820    /// Cancels the active live barcode scanning session.
821    ///
822    /// Use this when the user leaves the scanning screen or chooses another input
823    /// path. Hosts may treat cancellation of a non-running session as success.
824    pub fn cancel_scan(self) -> EffectBuilder<'a, 'b, S> {
825        self.effects.capability(CANCEL_BARCODE_SCAN, ())
826    }
827}
828
829/// Convenience builder for standard camera host capabilities.
830pub struct CameraEffects<'a, 'b, S: GlobalState> {
831    effects: &'a mut Effects<'b, S>,
832}
833
834impl<'a, 'b, S: GlobalState> CameraEffects<'a, 'b, S> {
835    /// Queries camera permission and available camera devices.
836    ///
837    /// Use this before showing camera-specific controls. The result contains the
838    /// current permission state and host-visible devices, including facing
839    /// direction and flashlight availability where known.
840    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
841        self.effects.capability(GET_CAMERA_AVAILABILITY, ())
842    }
843
844    /// Requests camera permission from the host.
845    ///
846    /// `request.reason` can carry product-facing context for hosts that support a
847    /// pre-prompt or custom rationale. The success action receives the resulting
848    /// `CameraPermission` state.
849    pub fn request_permission(self, request: CameraPermissionRequest) -> EffectBuilder<'a, 'b, S> {
850        self.effects.capability(REQUEST_CAMERA_PERMISSION, request)
851    }
852
853    /// Captures a still photo through the selected host camera.
854    ///
855    /// `request` chooses camera id or facing direction, optional resolution, image
856    /// format, flash behavior, and quality. The success action receives image
857    /// stream handle plus dimensions, byte length, and content type.
858    pub fn capture_photo(self, request: CameraCaptureRequest) -> EffectBuilder<'a, 'b, S> {
859        self.effects.capability(CAPTURE_PHOTO, request)
860    }
861
862    /// Enables, disables, or adjusts the camera flashlight where supported.
863    ///
864    /// `request.camera_id` selects the device, `enabled` chooses the desired
865    /// state, and `intensity` optionally requests a platform-specific brightness
866    /// level from 0 to 100. Many desktop cameras have no torch.
867    pub fn set_flashlight(self, request: CameraFlashlightRequest) -> EffectBuilder<'a, 'b, S> {
868        self.effects.capability(SET_CAMERA_FLASHLIGHT, request)
869    }
870
871    /// Cancels an active camera capture session.
872    ///
873    /// Use this when the user dismisses the camera flow before a photo is
874    /// returned. Hosts may return success when there is no active capture.
875    pub fn cancel_capture(self) -> EffectBuilder<'a, 'b, S> {
876        self.effects.capability(CANCEL_CAMERA_CAPTURE, ())
877    }
878}
879
880/// Convenience builder for standard clipboard host capabilities.
881pub struct ClipboardEffects<'a, 'b, S: GlobalState> {
882    effects: &'a mut Effects<'b, S>,
883}
884
885impl<'a, 'b, S: GlobalState> ClipboardEffects<'a, 'b, S> {
886    /// Reads text from the host clipboard.
887    ///
888    /// Use this in response to an explicit paste action. The success action
889    /// receives `ClipboardText` with `None` when there is no readable text.
890    pub fn read_text(self) -> EffectBuilder<'a, 'b, S> {
891        self.effects.capability(READ_CLIPBOARD_TEXT, ())
892    }
893
894    /// Writes plain text to the host clipboard.
895    ///
896    /// `request.text` should be the exact text the user asked to copy. Some hosts
897    /// may require focus or a user gesture before accepting the write.
898    pub fn write_text(self, request: ClipboardWriteTextRequest) -> EffectBuilder<'a, 'b, S> {
899        self.effects.capability(WRITE_CLIPBOARD_TEXT, request)
900    }
901
902    /// Reads typed clipboard content from the host.
903    ///
904    /// Use this when the product can accept richer content than plain text. The
905    /// success action receives zero or more `ClipboardItem` values with content
906    /// types and bytes.
907    pub fn read_content(self) -> EffectBuilder<'a, 'b, S> {
908        self.effects.capability(READ_CLIPBOARD_CONTENT, ())
909    }
910
911    /// Writes typed content items to the host clipboard.
912    ///
913    /// `request.items` should list content types the target host can expose.
914    /// Include a `text/plain` item when possible so paste targets have a portable
915    /// fallback.
916    pub fn write_content(self, request: ClipboardContent) -> EffectBuilder<'a, 'b, S> {
917        self.effects.capability(WRITE_CLIPBOARD_CONTENT, request)
918    }
919
920    /// Clears app-visible clipboard content where the host supports it.
921    ///
922    /// Use this for explicit privacy actions such as Clear copied password. Some
923    /// platforms may not allow apps to clear global clipboard state.
924    pub fn clear(self) -> EffectBuilder<'a, 'b, S> {
925        self.effects.capability(CLEAR_CLIPBOARD, ())
926    }
927}
928
929/// Convenience builder for standard geolocation host capabilities.
930pub struct GeolocationEffects<'a, 'b, S: GlobalState> {
931    effects: &'a mut Effects<'b, S>,
932}
933
934impl<'a, 'b, S: GlobalState> GeolocationEffects<'a, 'b, S> {
935    /// Reads the current geolocation permission state without showing a prompt.
936    ///
937    /// Use this to decide whether a screen should show a request button, an
938    /// explanation, or a platform-settings hint. The result is host-reported and
939    /// may change outside the app.
940    pub fn permission(self) -> EffectBuilder<'a, 'b, S> {
941        self.effects.capability(GET_GEOLOCATION_PERMISSION, ())
942    }
943
944    /// Requests geolocation permission from the host.
945    ///
946    /// `request.precise` asks for precise coordinates when the platform exposes a
947    /// precise/approximate distinction. `request.background` should only be set
948    /// for product flows that genuinely need background location and have matching
949    /// platform configuration.
950    pub fn request_permission(
951        self,
952        request: GeolocationPermissionRequest,
953    ) -> EffectBuilder<'a, 'b, S> {
954        self.effects
955            .capability(REQUEST_GEOLOCATION_PERMISSION, request)
956    }
957
958    /// Requests the current location from the host.
959    ///
960    /// `request.high_accuracy`, `timeout_ms`, and `maximum_age_ms` let the app
961    /// trade precision, speed, power use, and cached values. The success action
962    /// receives latitude, longitude, accuracy, and optional motion metadata.
963    pub fn current_position(self, request: GeolocationPositionRequest) -> EffectBuilder<'a, 'b, S> {
964        self.effects.capability(GET_CURRENT_POSITION, request)
965    }
966}
967
968/// Convenience builder for standard haptic host capabilities.
969pub struct HapticEffects<'a, 'b, S: GlobalState> {
970    effects: &'a mut Effects<'b, S>,
971}
972
973impl<'a, 'b, S: GlobalState> HapticEffects<'a, 'b, S> {
974    /// Plays impact-style haptic feedback.
975    ///
976    /// Use this for physical-feeling interactions such as completing a drag,
977    /// snapping to a position, or confirming a strong action. The `style` field
978    /// tells the host how heavy the feedback should feel.
979    pub fn impact(self, request: HapticImpactRequest) -> EffectBuilder<'a, 'b, S> {
980        self.effects.capability(HAPTIC_IMPACT, request)
981    }
982
983    /// Plays notification-style haptic feedback.
984    ///
985    /// Use this to reinforce success, warning, or error states when tactile
986    /// feedback improves understanding. It should not replace visible or spoken
987    /// feedback for accessibility.
988    pub fn notification(self, request: HapticNotificationRequest) -> EffectBuilder<'a, 'b, S> {
989        self.effects.capability(HAPTIC_NOTIFICATION, request)
990    }
991
992    /// Plays selection-change haptic feedback.
993    ///
994    /// Use this for picker movement, segmented-control changes, or other repeated
995    /// selection adjustments where a light tick helps the user track movement.
996    pub fn selection(self) -> EffectBuilder<'a, 'b, S> {
997        self.effects.capability(HAPTIC_SELECTION, ())
998    }
999
1000    /// Plays a bounded custom haptic pattern.
1001    ///
1002    /// `request.steps` contains duration and intensity values. Keep patterns
1003    /// short and meaningful; hosts may reject long, empty, or unsupported
1004    /// patterns.
1005    pub fn pattern(self, request: HapticPatternRequest) -> EffectBuilder<'a, 'b, S> {
1006        self.effects.capability(HAPTIC_PATTERN, request)
1007    }
1008}
1009
1010/// Convenience builder for standard microphone host capabilities.
1011pub struct MicrophoneEffects<'a, 'b, S: GlobalState> {
1012    effects: &'a mut Effects<'b, S>,
1013}
1014
1015impl<'a, 'b, S: GlobalState> MicrophoneEffects<'a, 'b, S> {
1016    /// Queries microphone permission and available input devices.
1017    ///
1018    /// Use this before showing recording controls. The result tells the app
1019    /// whether microphone permission is granted and which host input devices are
1020    /// visible.
1021    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
1022        self.effects.capability(GET_MICROPHONE_AVAILABILITY, ())
1023    }
1024
1025    /// Requests microphone permission from the host.
1026    ///
1027    /// `request.reason` can be used by hosts that support a product-specific
1028    /// rationale before the platform prompt. The success action receives the
1029    /// resulting `MicrophonePermission` state.
1030    pub fn request_permission(
1031        self,
1032        request: MicrophonePermissionRequest,
1033    ) -> EffectBuilder<'a, 'b, S> {
1034        self.effects
1035            .capability(REQUEST_MICROPHONE_PERMISSION, request)
1036    }
1037
1038    /// Captures bounded audio from the selected microphone.
1039    ///
1040    /// `request.duration_ms` must define the intended capture length. Optional
1041    /// sample rate, channel count, and sample format let the host choose the
1042    /// closest supported recording configuration. The success action receives a
1043    /// stream handle plus recording metadata.
1044    pub fn capture_audio(self, request: MicrophoneCaptureRequest) -> EffectBuilder<'a, 'b, S> {
1045        self.effects.capability(CAPTURE_MICROPHONE_AUDIO, request)
1046    }
1047
1048    /// Cancels an active microphone capture session.
1049    ///
1050    /// Use this when the user stops recording, closes the screen, or chooses a
1051    /// different input path before the bounded capture completes.
1052    pub fn cancel_capture(self) -> EffectBuilder<'a, 'b, S> {
1053        self.effects.capability(CANCEL_MICROPHONE_CAPTURE, ())
1054    }
1055}
1056
1057/// Convenience builder for standard Wi-Fi host capabilities.
1058pub struct WifiEffects<'a, 'b, S: GlobalState> {
1059    effects: &'a mut Effects<'b, S>,
1060}
1061
1062impl<'a, 'b, S: GlobalState> WifiEffects<'a, 'b, S> {
1063    /// Queries current Wi-Fi adapter and connection availability.
1064    ///
1065    /// Use this before showing scan or connect controls. The result can include
1066    /// whether the adapter is enabled and which network, if any, is connected.
1067    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
1068        self.effects.capability(GET_WIFI_AVAILABILITY, ())
1069    }
1070
1071    /// Requests Wi-Fi or nearby-network permission from the host.
1072    ///
1073    /// `request.reason` should describe the feature that needs network discovery
1074    /// or management. Hosts may map this to Wi-Fi, nearby-device, or location
1075    /// permission prompts depending on platform policy.
1076    pub fn request_permission(self, request: WifiPermissionRequest) -> EffectBuilder<'a, 'b, S> {
1077        self.effects.capability(REQUEST_WIFI_PERMISSION, request)
1078    }
1079
1080    /// Scans for nearby Wi-Fi networks where the host permits scanning.
1081    ///
1082    /// `request.ssid_prefix` narrows results for device-setup flows,
1083    /// `include_hidden` asks the host to include hidden networks when possible,
1084    /// and `timeout_ms` bounds the scan.
1085    pub fn scan_networks(self, request: WifiScanRequest) -> EffectBuilder<'a, 'b, S> {
1086        self.effects.capability(SCAN_WIFI_NETWORKS, request)
1087    }
1088
1089    /// Requests connection to one Wi-Fi network.
1090    ///
1091    /// `request` carries SSID, optional passphrase, security type, and hidden
1092    /// network flag. Hosts may reject connections that require user confirmation,
1093    /// saved network profiles, entitlements, or administrator privileges.
1094    pub fn connect_network(self, request: WifiConnectRequest) -> EffectBuilder<'a, 'b, S> {
1095        self.effects.capability(CONNECT_WIFI_NETWORK, request)
1096    }
1097
1098    /// Requests disconnection from a Wi-Fi network.
1099    ///
1100    /// `request.ssid` can limit the operation to a specific network when the host
1101    /// supports that distinction. Some platforms do not allow apps to disconnect
1102    /// global network state.
1103    pub fn disconnect_network(self, request: WifiDisconnectRequest) -> EffectBuilder<'a, 'b, S> {
1104        self.effects.capability(DISCONNECT_WIFI_NETWORK, request)
1105    }
1106}
1107
1108/// Convenience builder for standard volume-control host capabilities.
1109pub struct VolumeEffects<'a, 'b, S: GlobalState> {
1110    effects: &'a mut Effects<'b, S>,
1111}
1112
1113impl<'a, 'b, S: GlobalState> VolumeEffects<'a, 'b, S> {
1114    /// Reads the current level for one host volume stream.
1115    ///
1116    /// `stream` identifies the logical audio stream the app cares about. Hosts
1117    /// map that stream to the closest platform mixer or media channel and return
1118    /// a `VolumeLevel` with level and mute state.
1119    pub fn get_level(self, stream: VolumeStream) -> EffectBuilder<'a, 'b, S> {
1120        self.effects.capability(GET_VOLUME_LEVEL, stream)
1121    }
1122
1123    /// Sets the level and optional mute state for one host volume stream.
1124    ///
1125    /// `request.level` is a percentage-like value from 0 to 100. Hosts should
1126    /// clamp or reject values they cannot represent and return a typed error when
1127    /// the platform does not expose system volume control.
1128    pub fn set_level(self, request: VolumeSetRequest) -> EffectBuilder<'a, 'b, S> {
1129        self.effects.capability(SET_VOLUME_LEVEL, request)
1130    }
1131
1132    /// Adjusts a host volume stream relative to its current level.
1133    ///
1134    /// `request.direction` chooses increase, decrease, or toggle mute, and
1135    /// `request.step` controls the requested amount. Use this for keyboard-like
1136    /// or remote-control volume actions.
1137    pub fn adjust_level(self, request: VolumeAdjustRequest) -> EffectBuilder<'a, 'b, S> {
1138        self.effects.capability(ADJUST_VOLUME_LEVEL, request)
1139    }
1140}
1141
1142/// Fluent builder returned by [`Effects::capability`], [`Effects::app`], and
1143/// related effect constructors.
1144///
1145/// Attach `on_ok` and `on_err` callback envelopes before the builder is dropped.
1146///
1147/// # Example
1148///
1149/// ```rust,ignore
1150/// ctx.effects.capability(MY_CAPABILITY, request)
1151///     .on_ok(ok_envelope)
1152///     .on_err(err_envelope)
1153///     .dispatch(); // optional -- dropping also finalises
1154/// ```
1155pub struct EffectBuilder<'a, 'b, S: GlobalState> {
1156    effects: &'a mut Effects<'b, S>,
1157    index: usize,
1158}
1159
1160impl<'a, 'b, S: GlobalState> EffectBuilder<'a, 'b, S> {
1161    pub fn on_ok(self, action: ActionEnvelope) -> Self {
1162        self.effects.out[self.index].on_ok = Some(action);
1163        self
1164    }
1165
1166    pub fn on_err(self, action: ActionEnvelope) -> Self {
1167        self.effects.out[self.index].on_err = Some(action);
1168        self
1169    }
1170
1171    pub fn dispatch(self) {
1172        // Drop
1173    }
1174}
1175
1176pub struct ServiceStartBuilder<'a, 'b, S: GlobalState> {
1177    effects: &'a mut Effects<'b, S>,
1178    index: usize,
1179}
1180
1181impl<'a, 'b, S: GlobalState> ServiceStartBuilder<'a, 'b, S> {
1182    pub fn on_started(self, action: ActionEnvelope) -> Self {
1183        if let Some(bindings) = self.effects.out[self.index].service_bindings.as_mut() {
1184            bindings.on_started = Some(action);
1185        }
1186        self
1187    }
1188
1189    pub fn on_start_failed(self, action: ActionEnvelope) -> Self {
1190        if let Some(bindings) = self.effects.out[self.index].service_bindings.as_mut() {
1191            bindings.on_start_failed = Some(action);
1192        }
1193        self
1194    }
1195
1196    pub fn on_event(self, action: ActionEnvelope) -> Self {
1197        if let Some(bindings) = self.effects.out[self.index].service_bindings.as_mut() {
1198            bindings.on_event = Some(action);
1199        }
1200        self
1201    }
1202
1203    pub fn on_stopped(self, action: ActionEnvelope) -> Self {
1204        if let Some(bindings) = self.effects.out[self.index].service_bindings.as_mut() {
1205            bindings.on_stopped = Some(action);
1206        }
1207        self
1208    }
1209
1210    pub fn on_command_ok(self, action: ActionEnvelope) -> Self {
1211        if let Some(bindings) = self.effects.out[self.index].service_bindings.as_mut() {
1212            bindings.on_command_ok = Some(action);
1213        }
1214        self
1215    }
1216
1217    pub fn on_command_err(self, action: ActionEnvelope) -> Self {
1218        if let Some(bindings) = self.effects.out[self.index].service_bindings.as_mut() {
1219            bindings.on_command_err = Some(action);
1220        }
1221        self
1222    }
1223
1224    pub fn dispatch(self) {}
1225}