master #1

Merged
jess merged 3 commits from master into main 2026-05-19 02:17:21 +00:00
3 changed files with 239 additions and 14 deletions
Showing only changes of commit d67dc1c5e8 - Show all commits

View File

@ -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<LayoutTarget, Layout>,
}
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<Item = (&LayoutTarget, &Layout)> {
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;
}
}

View File

@ -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<String>,
}
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<FrontendMessage>) {
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 {

View File

@ -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(),
}
}