use crate::PluginHandler;
use super::components::{Response, UiComponent, UiComponentType};
use std::{
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
};
use uuid::Uuid;
pub struct Ui {
pub(crate) components: Vec<UiComponent>,
pub(crate) plugin_id: String,
pub(crate) layout_stack: Vec<LayoutContext>,
pub(crate) clicked_components: HashSet<String>,
pub(crate) changed_components: HashSet<String>,
pub(crate) ui_event_data: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub(crate) enum LayoutContext {
Root,
Horizontal,
Vertical,
}
impl Ui {
pub fn new(plugin_id: String) -> Arc<Mutex<Self>> {
Arc::new(Mutex::new(Self {
components: Vec::new(),
plugin_id,
layout_stack: vec![LayoutContext::Root],
clicked_components: HashSet::new(),
changed_components: HashSet::new(),
ui_event_data: HashMap::new(),
}))
}
pub fn plugin_id(&self) -> &str {
&self.plugin_id
}
pub fn label(&mut self, text: &str) {
let component = UiComponent {
id: format!("label_{}", Uuid::new_v4()),
component: UiComponentType::Label {
text: text.to_string(),
},
};
self.add_component(component);
}
pub fn button(&mut self, text: &str) -> Response {
let id = format!(
"button_{}_{}",
self.components.len(),
text.replace(" ", "_")
);
let component = UiComponent {
id: id.clone(),
component: UiComponentType::Button {
text: text.to_string(),
enabled: true,
},
};
self.add_component(component);
let was_clicked = self.clicked_components.contains(&id);
Response::new_with_component_and_state(id, was_clicked, false)
}
pub fn text_edit_singleline(&mut self, value: &mut String) -> Response {
let id = format!("textedit_{}", self.components.len());
let was_changed = self.changed_components.contains(&id);
if was_changed {
if let Some(new_value) = self.ui_event_data.get(&id) {
*value = new_value.clone();
}
}
let component = UiComponent {
id: id.clone(),
component: UiComponentType::TextEdit {
value: value.clone(),
hint: String::new(),
},
};
self.add_component(component);
Response::new_with_component_and_state(id, false, was_changed)
}
pub fn combo_box<T>(
&mut self,
options: Vec<T>,
selected: &mut Option<T>,
placeholder: &str,
) -> Response
where
T: Clone + PartialEq + ToString,
{
let id = format!(
"combo_{}_{}",
self.components.len(),
placeholder.replace(" ", "_")
);
let was_clicked = self.clicked_components.contains(&id);
let was_changed = self.changed_components.contains(&id);
if was_changed {
if let Some(new_value) = self.ui_event_data.get(&id) {
if let Ok(selection_index) = new_value.parse::<usize>() {
if selection_index < options.len() {
*selected = Some(options[selection_index].clone());
} else {
*selected = None;
}
}
}
}
let selected_index = if let Some(ref selected_value) = selected {
options.iter().position(|opt| opt == selected_value)
} else {
None
};
if selected.is_some() && selected_index.is_none() {
*selected = None;
}
let component = UiComponent {
id: id.clone(),
component: UiComponentType::ComboBox {
options: options.iter().map(|opt| opt.to_string()).collect(),
selected: selected_index,
placeholder: placeholder.to_string(),
},
};
self.add_component(component);
Response::new_with_component_and_state(id, was_clicked, was_changed)
}
pub fn toggle(&mut self, value: &mut bool) -> Response {
let id = format!("toggle_{}", self.components.len());
let was_clicked = self.clicked_components.contains(&id);
let was_changed = self.changed_components.contains(&id);
if was_changed {
if let Some(new_value) = self.ui_event_data.get(&id) {
if let Ok(toggle_value) = new_value.parse::<bool>() {
*value = toggle_value;
}
}
}
let component = UiComponent {
id: id.clone(),
component: UiComponentType::Toggle { value: *value },
};
self.add_component(component);
Response::new_with_component_and_state(id, was_clicked, was_changed)
}
pub fn horizontal<R>(&mut self, add_contents: impl FnOnce(&mut Self) -> R) -> R {
self.layout_stack.push(LayoutContext::Horizontal);
let start_index = self.components.len();
let result = add_contents(self);
let children = self.components.split_off(start_index);
if !children.is_empty() {
let horizontal_component = UiComponent {
id: format!("horizontal_{}", Uuid::new_v4()),
component: UiComponentType::Horizontal { children },
};
self.components.push(horizontal_component);
}
self.layout_stack.pop();
result
}
pub fn vertical<R>(&mut self, add_contents: impl FnOnce(&mut Self) -> R) -> R {
self.layout_stack.push(LayoutContext::Vertical);
let start_index = self.components.len();
let result = add_contents(self);
let children = self.components.split_off(start_index);
if !children.is_empty() {
let vertical_component = UiComponent {
id: format!("vertical_{}", Uuid::new_v4()),
component: UiComponentType::Vertical { children },
};
self.components.push(vertical_component);
}
self.layout_stack.pop();
result
}
fn add_component(&mut self, component: UiComponent) {
self.components.push(component);
}
pub fn get_components(&self) -> &[UiComponent] {
&self.components
}
pub fn clear(&mut self) {
self.components.clear();
self.clicked_components.clear();
self.changed_components.clear();
self.ui_event_data.clear();
}
pub fn clear_components_only(&mut self) {
self.components.clear();
}
pub fn clear_events(&mut self) {
self.clicked_components.clear();
self.changed_components.clear();
self.ui_event_data.clear();
}
pub fn handle_ui_event(&mut self, component_id: &str, value: &str) -> bool {
self.ui_event_data
.insert(component_id.to_string(), value.to_string());
if component_id.starts_with("combo_") {
self.clicked_components.insert(component_id.to_string());
self.changed_components.insert(component_id.to_string());
true
} else if component_id.starts_with("button_") {
self.clicked_components.insert(component_id.to_string());
true
} else if component_id.starts_with("textedit_") {
self.changed_components.insert(component_id.to_string());
true
} else if component_id.starts_with("toggle_") {
self.clicked_components.insert(component_id.to_string());
self.changed_components.insert(component_id.to_string());
true
} else {
false
}
}
}
pub trait PluginUiOption {
fn refresh_ui(&self, plugin_ctx: &crate::metadata::PluginInstanceContext) -> bool;
}
impl<T: PluginHandler> PluginUiOption for T {
fn refresh_ui(&self, plugin_ctx: &crate::metadata::PluginInstanceContext) -> bool {
let plugin_id = &plugin_ctx.metadata.id;
let instance_id = plugin_ctx
.metadata
.instance_id
.as_ref()
.unwrap_or(&plugin_ctx.metadata.id);
let payload = serde_json::json!({
"plugin": plugin_id,
"instance": instance_id
})
.to_string();
plugin_ctx.send_to_frontend("plugin-ui-refreshed", &payload)
}
}