Update UI widget designs to improve color consistency and add a narrow height mode

This commit is contained in:
Keavon Chambers
2025-09-08 21:48:22 -07:00
parent 485152bf8d
commit 22aa8c1264
41 changed files with 364 additions and 197 deletions
@@ -79,18 +79,17 @@ impl LayoutMessageHandler {
LayoutGroup::Section { layout, .. } => {
stack.extend(layout.iter().enumerate().map(|(index, val)| ([widget_path.as_slice(), &[index]].concat(), val)));
}
LayoutGroup::Table { rows } => {
for (row_index, cell) in rows.iter().enumerate() {
for (cell_index, entry) in cell.iter().enumerate() {
for (row_index, row) in rows.iter().enumerate() {
for (cell_index, cell) in row.iter().enumerate() {
// Return if this is the correct ID
if entry.widget_id == widget_id {
if cell.widget_id == widget_id {
widget_path.push(row_index);
widget_path.push(cell_index);
return Some((entry, widget_path));
return Some((cell, widget_path));
}
if let Widget::PopoverButton(popover) = &entry.widget {
if let Widget::PopoverButton(popover) = &cell.widget {
stack.extend(
popover
.popover_layout
@@ -118,6 +118,8 @@ pub struct TextButton {
pub disabled: bool,
pub narrow: bool,
pub tooltip: String,
#[serde(skip)]
@@ -179,6 +181,8 @@ pub struct ColorInput {
pub disabled: bool,
pub narrow: bool,
#[serde(rename = "menuDirection")]
pub menu_direction: Option<MenuDirection>,
@@ -87,6 +87,8 @@ pub struct DropdownInput {
pub disabled: bool,
pub narrow: bool,
pub tooltip: String,
#[serde(skip)]
@@ -195,6 +197,8 @@ pub struct NumberInput {
// Disabled
pub disabled: bool,
pub narrow: bool,
// Value
#[widget_builder(constructor)]
pub value: Option<f64>,
@@ -333,6 +337,8 @@ pub struct RadioInput {
pub disabled: bool,
pub narrow: bool,
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
#[serde(rename = "selectedIndex")]
pub selected_index: Option<u32>,
@@ -409,6 +415,8 @@ pub struct TextInput {
pub disabled: bool,
pub narrow: bool,
pub tooltip: String,
pub centered: bool,
@@ -40,6 +40,8 @@ pub enum SeparatorType {
pub struct TextLabel {
pub disabled: bool,
pub narrow: bool,
pub bold: bool,
pub italic: bool,
@@ -195,6 +195,7 @@ trait TableRowLayout {
fn element_widget(&self, index: usize) -> WidgetHolder {
TextButton::new(self.identifier())
.on_update(move |_| DataPanelMessage::PushToElementPath { index }.into())
.narrow(true)
.widget_holder()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
@@ -227,11 +228,13 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
.enumerate()
.map(|(index, row)| {
vec![
TextLabel::new(format!("{index}")).widget_holder(),
TextLabel::new(format!("{index}")).narrow(true).widget_holder(),
row.element.element_widget(index),
TextLabel::new(format_transform_matrix(row.transform)).widget_holder(),
TextLabel::new(format!("{}", row.alpha_blending)).widget_holder(),
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(),
TextLabel::new(format_transform_matrix(row.transform)).narrow(true).widget_holder(),
TextLabel::new(format!("{}", row.alpha_blending)).narrow(true).widget_holder(),
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0)))
.narrow(true)
.widget_holder(),
]
})
.collect::<Vec<_>>();
@@ -315,104 +318,130 @@ impl TableRowLayout for Vector {
match self.style.fill.clone() {
Fill::None => table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
ColorInput::new(FillChoice::None).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
TextLabel::new("Fill").narrow(true).widget_holder(),
ColorInput::new(FillChoice::None).disabled(true).menu_direction(Some(MenuDirection::Top)).narrow(true).widget_holder(),
]),
Fill::Solid(color) => table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
ColorInput::new(FillChoice::Solid(color)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
TextLabel::new("Fill").narrow(true).widget_holder(),
ColorInput::new(FillChoice::Solid(color))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_holder(),
]),
Fill::Gradient(gradient) => {
table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
TextLabel::new("Fill").narrow(true).widget_holder(),
ColorInput::new(FillChoice::Gradient(gradient.stops))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Type").widget_holder(),
TextLabel::new(gradient.gradient_type.to_string()).widget_holder(),
TextLabel::new("Fill Gradient Type").narrow(true).widget_holder(),
TextLabel::new(gradient.gradient_type.to_string()).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Start").widget_holder(),
TextLabel::new(format_dvec2(gradient.start)).widget_holder(),
TextLabel::new("Fill Gradient Start").narrow(true).widget_holder(),
TextLabel::new(format_dvec2(gradient.start)).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient End").narrow(true).widget_holder(),
TextLabel::new(format_dvec2(gradient.end)).narrow(true).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Fill Gradient End").widget_holder(), TextLabel::new(format_dvec2(gradient.end)).widget_holder()]);
}
}
if let Some(stroke) = self.style.stroke.clone() {
let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None };
table_rows.push(vec![
TextLabel::new("Stroke").widget_holder(),
ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
TextLabel::new("Stroke").narrow(true).widget_holder(),
ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).narrow(true).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Weight").widget_holder(), TextLabel::new(format!("{} px", stroke.weight)).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Lengths").widget_holder(),
TextLabel::new("Stroke Weight").narrow(true).widget_holder(),
TextLabel::new(format!("{} px", stroke.weight)).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Lengths").narrow(true).widget_holder(),
TextLabel::new(if stroke.dash_lengths.is_empty() {
"-".to_string()
} else {
format!("[{}]", stroke.dash_lengths.iter().map(|x| format!("{x} px")).collect::<Vec<_>>().join(", "))
})
.narrow(true)
.widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Offset").widget_holder(),
TextLabel::new(format!("{}", stroke.dash_offset)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Cap").widget_holder(), TextLabel::new(stroke.cap.to_string()).widget_holder()]);
table_rows.push(vec![TextLabel::new("Stroke Join").widget_holder(), TextLabel::new(stroke.join.to_string()).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Join Miter Limit").widget_holder(),
TextLabel::new(format!("{}", stroke.join_miter_limit)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Align").widget_holder(), TextLabel::new(stroke.align.to_string()).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Transform").widget_holder(),
TextLabel::new(format_transform_matrix(&stroke.transform)).widget_holder(),
TextLabel::new("Stroke Dash Offset").narrow(true).widget_holder(),
TextLabel::new(format!("{}", stroke.dash_offset)).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Non-Scaling").widget_holder(),
TextLabel::new((if stroke.non_scaling { "Yes" } else { "No" }).to_string()).widget_holder(),
TextLabel::new("Stroke Cap").narrow(true).widget_holder(),
TextLabel::new(stroke.cap.to_string()).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Paint Order").widget_holder(),
TextLabel::new(stroke.paint_order.to_string()).widget_holder(),
TextLabel::new("Stroke Join").narrow(true).widget_holder(),
TextLabel::new(stroke.join.to_string()).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Join Miter Limit").narrow(true).widget_holder(),
TextLabel::new(format!("{}", stroke.join_miter_limit)).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Align").narrow(true).widget_holder(),
TextLabel::new(stroke.align.to_string()).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Transform").narrow(true).widget_holder(),
TextLabel::new(format_transform_matrix(&stroke.transform)).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Non-Scaling").narrow(true).widget_holder(),
TextLabel::new((if stroke.non_scaling { "Yes" } else { "No" }).to_string()).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Paint Order").narrow(true).widget_holder(),
TextLabel::new(stroke.paint_order.to_string()).narrow(true).widget_holder(),
]);
}
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
let colinear = if colinear.is_empty() { "-".to_string() } else { colinear };
table_rows.push(vec![TextLabel::new("Colinear Handle IDs").widget_holder(), TextLabel::new(colinear).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Colinear Handle IDs").narrow(true).widget_holder(),
TextLabel::new(colinear).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Upstream Nested Layers").widget_holder(),
TextLabel::new("Upstream Nested Layers").narrow(true).widget_holder(),
TextLabel::new(if self.upstream_nested_layers.is_some() {
"Yes (this preserves references to its upstream nested layers for editing by tools)"
} else {
"No (this doesn't preserve references to its upstream nested layers for editing by tools)"
})
.narrow(true)
.widget_holder(),
]);
}
VectorTableTab::Points => {
table_rows.push(column_headings(&["", "position"]));
table_rows.extend(
self.point_domain
.iter()
.map(|(id, position)| vec![TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{position}")).widget_holder()]),
);
table_rows.extend(self.point_domain.iter().map(|(id, position)| {
vec![
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_holder(),
TextLabel::new(format!("{position}")).narrow(true).widget_holder(),
]
}));
}
VectorTableTab::Segments => {
table_rows.push(column_headings(&["", "start_index", "end_index", "handles"]));
table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(),
TextLabel::new(format!("{start}")).widget_holder(),
TextLabel::new(format!("{end}")).widget_holder(),
TextLabel::new(format!("{handles:?}")).widget_holder(),
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_holder(),
TextLabel::new(format!("{start}")).narrow(true).widget_holder(),
TextLabel::new(format!("{end}")).narrow(true).widget_holder(),
TextLabel::new(format!("{handles:?}")).narrow(true).widget_holder(),
]
}));
}
@@ -420,9 +449,9 @@ impl TableRowLayout for Vector {
table_rows.push(column_headings(&["", "segment_range", "fill"]));
table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| {
vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(),
TextLabel::new(format!("{segment_range:?}")).widget_holder(),
TextLabel::new(format!("{}", fill.inner())).widget_holder(),
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_holder(),
TextLabel::new(format!("{segment_range:?}")).narrow(true).widget_holder(),
TextLabel::new(format!("{}", fill.inner())).narrow(true).widget_holder(),
]
}));
}
@@ -477,7 +506,11 @@ impl TableRowLayout for Color {
format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb())
}
fn element_widget(&self, _index: usize) -> WidgetHolder {
ColorInput::new(FillChoice::Solid(*self)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder()
ColorInput::new(FillChoice::Solid(*self))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_holder()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![self.element_widget(0)];
@@ -494,8 +527,9 @@ impl TableRowLayout for GradientStops {
}
fn element_widget(&self, _index: usize) -> WidgetHolder {
ColorInput::new(FillChoice::Gradient(self.clone()))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.disabled(true)
.narrow(true)
.widget_holder()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
@@ -16,7 +16,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster::Image;
use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::ClickTarget;
use graphene_std::vector::style::ViewMode;
use graphene_std::vector::style::RenderMode;
#[impl_message(Message, PortfolioMessage, Document)]
#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize)]
@@ -176,8 +176,8 @@ pub enum DocumentMessage {
node_id: NodeId,
is_layer: bool,
},
SetViewMode {
view_mode: ViewMode,
SetRenderMode {
render_mode: RenderMode,
},
AddTransaction,
StartTransaction,
@@ -39,7 +39,7 @@ use graphene_std::table::Table;
use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
use graphene_std::vector::style::ViewMode;
use graphene_std::vector::style::RenderMode;
use kurbo::{Affine, CubicBez, Line, ParamCurve, PathSeg, QuadBez};
use std::path::PathBuf;
use std::time::Duration;
@@ -92,9 +92,10 @@ pub struct DocumentMessageHandler {
pub document_ptz: PTZ,
/// The current mode that the document is in, which starts out as Design Mode. This choice affects the editing behavior of the tools.
pub document_mode: DocumentMode,
/// The current view mode that the user has set for rendering the document within the viewport.
/// The current mode that the user has set for rendering the document within the viewport.
/// This is usually "Normal" but can be set to "Outline" or "Pixels" to see the canvas differently.
pub view_mode: ViewMode,
#[serde(alias = "view_mode")]
pub render_mode: RenderMode,
/// Sets whether or not all the viewport overlays should be drawn on top of the artwork.
/// This includes tool interaction visualizations (like the transform cage and path anchors/handles), the grid, and more.
pub overlays_visibility_settings: OverlaysVisibilitySettings,
@@ -163,7 +164,7 @@ impl Default for DocumentMessageHandler {
commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(),
document_ptz: PTZ::default(),
document_mode: DocumentMode::DesignMode,
view_mode: ViewMode::default(),
render_mode: RenderMode::default(),
overlays_visibility_settings: OverlaysVisibilitySettings::default(),
rulers_visible: true,
graph_view_overlay_open: false,
@@ -1266,8 +1267,8 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
responses.add(NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer });
responses.add(DocumentMessage::EndTransaction);
}
DocumentMessage::SetViewMode { view_mode } => {
self.view_mode = view_mode;
DocumentMessage::SetRenderMode { render_mode } => {
self.render_mode = render_mode;
responses.add_front(NodeGraphMessage::RunDocumentGraph);
}
DocumentMessage::AddTransaction => {
@@ -1821,9 +1822,9 @@ impl DocumentMessageHandler {
pub document_ptz: PTZ,
/// The current mode that the document is in, which starts out as Design Mode. This choice affects the editing behavior of the tools.
pub document_mode: DocumentMode,
/// The current view mode that the user has set for rendering the document within the viewport.
/// The current mode that the user has set for rendering the document within the viewport.
/// This is usually "Normal" but can be set to "Outline" or "Pixels" to see the canvas differently.
pub view_mode: ViewMode,
pub view_mode: RenderMode,
/// Sets whether or not all the viewport overlays should be drawn on top of the artwork.
/// This includes tool interaction visualizations (like the transform cage and path anchors/handles), the grid, and more.
pub overlays_visibility_settings: OverlaysVisibilitySettings,
@@ -1841,7 +1842,7 @@ impl DocumentMessageHandler {
commit_hash: old_message_handler.commit_hash,
document_ptz: old_message_handler.document_ptz,
document_mode: old_message_handler.document_mode,
view_mode: old_message_handler.view_mode,
render_mode: old_message_handler.view_mode,
overlays_visibility_settings: old_message_handler.overlays_visibility_settings,
rulers_visible: old_message_handler.rulers_visible,
graph_view_overlay_open: old_message_handler.graph_view_overlay_open,
@@ -2532,28 +2533,30 @@ impl DocumentMessageHandler {
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
RadioInput::new(vec![
RadioEntryData::new("normal")
.icon("ViewModeNormal")
.tooltip("View Mode: Normal")
.on_update(|_| DocumentMessage::SetViewMode { view_mode: ViewMode::Normal }.into()),
RadioEntryData::new("outline")
.icon("ViewModeOutline")
.tooltip("View Mode: Outline")
.on_update(|_| DocumentMessage::SetViewMode { view_mode: ViewMode::Outline }.into()),
RadioEntryData::new("pixels")
.icon("ViewModePixels")
.tooltip("View Mode: Pixels")
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(320) }.into()),
RadioEntryData::new("Normal")
.icon("RenderModeNormal")
.tooltip("Render Mode: Normal")
.on_update(|_| DocumentMessage::SetRenderMode { render_mode: RenderMode::Normal }.into()),
RadioEntryData::new("Outline")
.icon("RenderModeOutline")
.tooltip("Render Mode: Outline")
.on_update(|_| DocumentMessage::SetRenderMode { render_mode: RenderMode::Outline }.into()),
// RadioEntryData::new("PixelPreview")
// .icon("RenderModePixels")
// .tooltip("Render Mode: Pixel Preview")
// .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(320) }.into()),
// RadioEntryData::new("SvgPreview")
// .icon("RenderModeSvg")
// .tooltip("Render Mode: SVG Preview")
// .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1845) }.into()),
])
.selected_index(match self.view_mode {
ViewMode::Normal => Some(0),
_ => Some(1),
})
.selected_index(Some(self.render_mode as u32))
.narrow(true)
.widget_holder(),
// PopoverButton::new()
// .popover_layout(vec![
// LayoutGroup::Row {
// widgets: vec![TextLabel::new("View Mode").bold(true).widget_holder()],
// widgets: vec![TextLabel::new("Render Mode").bold(true).widget_holder()],
// },
// LayoutGroup::Row {
// widgets: vec![TextLabel::new("Coming soon").widget_holder()],
+2 -2
View File
@@ -149,7 +149,7 @@ impl NodeGraphExecutor {
export_format: graphene_std::application_io::ExportFormat::Canvas,
#[cfg(not(any(feature = "resvg", feature = "vello")))]
export_format: graphene_std::application_io::ExportFormat::Svg,
view_mode: document.view_mode,
render_mode: document.render_mode,
hide_artboards: false,
for_export: false,
};
@@ -198,7 +198,7 @@ impl NodeGraphExecutor {
},
time: Default::default(),
export_format: graphene_std::application_io::ExportFormat::Svg,
view_mode: document.view_mode,
render_mode: document.render_mode,
hide_artboards: export_config.transparent_background,
for_export: true,
};