use std::collections::{HashMap, HashSet};
use a2ui_base::catalog::function_api::FunctionImplementation;
use a2ui_base::components::dispatch_event;
use a2ui_base::event::{InputEvent, InputKey};
use a2ui_base::focus::FocusManager;
use a2ui_base::interaction::apply_event_result;
use a2ui_base::message_processor::MessageProcessor;
use a2ui_base::model::component_context::ComponentContext;
use a2ui_base::protocol::common_types::DynamicString;
use a2ui_base::protocol::server_to_client::A2uiMessage;
use crate::components::Walk;
use crate::edit_state::EditBuffers;
use crate::interaction::PendingInteraction;
use crate::walker::render_node;
pub struct EguiApp {
processor: MessageProcessor,
functions: HashMap<String, Box<dyn FunctionImplementation>>,
focus: FocusManager,
samples: Vec<(String, Vec<A2uiMessage>)>,
selected_sample: usize,
edit_buffers: EditBuffers,
open_modals: HashSet<String>,
image_cache: HashMap<String, Option<egui::TextureHandle>>,
local_tabs: HashMap<String, usize>,
icons_installed: bool,
}
impl EguiApp {
pub fn new(
catalogs: Vec<a2ui_base::catalog::Catalog>,
functions: HashMap<String, Box<dyn FunctionImplementation>>,
) -> Self {
Self {
processor: MessageProcessor::new(catalogs),
functions,
focus: FocusManager::new(),
samples: Vec::new(),
selected_sample: 0,
edit_buffers: EditBuffers::default(),
open_modals: HashSet::new(),
image_cache: HashMap::new(),
local_tabs: HashMap::new(),
icons_installed: false,
}
}
pub fn set_samples(&mut self, samples: Vec<(String, Vec<A2uiMessage>)>, initial: usize) {
self.samples = samples;
self.load_sample(initial);
}
pub fn process_message(&mut self, message: A2uiMessage) {
let _ = self.processor.process_message(message);
self.rebuild_focus();
}
pub fn rebuild_focus(&mut self) {
if let Some(surface) = self.processor.model.surfaces().next() {
let components = surface.components.borrow();
self.focus.rebuild_from_components(&components);
}
}
fn load_sample(&mut self, idx: usize) {
let Some(messages) = self.samples.get(idx).map(|(_, m)| m.clone()) else {
return;
};
self.processor.reset();
for msg in &messages {
let _ = self.processor.process_message(msg.clone());
}
self.focus.reset();
if let Some(surface) = self.processor.model.surfaces().next() {
let components = surface.components.borrow();
self.focus.rebuild_from_components(&components);
}
self.edit_buffers.invalidate();
self.open_modals.clear();
self.image_cache.clear();
self.local_tabs.clear();
self.selected_sample = idx;
}
}
impl eframe::App for EguiApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
if !self.icons_installed {
install_icon_font(ui.ctx());
self.icons_installed = true;
}
self.load_images(ui.ctx());
let selected = self.selected_sample;
let mut clicked: Option<usize> = None;
egui::Panel::left("sample_browser")
.default_size(240.0)
.resizable(true)
.show_inside(ui, |ui| {
ui.heading("Samples");
egui::ScrollArea::vertical().show(ui, |ui| {
for (i, (name, _)) in self.samples.iter().enumerate() {
let is_sel = i == selected;
if ui.selectable_label(is_sel, name).clicked() {
clicked = Some(i);
}
}
});
});
if let Some(i) = clicked {
self.load_sample(i);
}
let mut pending: Vec<PendingInteraction> = Vec::new();
egui::CentralPanel::default().show_inside(ui, |ui| {
self.edit_buffers.begin_frame();
let Some(surface) = self.processor.model.surfaces().next() else {
ui.label("No surface loaded.");
return;
};
if !surface.components.borrow().contains("root") {
ui.label("No root component");
return;
}
let data_model = surface.data_model.borrow();
let components = surface.components.borrow();
let focused_id = self.focus.focused_id().map(str::to_string);
let walk = Walk {
surface_id: &surface.id,
data_model: &data_model,
components: &components,
functions: &self.functions,
focused_id: focused_id.as_deref(),
open_modals: &self.open_modals,
image_cache: &self.image_cache,
local_tabs: &self.local_tabs,
};
egui::ScrollArea::vertical().show(ui, |ui| {
render_node(
"root",
walk.surface_id,
"",
ui,
walk.data_model,
walk.components,
walk.functions,
walk.focused_id,
walk.open_modals,
walk.image_cache,
walk.local_tabs,
&mut self.edit_buffers,
&mut pending,
);
});
});
let ctx = ui.ctx().clone();
let open_modals: Vec<String> = self.open_modals.iter().cloned().collect();
for modal_id in open_modals {
self.render_modal_overlay(&ctx, &modal_id, &mut pending);
}
self.apply_pending(pending);
}
}
impl EguiApp {
fn render_modal_overlay(
&mut self,
ctx: &egui::Context,
modal_id: &str,
pending: &mut Vec<PendingInteraction>,
) {
let content_id = {
let Some(surface) = self.processor.model.surfaces().next() else {
return;
};
let components = surface.components.borrow();
components.get(modal_id).and_then(|m| {
(m.component_type == "Modal")
.then(|| m.get_property::<String>("content"))
.flatten()
})
};
let Some(content_id) = content_id else { return };
let mut open = true;
egui::Window::new("Modal")
.id(egui::Id::new(modal_id))
.open(&mut open)
.resizable(true)
.collapsible(false)
.show(ctx, |ui| {
let Some(surface) = self.processor.model.surfaces().next() else {
return;
};
let data_model = surface.data_model.borrow();
let components = surface.components.borrow();
let focused_id = self.focus.focused_id().map(str::to_string);
render_node(
&content_id,
&surface.id,
"",
ui,
&data_model,
&components,
&self.functions,
focused_id.as_deref(),
&self.open_modals,
&self.image_cache,
&self.local_tabs,
&mut self.edit_buffers,
pending,
);
});
if !open {
pending.push(PendingInteraction::ModalClose {
modal_id: modal_id.to_string(),
});
}
}
fn apply_pending(&mut self, pending: Vec<PendingInteraction>) {
for interaction in pending {
match interaction {
PendingInteraction::ButtonActivate { component_id } => {
self.handle_activate(&component_id);
}
PendingInteraction::DataUpdate { path, value } => {
if !path.is_empty()
&& let Some(surface) = self.processor.model.surfaces_mut().next()
{
surface.data_model.borrow_mut().set(&path, value);
}
}
PendingInteraction::TabActivate { component_id, index } => {
self.local_tabs.insert(component_id, index);
}
PendingInteraction::Toggle { path } => {
if let Some(surface) = self.processor.model.surfaces_mut().next() {
let cur = surface
.data_model
.borrow()
.get(&path)
.and_then(|v| v.as_bool())
.unwrap_or(false);
surface
.data_model
.borrow_mut()
.set(&path, serde_json::json!(!cur));
}
}
PendingInteraction::ModalTrigger { modal_id } => {
self.open_modals.insert(modal_id);
}
PendingInteraction::ModalClose { modal_id } => {
self.open_modals.remove(&modal_id);
}
}
}
}
fn load_images(&mut self, ctx: &egui::Context) {
let urls: Vec<String> = {
let Some(surface) = self.processor.model.surfaces().next() else {
return;
};
let components = surface.components.borrow();
let data_model = surface.data_model.borrow();
components
.all()
.iter()
.filter_map(|(id, model)| {
if model.component_type != "Image" {
return None;
}
let ctx = ComponentContext::new(
id.clone(),
surface.id.clone(),
&data_model,
&components,
&self.functions,
"",
None,
);
let url = model
.get_property::<DynamicString>("url")
.map(|ds| ctx.data_context.resolve_dynamic_string(&ds))
.unwrap_or_default();
if url.is_empty() || self.image_cache.contains_key(&url) {
return None;
}
Some(url)
})
.collect()
};
for url in urls {
let handle = crate::images::decode_url(&url).map(|image| {
ctx.load_texture(&url, image, egui::TextureOptions::LINEAR)
});
self.image_cache.insert(url, handle);
}
}
fn handle_activate(&mut self, node_id: &str) {
let result = {
let surface = match self.processor.model.surfaces().next() {
Some(s) => s,
None => return,
};
let comp_type = match surface.components.borrow().get(node_id) {
Some(m) => m.component_type.clone(),
None => return,
};
let data_model = surface.data_model.borrow();
let components = surface.components.borrow();
let ctx = ComponentContext::new(
node_id.to_string(),
surface.id.clone(),
&data_model,
&components,
&self.functions,
"",
Some(node_id.to_string()),
);
dispatch_event(
&comp_type,
&ctx,
&InputEvent::KeyPress { key: InputKey::Enter },
)
};
if let Some(result) = result {
let _ = apply_event_result(&mut self.processor, result);
}
self.apply_modal_interaction(node_id);
}
fn apply_modal_interaction(&mut self, node_id: &str) {
let modal_id = {
let Some(surface) = self.processor.model.surfaces().next() else {
return;
};
let components = surface.components.borrow();
let is_modal = components
.get(node_id)
.map(|m| m.component_type == "Modal")
.unwrap_or(false);
if is_modal {
if self.open_modals.insert(node_id.to_string()) {
return; }
Some(node_id.to_string()) } else {
components.all().iter().find_map(|(id, m)| {
(m.component_type == "Modal"
&& m.get_property::<String>("trigger").as_deref() == Some(node_id))
.then(|| id.clone())
})
}
};
match modal_id {
Some(id) if id == node_id => {
self.open_modals.remove(&id);
}
Some(id) => {
self.open_modals.insert(id);
}
None => {}
}
}
}
fn install_icon_font(ctx: &egui::Context) {
let bytes = include_bytes!("../assets/fonts/a2ui-icons.ttf").to_vec();
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"a2ui-icons".to_owned(),
std::sync::Arc::new(egui::FontData::from_owned(bytes)),
);
fonts
.families
.entry(egui::FontFamily::Name(std::sync::Arc::from("Icons")))
.or_default()
.push("a2ui-icons".to_owned());
for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
fonts.families.entry(family).or_default().push("a2ui-icons".to_owned());
}
ctx.set_fonts(fonts);
}