plugin_interfaces/pluginui/
ui.rs1use crate::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 { value: *value },
189 };
190 self.add_component(component);
191
192 Response::new_with_component_and_state(id, was_clicked, was_changed)
194 }
195
196 pub fn horizontal<R>(&mut self, add_contents: impl FnOnce(&mut Self) -> R) -> R {
198 self.layout_stack.push(LayoutContext::Horizontal);
199 let start_index = self.components.len();
200
201 let result = add_contents(self);
202
203 let children = self.components.split_off(start_index);
205
206 if !children.is_empty() {
207 let horizontal_component = UiComponent {
208 id: format!("horizontal_{}", Uuid::new_v4()),
209 component: UiComponentType::Horizontal { children },
210 };
211 self.components.push(horizontal_component);
212 }
213
214 self.layout_stack.pop();
215 result
216 }
217
218 pub fn vertical<R>(&mut self, add_contents: impl FnOnce(&mut Self) -> R) -> R {
220 self.layout_stack.push(LayoutContext::Vertical);
221 let start_index = self.components.len();
222
223 let result = add_contents(self);
224
225 let children = self.components.split_off(start_index);
227
228 if !children.is_empty() {
229 let vertical_component = UiComponent {
230 id: format!("vertical_{}", Uuid::new_v4()),
231 component: UiComponentType::Vertical { children },
232 };
233 self.components.push(vertical_component);
234 }
235
236 self.layout_stack.pop();
237 result
238 }
239
240 fn add_component(&mut self, component: UiComponent) {
242 self.components.push(component);
243 }
244
245 pub fn get_components(&self) -> &[UiComponent] {
247 &self.components
248 }
249
250 pub fn clear(&mut self) {
252 self.components.clear();
253 self.clicked_components.clear();
255 self.changed_components.clear();
256 self.ui_event_data.clear();
257 }
258
259 pub fn clear_components_only(&mut self) {
261 self.components.clear();
262 }
264
265 pub fn clear_events(&mut self) {
267 self.clicked_components.clear();
268 self.changed_components.clear();
269 self.ui_event_data.clear();
270 }
271
272 pub fn handle_ui_event(&mut self, component_id: &str, value: &str) -> bool {
274 self.ui_event_data
276 .insert(component_id.to_string(), value.to_string());
277
278 if component_id.starts_with("combo_") {
280 self.clicked_components.insert(component_id.to_string());
281 self.changed_components.insert(component_id.to_string());
282 true
283 } else if component_id.starts_with("button_") {
284 self.clicked_components.insert(component_id.to_string());
285 true
286 } else if component_id.starts_with("textedit_") {
287 self.changed_components.insert(component_id.to_string());
288 true
289 } else if component_id.starts_with("toggle_") {
290 self.clicked_components.insert(component_id.to_string());
291 self.changed_components.insert(component_id.to_string());
292 true
293 } else {
294 false
295 }
296 }
297}
298
299pub trait PluginUiOption {
302 fn refresh_ui(&self, plugin_ctx: &crate::metadata::PluginInstanceContext) -> bool;
304}
305
306impl<T: PluginHandler> PluginUiOption for T {
307 fn refresh_ui(&self, plugin_ctx: &crate::metadata::PluginInstanceContext) -> bool {
308 let plugin_id = &plugin_ctx.metadata.id;
310 let instance_id = plugin_ctx
311 .metadata
312 .instance_id
313 .as_ref()
314 .unwrap_or(&plugin_ctx.metadata.id);
315
316 let payload = serde_json::json!({
318 "plugin": plugin_id,
319 "instance": instance_id
320 })
321 .to_string();
322
323 plugin_ctx.send_to_frontend("plugin-ui-refreshed", &payload)
325 }
326}