1use crate::event;
2use crate::ffi;
3use std::rc::Rc;
4
5fn with_utf8(value: &str, callback: impl FnOnce(usize, u32)) {
6 let bytes = value.as_bytes();
7 callback(
8 if bytes.is_empty() {
9 0
10 } else {
11 bytes.as_ptr() as usize
12 },
13 bytes.len() as u32,
14 );
15}
16
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct PersistedScrollOffset {
19 pub x: f32,
20 pub y: f32,
21}
22
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct PersistedTextState {
25 pub version: u32,
26 pub payload: String,
27}
28
29pub trait PersistedStateAdapter {
30 fn kind(&self) -> &str;
31 fn version(&self) -> u32;
32 fn capture(&self) -> Option<String>;
33 fn restore(&self, payload: &str, version: u32);
34}
35
36pub trait PersistedStateCodec<T>: 'static {
37 fn encode(&self, value: T) -> String;
38 fn decode(&self, payload: &str, version: u32) -> T;
39}
40
41pub struct PersistedStringCodec;
42pub struct PersistedBoolCodec;
43pub struct PersistedInt32Codec;
44pub struct PersistedFloat32Codec;
45
46impl PersistedStateCodec<String> for PersistedStringCodec {
47 fn encode(&self, value: String) -> String {
48 value
49 }
50
51 fn decode(&self, payload: &str, _version: u32) -> String {
52 payload.to_string()
53 }
54}
55
56impl PersistedStateCodec<bool> for PersistedBoolCodec {
57 fn encode(&self, value: bool) -> String {
58 value.to_string()
59 }
60
61 fn decode(&self, payload: &str, _version: u32) -> bool {
62 payload.parse::<bool>().unwrap_or(false)
63 }
64}
65
66impl PersistedStateCodec<i32> for PersistedInt32Codec {
67 fn encode(&self, value: i32) -> String {
68 value.to_string()
69 }
70
71 fn decode(&self, payload: &str, _version: u32) -> i32 {
72 payload.parse::<i32>().unwrap_or(0)
73 }
74}
75
76impl PersistedStateCodec<f32> for PersistedFloat32Codec {
77 fn encode(&self, value: f32) -> String {
78 value.to_string()
79 }
80
81 fn decode(&self, payload: &str, _version: u32) -> f32 {
82 payload.parse::<f32>().unwrap_or(0.0)
83 }
84}
85
86struct PersistedValueAdapter<T, TCodec>
87where
88 T: 'static,
89 TCodec: PersistedStateCodec<T>,
90{
91 kind: String,
92 version: u32,
93 codec: TCodec,
94 capture_value: Rc<dyn Fn() -> Option<T>>,
95 restore_value: Rc<dyn Fn(T)>,
96}
97
98impl<T, TCodec> PersistedStateAdapter for PersistedValueAdapter<T, TCodec>
99where
100 T: 'static,
101 TCodec: PersistedStateCodec<T>,
102{
103 fn kind(&self) -> &str {
104 &self.kind
105 }
106
107 fn version(&self) -> u32 {
108 self.version
109 }
110
111 fn capture(&self) -> Option<String> {
112 (self.capture_value)().map(|value| self.codec.encode(value))
113 }
114
115 fn restore(&self, payload: &str, version: u32) {
116 (self.restore_value)(self.codec.decode(payload, version));
117 }
118}
119
120pub fn persisted_value_adapter<T, TCodec>(
121 kind: impl Into<String>,
122 codec: TCodec,
123 version: u32,
124 capture_value: impl Fn() -> Option<T> + 'static,
125 restore_value: impl Fn(T) + 'static,
126) -> Rc<dyn PersistedStateAdapter>
127where
128 T: 'static,
129 TCodec: PersistedStateCodec<T>,
130{
131 let kind = kind.into();
132 assert!(
133 !kind.is_empty(),
134 "PersistedStateAdapter requires a non-empty kind."
135 );
136 Rc::new(PersistedValueAdapter {
137 kind,
138 version,
139 codec,
140 capture_value: Rc::new(capture_value),
141 restore_value: Rc::new(restore_value),
142 })
143}
144
145pub fn store_scroll_offset(node_id: &str, x: f32, y: f32) {
146 with_utf8(node_id, |node_id_ptr, node_id_len| unsafe {
147 ffi::fui_set_persisted_scroll_offset(node_id_ptr, node_id_len, x, y);
148 });
149}
150
151pub fn store_text_state(node_id: &str, kind: &str, version: u32, payload: &str) {
152 with_utf8(node_id, |node_id_ptr, node_id_len| {
153 with_utf8(kind, |kind_ptr, kind_len| {
154 with_utf8(payload, |payload_ptr, payload_len| unsafe {
155 ffi::fui_set_persisted_state(
156 node_id_ptr,
157 node_id_len,
158 kind_ptr,
159 kind_len,
160 version,
161 payload_ptr,
162 payload_len,
163 );
164 })
165 })
166 });
167}
168
169pub fn try_load_scroll_offset(node_id: &str) -> Option<PersistedScrollOffset> {
170 let mut x = 0.0f32;
171 let mut y = 0.0f32;
172 let found = with_utf8_result(node_id, |node_id_ptr, node_id_len| unsafe {
173 ffi::fui_try_get_persisted_scroll_offset(
174 node_id_ptr,
175 node_id_len,
176 (&mut x as *mut f32) as usize,
177 (&mut y as *mut f32) as usize,
178 )
179 });
180 if found {
181 Some(PersistedScrollOffset { x, y })
182 } else {
183 None
184 }
185}
186
187pub fn try_load_text_state(node_id: &str, kind: &str) -> Option<PersistedTextState> {
188 let mut version = 0u32;
189 let payload_ptr = event::__fui_text_buffer() as usize;
190 let payload_capacity = event::__fui_text_buffer_size();
191 let copied = with_utf8_result(node_id, |node_id_ptr, node_id_len| {
192 with_utf8_result(kind, |kind_ptr, kind_len| unsafe {
193 ffi::fui_copy_persisted_state(
194 node_id_ptr,
195 node_id_len,
196 kind_ptr,
197 kind_len,
198 (&mut version as *mut u32) as usize,
199 payload_ptr,
200 payload_capacity,
201 )
202 })
203 });
204 if copied < 0 {
205 return None;
206 }
207 let copied_len = copied as usize;
208 if copied_len > payload_capacity as usize {
209 panic!("Persisted state payload exceeded shared Rust text buffer capacity.");
210 }
211 let payload = if copied_len == 0 {
212 String::new()
213 } else {
214 let bytes = unsafe { std::slice::from_raw_parts(payload_ptr as *const u8, copied_len) };
215 String::from_utf8_lossy(bytes).into_owned()
216 };
217 Some(PersistedTextState { version, payload })
218}
219
220fn with_utf8_result<T>(value: &str, callback: impl FnOnce(usize, u32) -> T) -> T {
221 let bytes = value.as_bytes();
222 callback(
223 if bytes.is_empty() {
224 0
225 } else {
226 bytes.as_ptr() as usize
227 },
228 bytes.len() as u32,
229 )
230}
231
232#[cfg(test)]
233mod tests {
234 use super::{
235 persisted_value_adapter, store_scroll_offset, store_text_state, try_load_scroll_offset,
236 try_load_text_state, PersistedBoolCodec, PersistedScrollOffset, PersistedTextState,
237 };
238 use crate::ffi::{self, Call};
239
240 #[test]
241 fn persisted_scroll_round_trips_through_host() {
242 ffi::test::reset();
243 store_scroll_offset("list", 12.0, 34.0);
244 assert_eq!(
245 try_load_scroll_offset("list"),
246 Some(PersistedScrollOffset { x: 12.0, y: 34.0 }),
247 );
248 let calls = ffi::test::take_calls();
249 assert!(calls.iter().any(|call| matches!(call, Call::SetPersistedScrollOffset { node_id, x, y } if node_id == "list" && *x == 12.0 && *y == 34.0)));
250 assert!(calls.iter().any(|call| matches!(call, Call::TryGetPersistedScrollOffset { node_id } if node_id == "list")));
251 }
252
253 #[test]
254 fn persisted_text_round_trips_through_host() {
255 ffi::test::reset();
256 store_text_state("input", "text", 2, "hello");
257 assert_eq!(
258 try_load_text_state("input", "text"),
259 Some(PersistedTextState {
260 version: 2,
261 payload: "hello".to_string(),
262 }),
263 );
264 let calls = ffi::test::take_calls();
265 assert!(calls.iter().any(|call| matches!(call, Call::SetPersistedState { node_id, kind, version, payload } if node_id == "input" && kind == "text" && *version == 2 && payload == "hello")));
266 assert!(calls.iter().any(|call| matches!(call, Call::CopyPersistedState { node_id, kind } if node_id == "input" && kind == "text")));
267 }
268
269 #[test]
270 #[should_panic(expected = "PersistedStateAdapter requires a non-empty kind.")]
271 fn persisted_value_adapter_rejects_empty_kind() {
272 let _ = persisted_value_adapter("", PersistedBoolCodec, 1, || Some(true), |_| {});
273 }
274}