plugin_interfaces/pluginui/
ui.rs

1//! Main UI builder implementation
2
3use crate::PluginHandler;
4
5use super::components::{Response, UiComponent, UiComponentType};
6use std::{
7    collections::{HashMap, HashSet},
8    sync::{Arc, Mutex},
9};
10use uuid::Uuid;
11
12/// Main UI builder - provides immediate mode UI building
13pub struct Ui {
14    /// Components built in this frame
15    pub(crate) components: Vec<UiComponent>,
16    /// Plugin ID for this UI
17    pub(crate) plugin_id: String,
18    /// Current layout context
19    pub(crate) layout_stack: Vec<LayoutContext>,
20    /// Components that were clicked in this frame
21    pub(crate) clicked_components: HashSet<String>,
22    /// Components that were changed in this frame
23    pub(crate) changed_components: HashSet<String>,
24    /// UI event data from frontend (component_id -> value)
25    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    /// Get plugin Id
48    pub fn plugin_id(&self) -> &str {
49        &self.plugin_id
50    }
51
52    /// Add a text label
53    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    /// Add a clickable button
64    pub fn button(&mut self, text: &str) -> Response {
65        // Use a hash of the text and current component count for stable ID
66        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        // Check if this component was clicked in this frame
81        let was_clicked = self.clicked_components.contains(&id);
82
83        // Return a response with click state
84        Response::new_with_component_and_state(id, was_clicked, false)
85    }
86
87    /// Add a single-line text editor
88    pub fn text_edit_singleline(&mut self, value: &mut String) -> Response {
89        // Use a stable ID based on component count for consistent identification
90        let id = format!("textedit_{}", self.components.len());
91
92        // Check if this component was changed and update the value from frontend data
93        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    /// Add a combo box (dropdown) widget
113    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        // Use a stable ID based on component count and placeholder
123        let id = format!(
124            "combo_{}_{}",
125            self.components.len(),
126            placeholder.replace(" ", "_")
127        );
128
129        // Check if this component was clicked or changed and update the selection from frontend data
130        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        // Validate that selected value exists in options, otherwise set to None
145        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 value doesn't exist in options, set selected to None
152        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        // Return a response with event states
167        Response::new_with_component_and_state(id, was_clicked, was_changed)
168    }
169
170    /// Add a toggle switch
171    pub fn toggle(&mut self, value: &mut bool) -> Response {
172        // Use a stable ID based on component count
173        let id = format!("toggle_{}", self.components.len());
174
175        // Check if this component was clicked and update the value from frontend data
176        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        // Return a response with event states
193        Response::new_with_component_and_state(id, was_clicked, was_changed)
194    }
195
196    /// Create a horizontal layout
197    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        // Collect components added in this horizontal context
204        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    /// Create a vertical layout
219    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        // Collect components added in this vertical context
226        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    /// Internal method to add a component
241    fn add_component(&mut self, component: UiComponent) {
242        self.components.push(component);
243    }
244
245    /// Get all components for serialization
246    pub fn get_components(&self) -> &[UiComponent] {
247        &self.components
248    }
249
250    /// Clear all components (called at start of each frame)
251    pub fn clear(&mut self) {
252        self.components.clear();
253        // Clear event tracking - events should only be active for one frame
254        self.clicked_components.clear();
255        self.changed_components.clear();
256        self.ui_event_data.clear();
257    }
258
259    /// Clear only components, keep event tracking for current frame
260    pub fn clear_components_only(&mut self) {
261        self.components.clear();
262        // Keep event tracking for this update_ui call
263    }
264
265    /// Clear event tracking after update_ui is complete
266    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    /// Handle UI events (called when frontend sends UI events)
273    pub fn handle_ui_event(&mut self, component_id: &str, value: &str) -> bool {
274        // Store the event data for use in the event loop
275        self.ui_event_data
276            .insert(component_id.to_string(), value.to_string());
277
278        // Track the event based on component type for the event loop pattern
279        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
299/// 插件UI选项 trait
300/// 提供UI相关的便捷方法,使用上下文传递模式
301pub trait PluginUiOption {
302    /// 刷新UI,需要传入插件实例上下文
303    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        // 使用上下文中的信息发送UI刷新事件
309        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        // 构建刷新事件的载荷
317        let payload = serde_json::json!({
318            "plugin": plugin_id,
319            "instance": instance_id
320        })
321        .to_string();
322
323        // 通过上下文发送消息到前端
324        plugin_ctx.send_to_frontend("plugin-ui-refreshed", &payload)
325    }
326}