From d67dc1c5e8420adb492d2b618215f99ee8bd5c5c Mon Sep 17 00:00:00 2001 From: jess Date: Sun, 17 May 2026 02:28:21 -0700 Subject: [PATCH] structure for ICED to fill GUI into. --- frontend/iced/src/layout.rs | 66 +++++++++++++++++++ frontend/iced/src/main.rs | 63 ++++++++++++++---- frontend/iced/src/widgets.rs | 124 +++++++++++++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 14 deletions(-) create mode 100644 frontend/iced/src/layout.rs create mode 100644 frontend/iced/src/widgets.rs diff --git a/frontend/iced/src/layout.rs b/frontend/iced/src/layout.rs new file mode 100644 index 00000000..ce579e6d --- /dev/null +++ b/frontend/iced/src/layout.rs @@ -0,0 +1,66 @@ +/// applies the editor's layout diffs. each diff is comprised of a widget_path and a new_value. empty path = swap the whole layout. +/// otherwise, descend one step each index. tables descend through [row, col] indexed cells. +use graphite_editor::messages::layout::utility_types::widget_prelude::*; +use std::collections::HashMap; + +#[derive(Default)] +pub struct LayoutStore { + layouts: HashMap, +} + +impl LayoutStore { + pub fn apply(&mut self, target: LayoutTarget, diff: WidgetDiff) { + let layout = self.layouts.entry(target).or_default(); + apply_diff_layout(layout, &diff.widget_path, diff.new_value); + } + + pub fn iter(&self) -> impl Iterator { + self.layouts.iter() + } +} + +fn apply_diff_layout(layout: &mut Layout, path: &[usize], new_value: DiffUpdate) { + let Some((first, rest)) = path.split_first() else { + if let DiffUpdate::Layout(new) = new_value { + *layout = new; + } + return; + }; + if let Some(group) = layout.0.get_mut(*first) { + apply_diff_group(group, rest, new_value); + } +} + +fn apply_diff_group(group: &mut LayoutGroup, path: &[usize], new_value: DiffUpdate) { + let Some((first, rest)) = path.split_first() else { + if let DiffUpdate::LayoutGroup(new) = new_value { + *group = new; + } + return; + }; + match group { + LayoutGroup::Column(WidgetColumn { widgets }) => apply_diff_instance(widgets.get_mut(*first), rest, new_value), + LayoutGroup::Row(WidgetRow { widgets }) => apply_diff_instance(widgets.get_mut(*first), rest, new_value), + LayoutGroup::Section(WidgetSection { layout, .. }) => { + if let Some(child) = layout.0.get_mut(*first) { + apply_diff_group(child, rest, new_value); + } + } + LayoutGroup::Table(WidgetTable { rows, .. }) => { + if let Some(table_row) = rows.get_mut(*first) + && let Some((col, rest_after_col)) = rest.split_first() + { + apply_diff_instance(table_row.get_mut(*col), rest_after_col, new_value); + } + } + } +} + +fn apply_diff_instance(slot: Option<&mut WidgetInstance>, rest: &[usize], new_value: DiffUpdate) { + if !rest.is_empty() { + return; + } + if let (Some(slot), DiffUpdate::Widget(new)) = (slot, new_value) { + *slot = new; + } +} diff --git a/frontend/iced/src/main.rs b/frontend/iced/src/main.rs index 243b3fd2..1a79b338 100644 --- a/frontend/iced/src/main.rs +++ b/frontend/iced/src/main.rs @@ -1,17 +1,24 @@ +mod layout; +mod widgets; + use graphite_editor::application::{Editor, Environment, Host, Platform}; use graphite_editor::messages::prelude::*; -use iced::widget::{column, container, text}; +use iced::widget::{column, container, scrollable, text}; use iced::{Element, Length, Task, Theme}; use rand::Rng; +use crate::layout::LayoutStore; + #[derive(Debug, Clone)] -enum Message { +pub enum Message { Init, + WidgetActivated, } struct App { editor: Editor, - last_frontend_count: usize, + store: LayoutStore, + frontend_log: Vec, } impl App { @@ -23,7 +30,14 @@ impl App { let seed = rand::rng().random(); let editor = Editor::new(environment, seed); - (Self { editor, last_frontend_count: 0 }, Task::done(Message::Init)) + ( + Self { + editor, + store: LayoutStore::default(), + frontend_log: Vec::new(), + }, + Task::done(Message::Init), + ) } fn title(&self) -> String { @@ -34,22 +48,43 @@ impl App { match message { Message::Init => { let responses = self.editor.handle_message(PortfolioMessage::Init); - self.last_frontend_count = responses.len(); - tracing::info!(count = responses.len(), "editor responded to PortfolioMessage::Init"); - for response in &responses { - tracing::info!(kind = %response.to_discriminant().local_name(), "frontend message"); - } + self.absorb(responses); } + Message::WidgetActivated => {} } Task::none() } + fn absorb(&mut self, responses: Vec) { + for message in responses { + let kind = message.to_discriminant().local_name(); + match message { + FrontendMessage::UpdateLayout { layout_target, diff } => { + self.frontend_log.push(format!("UpdateLayout → {layout_target:?} ({} diffs)", diff.len())); + for d in diff { + self.store.apply(layout_target, d); + } + } + _ => self.frontend_log.push(kind.to_string()), + } + } + } + fn view(&self) -> Element<'_, Message> { - container(column![text("Graphite").size(40), text(format!("editor produced {} frontend messages at boot", self.last_frontend_count)).size(14),].spacing(8)) - .padding(24) - .width(Length::Fill) - .height(Length::Fill) - .into() + let mut targets = column![].spacing(12); + let mut entries: Vec<_> = self.store.iter().collect(); + entries.sort_by_key(|(target, _)| **target as u8); + for (target, layout) in entries { + let header = text(format!("{target:?}")).size(14); + let body = widgets::render_layout(layout); + targets = targets.push(column![header, container(body).padding(6)].spacing(4)); + } + + let log_body = self.frontend_log.iter().fold(column![].spacing(2), |c, line| c.push(text(line.as_str()).size(11))); + + let log_pane = column![text("frontend messages").size(14), container(log_body).padding(6)].spacing(4); + + container(scrollable(column![targets, log_pane].spacing(16))).padding(16).width(Length::Fill).height(Length::Fill).into() } fn theme(&self) -> Theme { diff --git a/frontend/iced/src/widgets.rs b/frontend/iced/src/widgets.rs new file mode 100644 index 00000000..a5cce79f --- /dev/null +++ b/frontend/iced/src/widgets.rs @@ -0,0 +1,124 @@ +use graphite_editor::messages::layout::utility_types::widget_prelude::*; +use iced::widget::{Space, button, column, container, row, text}; +use iced::{Alignment, Element, Length}; + +use crate::Message; + +pub fn render_layout(layout: &Layout) -> Element<'_, Message> { + let mut col = column![].spacing(4); + for group in &layout.0 { + col = col.push(render_group(group)); + } + col.into() +} + +fn render_group(group: &LayoutGroup) -> Element<'_, Message> { + match group { + LayoutGroup::Column(WidgetColumn { widgets }) => { + let mut col = column![].spacing(4); + for instance in widgets { + col = col.push(widget_to_element(&instance.widget)); + } + col.into() + } + LayoutGroup::Row(WidgetRow { widgets }) => { + let mut r = row![].spacing(4).align_y(Alignment::Center); + for instance in widgets { + r = r.push(widget_to_element(&instance.widget)); + } + r.into() + } + LayoutGroup::Section(WidgetSection { name, layout, .. }) => { + let mut col = column![text(name.as_str()).size(12)].spacing(4); + for child in &layout.0 { + col = col.push(render_group(child)); + } + container(col).padding(6).into() + } + LayoutGroup::Table(WidgetTable { rows, .. }) => { + let mut col = column![].spacing(2); + for table_row in rows { + let mut r = row![].spacing(4); + for instance in table_row { + r = r.push(widget_to_element(&instance.widget)); + } + col = col.push(r); + } + col.into() + } + } +} + +/// maps every Widget variant to an ICED element. labels appended for all available items, not just implemented ones. +fn widget_to_element(widget: &Widget) -> Element<'_, Message> { + match widget { + Widget::TextLabel(w) => text(w.value.as_str()).size(13).into(), + Widget::IconLabel(w) => text(format!("[{}]", w.icon)).size(13).into(), + Widget::ShortcutLabel(_) => text("⌘").size(11).into(), + Widget::Separator(w) => match w.direction { + SeparatorDirection:i :Horizontal => Space::new().width(Length::Fixed(8.0)).into(), + SeparatorDirection::Vertical => Space::new().height(Length::Fixed(8.0)).into(), + }, + Widget::IconButton(w) => button(text(format!("[{}]", w.icon))).on_press(Message::WidgetActivated).into(), + Widget::TextButton(w) => button(text(w.label.as_str()).size(13)).on_press(Message::WidgetActivated).into(), + Widget::PopoverButton(w) => { + let label = w.icon.as_deref().unwrap_or("▾"); + button(text(format!("[{label}]"))).on_press(Message::WidgetActivated).into() + } + Widget::CheckboxInput(w) => { + let mark = if w.checked { "[x]" } else { "[ ]" }; + text(mark).size(13).into() + } + Widget::RadioInput(w) => { + let mut r = row![].spacing(4); + for entry in &w.entries { + let label = if !entry.label.is_empty() { + entry.label.as_str() + } else if let Some(icon) = entry.icon.as_deref() { + icon + } else { + "●" + }; + r = r.push(button(text(label).size(12)).on_press(Message::WidgetActivated)); + } + r.into() + } + Widget::DropdownInput(w) => { + let current = w + .selected_index + .and_then(|i| w.entries.iter().flatten().nth(i as usize)) + .map(|entry| entry.label.as_str()) + .unwrap_or("▾"); + button(text(format!("{current} ▾")).size(12)).on_press(Message::WidgetActivated).into() + } + Widget::NumberInput(w) => { + let body = match w.value { + Some(v) => format!("{v}"), + None => String::from("—"), + }; + let body = if !w.label.is_empty() { format!("{}: {body}", w.label) } else { body }; + text(body).size(12).into() + } + Widget::TextInput(w) => text(w.value.as_str()).size(12).into(), + Widget::TextAreaInput(w) => text(w.value.as_str()).size(12).into(), + Widget::ColorInput(_) => text("◼ color").size(12).into(), + Widget::ColorComparisonInput(_) => text("◼◼").size(12).into(), + Widget::ColorPresetsInput(_) => text("◼◼◼").size(12).into(), + Widget::SpectrumInput(_) => text("[spectrum]").size(11).into(), + Widget::VisualColorPickersInput(_) => text("[picker]").size(11).into(), + Widget::WorkingColorsInput(_) => text("◼/◻").size(12).into(), + Widget::ReferencePointInput(_) => text("·").size(13).into(), + Widget::BreadcrumbTrailButtons(w) => { + let mut r = row![].spacing(4); + for label in &w.labels { + r = r.push(text(label.as_str()).size(12)); + r = r.push(text("›").size(12)); + } + r.into() + } + Widget::ParameterExposeButton(_) => button(text("●").size(11)).on_press(Message::WidgetActivated).into(), + Widget::NodeCatalog(_) => text("[node catalog]").size(11).into(), + Widget::ImageButton(_) => button(text("[img]").size(11)).on_press(Message::WidgetActivated).into(), + Widget::ImageLabel(_) => text("[img]").size(11).into(), + } +}