plugin_interfaces/pluginui/
ui.rs1use crate::{send_to_frontend, PluginHandler};
4
5use super::components::{Response, UiComponent, UiComponentType};
6use std::{
7 collections::{HashMap, HashSet},
8 sync::{Arc, Mutex},
9};
10use uuid::Uuid;
11
12pub struct Ui {
14 pub(crate) components: Vec<UiComponent>,
16 pub(crate) plugin_id: String,
18 pub(crate) layout_stack: Vec<LayoutContext>,
20 pub(crate) clicked_components: HashSet<String>,
22 pub(crate) changed_components: HashSet<String>,
24 pub(crate) ui_event_data: HashMap<String, String>,
26}
27
28#[derive(Debug, Clone)]
29pub(crate) enum LayoutContext {
30 Root,
31 Horizontal,
32 Vertical,
33}
34
35impl Ui {
36 pub fn new(plugin_id: String) -> Arc<Mutex<Self>> {
37 Arc::new(Mutex::new(Self {
38 components: Vec::new(),
39 plugin_id,
40 layout_stack: vec![LayoutContext::Root],
41 clicked_components: HashSet::new(),
42 changed_components: HashSet::new(),
43 ui_event_data: HashMap::new(),
44 }))
45 }
46
47 pub fn plugin_id(&self) -> &str {
49 &self.plugin_id
50 }
51
52 pub fn label(&mut self, text: &str) {
54 let component = UiComponent {
55 id: format!("label_{}", Uuid::new_v4()),
56 component: UiComponentType::Label {
57 text: text.to_string(),
58 },
59 };
60 self.add_component(component);
61 }
62
63 pub fn button(&mut self, text: &str) -> Response {
65 let id = format!(
67 "button_{}_{}",
68 self.components.len(),
69 text.replace(" ", "_")
70 );
71 let component = UiComponent {
72 id: id.clone(),
73 component: UiComponentType::Button {
74 text: text.to_string(),
75 enabled: true,
76 },
77 };
78 self.add_component(component);
79
80 let was_clicked = self.clicked_components.contains(&id);
82
83 Response::new_with_component_and_state(id, was_clicked, false)
85 }
86
87 pub fn text_edit_singleline(&mut self, value: &mut String) -> Response {
89 let id = format!("textedit_{}", self.components.len());
91
92 let was_changed = self.changed_components.contains(&id);
94 if was_changed {
95 if let Some(new_value) = self.ui_event_data.get(&id) {
96 *value = new_value.clone();
97 }
98 }
99
100 let component = UiComponent {
101 id: id.clone(),
102 component: UiComponentType::TextEdit {
103 value: value.clone(),
104 hint: String::new(),
105 },
106 };
107 self.add_component(component);
108
109 Response::new_with_component_and_state(id, false, was_changed)
110 }
111
112 pub fn combo_box<T>(
114 &mut self,
115 options: Vec<T>,
116 selected: &mut Option<T>,
117 placeholder: &str,
118 ) -> Response
119 where
120 T: Clone + PartialEq + ToString,
121 {
122 let id = format!(
124 "combo_{}_{}",
125 self.components.len(),
126 placeholder.replace(" ", "_")
127 );
128
129 let was_clicked = self.clicked_components.contains(&id);
131 let was_changed = self.changed_components.contains(&id);
132 if was_changed {
133 if let Some(new_value) = self.ui_event_data.get(&id) {
134 if let Ok(selection_index) = new_value.parse::<usize>() {
135 if selection_index < options.len() {
136 *selected = Some(options[selection_index].clone());
137 } else {
138 *selected = None;
139 }
140 }
141 }
142 }
143
144 let selected_index = if let Some(ref selected_value) = selected {
146 options.iter().position(|opt| opt == selected_value)
147 } else {
148 None
149 };
150
151 if selected.is_some() && selected_index.is_none() {
153 *selected = None;
154 }
155
156 let component = UiComponent {
157 id: id.clone(),
158 component: UiComponentType::ComboBox {
159 options: options.iter().map(|opt| opt.to_string()).collect(),
160 selected: selected_index,
161 placeholder: placeholder.to_string(),
162 },
163 };
164 self.add_component(component);
165
166 Response::new_with_component_and_state(id, was_clicked, was_changed)
168 }
169
170 pub fn toggle(&mut self, value: &mut bool) -> Response {
172 let id = format!("toggle_{}", self.components.len());
174
175 let was_clicked = self.clicked_components.contains(&id);
177 let was_changed = self.changed_components.contains(&id);
178 if was_changed {
179 if let Some(new_value) = self.ui_event_data.get(&id) {
180 if let Ok(toggle_value) = new_value.parse::<bool>() {
181 *value = toggle_value;
182 }
183 }
184 }
185
186 let component = UiComponent {
187 id: id.clone(),
188 component: UiComponentType::Toggle {
189 value: *value,
190 },
191 };
192 self.add_component(component);
193
194 Response::new_with_component_and_state(id, was_clicked, was_changed)
196 }
197
198 pub fn horizontal<R>(&mut self, add_contents: impl FnOnce(&mut Self) -> R) -> R {
200 self.layout_stack.push(LayoutContext::Horizontal);
201 let start_index = self.components.len();
202
203 let result = add_contents(self);
204
205 let children = self.components.split_off(start_index);
207
208 if !children.is_empty() {
209 let horizontal_component = UiComponent {
210 id: format!("horizontal_{}", Uuid::new_v4()),
211 component: UiComponentType::Horizontal { children },
212 };
213 self.components.push(horizontal_component);
214 }
215
216 self.layout_stack.pop();
217 result
218 }
219
220 pub fn vertical<R>(&mut self, add_contents: impl FnOnce(&mut Self) -> R) -> R {
222 self.layout_stack.push(LayoutContext::Vertical);
223 let start_index = self.components.len();
224
225 let result = add_contents(self);
226
227 let children = self.components.split_off(start_index);
229
230 if !children.is_empty() {
231 let vertical_component = UiComponent {
232 id: format!("vertical_{}", Uuid::new_v4()),
233 component: UiComponentType::Vertical { children },
234 };
235 self.components.push(vertical_component);
236 }
237
238 self.layout_stack.pop();
239 result
240 }
241
242 fn add_component(&mut self, component: UiComponent) {
244 self.components.push(component);
245 }
246
247 pub fn get_components(&self) -> &[UiComponent] {
249 &self.components
250 }
251
252 pub fn clear(&mut self) {
254 self.components.clear();
255 self.clicked_components.clear();
257 self.changed_components.clear();
258 self.ui_event_data.clear();
259 }
260
261 pub fn clear_components_only(&mut self) {
263 self.components.clear();
264 }
266
267 pub fn clear_events(&mut self) {
269 self.clicked_components.clear();
270 self.changed_components.clear();
271 self.ui_event_data.clear();
272 }
273
274 pub fn handle_ui_event(&mut self, component_id: &str, value: &str) -> bool {
276 self.ui_event_data
278 .insert(component_id.to_string(), value.to_string());
279
280 if component_id.starts_with("combo_") {
282 self.clicked_components.insert(component_id.to_string());
283 self.changed_components.insert(component_id.to_string());
284 true
285 } else if component_id.starts_with("button_") {
286 self.clicked_components.insert(component_id.to_string());
287 true
288 } else if component_id.starts_with("textedit_") {
289 self.changed_components.insert(component_id.to_string());
290 true
291 } else if component_id.starts_with("toggle_") {
292 self.clicked_components.insert(component_id.to_string());
293 self.changed_components.insert(component_id.to_string());
294 true
295 } else {
296 false
297 }
298 }
299}
300
301pub trait PluginUiOption {
302 fn refresh_ui(&self) -> bool;
303}
304
305impl<T: PluginHandler> PluginUiOption for T {
306 fn refresh_ui(&self) -> bool {
307 let plugin_id = self.get_metadata().id;
308 let payload = serde_json::json!({
309 "plugin": plugin_id
310 }).to_string();
311 send_to_frontend("plugin-ui-refreshed", &payload.as_str())
312 }
313}