master #1
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,24 @@
|
||||||
|
mod layout;
|
||||||
|
mod widgets;
|
||||||
|
|
||||||
use graphite_editor::application::{Editor, Environment, Host, Platform};
|
use graphite_editor::application::{Editor, Environment, Host, Platform};
|
||||||
use graphite_editor::messages::prelude::*;
|
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 iced::{Element, Length, Task, Theme};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
|
use crate::layout::LayoutStore;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Message {
|
pub enum Message {
|
||||||
Init,
|
Init,
|
||||||
|
WidgetActivated,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
last_frontend_count: usize,
|
store: LayoutStore,
|
||||||
|
frontend_log: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
|
@ -23,7 +30,14 @@ impl App {
|
||||||
let seed = rand::rng().random();
|
let seed = rand::rng().random();
|
||||||
let editor = Editor::new(environment, seed);
|
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 {
|
fn title(&self) -> String {
|
||||||
|
|
@ -34,22 +48,43 @@ impl App {
|
||||||
match message {
|
match message {
|
||||||
Message::Init => {
|
Message::Init => {
|
||||||
let responses = self.editor.handle_message(PortfolioMessage::Init);
|
let responses = self.editor.handle_message(PortfolioMessage::Init);
|
||||||
self.last_frontend_count = responses.len();
|
self.absorb(responses);
|
||||||
tracing::info!(count = responses.len(), "editor responded to PortfolioMessage::Init");
|
|
||||||
for response in &responses {
|
|
||||||
tracing::info!(kind = %response.to_discriminant().local_name(), "frontend message");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Message::WidgetActivated => {}
|
||||||
}
|
}
|
||||||
Task::none()
|
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> {
|
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))
|
let mut targets = column![].spacing(12);
|
||||||
.padding(24)
|
let mut entries: Vec<_> = self.store.iter().collect();
|
||||||
.width(Length::Fill)
|
entries.sort_by_key(|(target, _)| **target as u8);
|
||||||
.height(Length::Fill)
|
for (target, layout) in entries {
|
||||||
.into()
|
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 {
|
fn theme(&self) -> Theme {
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue