StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use crate::elements::Empty; |
| 2 | use crate::platform::WindowStyle; |
| 3 | use crate::{App, AppContext, Element, Entity, TypedActionView}; |
| 4 | |
| 5 | #[test] |
| 6 | fn test_spawn_from_view() { |
| 7 | #[derive(Default)] |
| 8 | struct View { |
| 9 | count: usize, |
| 10 | } |
| 11 | |
| 12 | impl Entity for View { |
| 13 | type Event = (); |
| 14 | } |
| 15 | |
| 16 | impl super::View for View { |
| 17 | fn render<'a>(&self, _: &AppContext) -> Box<dyn Element> { |
| 18 | Empty::new().finish() |
| 19 | } |
| 20 | |
| 21 | fn ui_name() -> &'static str { |
| 22 | "View" |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | impl TypedActionView for View { |
| 27 | type Action = (); |
| 28 | } |
| 29 | |
| 30 | App::test((), |mut app| async move { |
| 31 | let (_, handle) = app.add_window(WindowStyle::NotStealFocus, |_| View::default()); |
| 32 | |
| 33 | let (tx, rx) = futures::channel::oneshot::channel(); |
| 34 | handle.update(&mut app, move |_, c| { |
| 35 | c.spawn(async { 7 }, move |me, output, _| { |
| 36 | tx.send(()).unwrap(); |
| 37 | me.count = output; |
| 38 | }) |
| 39 | }); |
| 40 | rx.await.unwrap(); |
| 41 | |
| 42 | let (tx, rx) = futures::channel::oneshot::channel(); |
| 43 | handle.read(&app, |view, _| assert_eq!(view.count, 7)); |
| 44 | handle.update(&mut app, move |_, c| { |
| 45 | c.spawn(async { 14 }, move |me, output, _| { |
| 46 | tx.send(()).unwrap(); |
| 47 | me.count = output; |
| 48 | }) |
| 49 | }); |
| 50 | rx.await.unwrap(); |
| 51 | handle.read(&app, |view, _| assert_eq!(view.count, 14)); |
| 52 | }); |
| 53 | } |
| 54 | |
| 55 | #[ignore] |
| 56 | #[test] |
| 57 | fn test_spawn_abortable_from_view() { |
| 58 | #[derive(Debug, Default, PartialEq)] |
| 59 | enum SpawnedOutcome { |
| 60 | #[default] |
| 61 | NotStarted, |
| 62 | Aborted, |
| 63 | Resolved { |
| 64 | value: usize, |
| 65 | }, |
| 66 | } |
| 67 | |
| 68 | #[derive(Default)] |
| 69 | struct View { |
| 70 | spawned_outcome: SpawnedOutcome, |
| 71 | } |
| 72 | |
| 73 | impl Entity for View { |
| 74 | type Event = (); |
| 75 | } |
| 76 | |
| 77 | impl super::View for View { |
| 78 | fn render<'a>(&self, _: &AppContext) -> Box<dyn Element> { |
| 79 | Empty::new().finish() |
| 80 | } |
| 81 | |
| 82 | fn ui_name() -> &'static str { |
| 83 | "View" |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | impl TypedActionView for View { |
| 88 | type Action = (); |
| 89 | } |
| 90 | |
| 91 | App::test((), |mut app| async move { |
| 92 | let (_, handle) = app.add_window(WindowStyle::NotStealFocus, |_| View::default()); |
| 93 | |
| 94 | let (tx, rx) = futures::channel::oneshot::channel(); |
| 95 | handle.update(&mut app, |_, c| { |
| 96 | c.spawn_abortable( |
| 97 | async { 7 }, |
| 98 | move |me, output, _| { |
| 99 | tx.send(()).unwrap(); |
| 100 | me.spawned_outcome = SpawnedOutcome::Resolved { value: output } |
| 101 | }, |
| 102 | |_, _| {}, |
| 103 | ) |
| 104 | }); |
| 105 | rx.await.unwrap(); |
| 106 | |
| 107 | handle.read(&app, |view, _| { |
| 108 | assert_eq!(view.spawned_outcome, SpawnedOutcome::Resolved { value: 7 }) |
| 109 | }); |
| 110 | |
| 111 | let (tx, rx) = futures::channel::oneshot::channel(); |
| 112 | handle.update(&mut app, move |_, c| { |
| 113 | let abort_handle = c.spawn_abortable( |
| 114 | async { 7 }, |
| 115 | |_, _, _| {}, |
| 116 | move |me, _| { |
| 117 | me.spawned_outcome = SpawnedOutcome::Aborted; |
| 118 | tx.send(()).unwrap(); |
| 119 | }, |
| 120 | ); |
| 121 | abort_handle.abort(); |
| 122 | }); |
| 123 | |
| 124 | rx.await.unwrap(); |
| 125 | |
| 126 | // The call future passed to `spawn_abortable` was successfully aborted. |
| 127 | handle.read(&app, |view, _| { |
| 128 | assert_eq!(view.spawned_outcome, SpawnedOutcome::Aborted) |
| 129 | }); |
| 130 | }); |
| 131 | } |
| 132 | |
| 133 | #[test] |
| 134 | fn test_view_spawner() { |
| 135 | #[derive(Default)] |
| 136 | struct View { |
| 137 | count: usize, |
| 138 | } |
| 139 | |
| 140 | impl Entity for View { |
| 141 | type Event = (); |
| 142 | } |
| 143 | |
| 144 | impl super::View for View { |
| 145 | fn render<'a>(&self, _: &AppContext) -> Box<dyn Element> { |
| 146 | Empty::new().finish() |
| 147 | } |
| 148 | |
| 149 | fn ui_name() -> &'static str { |
| 150 | "View" |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | impl TypedActionView for View { |
| 155 | type Action = (); |
| 156 | } |
| 157 | |
| 158 | App::test((), |mut app| async move { |
| 159 | let (_, handle) = app.add_window(WindowStyle::NotStealFocus, |_| View::default()); |
| 160 | |
| 161 | let spawner = handle.update(&mut app, |_, ctx| ctx.spawner()); |
| 162 | |
| 163 | // Test a single spawned task. |
| 164 | let result = spawner |
| 165 | .spawn(|view, ctx| { |
| 166 | view.count += 42; |
| 167 | ctx.notify(); |
| 168 | view.count |
| 169 | }) |
| 170 | .await |
| 171 | .unwrap(); |
| 172 | |
| 173 | assert_eq!(result, 42); |
| 174 | handle.read(&app, |view, _| assert_eq!(view.count, 42)); |
| 175 | |
| 176 | // Test multiple spawned tasks. |
| 177 | let task1 = spawner.spawn(|view, _| { |
| 178 | view.count *= 2; |
| 179 | view.count |
| 180 | }); |
| 181 | |
| 182 | let task2 = spawner.spawn(|view, _| { |
| 183 | view.count += 10; |
| 184 | view.count |
| 185 | }); |
| 186 | |
| 187 | let (result1, result2) = futures::future::join(task1, task2).await; |
| 188 | |
| 189 | // Note: The exact final value depends on task execution order but both tasks should succeed. |
| 190 | assert!(result1.is_ok()); |
| 191 | assert!(result2.is_ok()); |
| 192 | |
| 193 | handle.read(&app, |view, _| { |
| 194 | assert!(view.count > 42); |
| 195 | }); |
| 196 | }); |
| 197 | } |
| 198 |