Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions editor/src/messages/layout/utility_types/widgets/button_widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,24 @@ pub struct ColorInput {
pub on_commit: WidgetCallback<()>,
}

/// Shortens a breadcrumb label to fit the trail, replacing the tail with an ellipsis.
/// Quotes delimit a string value, so a quoted label budgets only the text between them.
pub fn truncate_breadcrumb_label(label: &str) -> String {
const MAX_CHARACTERS: usize = 40;

let quoted = label.len() >= 2 && label.starts_with('"') && label.ends_with('"');
let content = if quoted { &label[1..label.len() - 1] } else { label };

if content.chars().count() <= MAX_CHARACTERS {
Comment thread
Keavon marked this conversation as resolved.
return label.to_string();
}

let mut truncated: String = content.chars().take(MAX_CHARACTERS - 1).collect();
truncated.push('…');

if quoted { format!("\"{truncated}\"") } else { truncated }
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)]
#[derivative(Debug, PartialEq)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ pub struct TextLabel {
pub italic: bool,
pub monospace: bool,
pub multiline: bool,
pub enquote: bool,
pub selectable: bool,
#[serde(rename = "centerAlign")]
pub center_align: bool,
#[serde(rename = "tableAlign")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use graphene_std::color::SRGBA8;
use graphene_std::extract_xy::XY;
use graphene_std::gradient::Gradient;
use graphene_std::list::{Item, List, NodeIdPath};
use graphene_std::math::float_noise::round_away_float_noise;
use graphene_std::memo::IORecord;
use graphene_std::raster::{
CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice,
Expand Down Expand Up @@ -150,7 +151,8 @@ impl DataPanelMessageHandler {

// Element path breadcrumbs
if !layout_data.breadcrumbs.is_empty() {
let breadcrumb = BreadcrumbTrailButtons::new(layout_data.breadcrumbs)
let labels = layout_data.breadcrumbs.iter().map(|label| truncate_breadcrumb_label(label)).collect();
let breadcrumb = BreadcrumbTrailButtons::new(labels)
.on_update(|&len| DataPanelMessage::TruncateElementPath { len: len as usize }.into())
.widget_instance();
widgets.push(breadcrumb);
Expand Down Expand Up @@ -796,108 +798,53 @@ impl TableItemLayout for Gradient {
}
}

impl TableItemLayout for f64 {
fn type_name() -> &'static str {
"Number (f64)"
}
fn identifier(&self) -> String {
format!("{self}")
}
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![
NumberInput::new(Some(*self)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
])]
}
}

impl TableItemLayout for u8 {
fn type_name() -> &'static str {
"Byte"
}
fn identifier(&self) -> String {
format!("{self:02X}")
}
// Values fall back to the default drill-in button (labeled with the hex string via `identifier`); the value page shows the same hex value as a label.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![TextLabel::new(self.identifier()).widget_instance()])]
}
}

impl TableItemLayout for f32 {
fn type_name() -> &'static str {
"Number (f32)"
}
fn identifier(&self) -> String {
format!("{self}")
}
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
])]
}
}

impl TableItemLayout for u32 {
fn type_name() -> &'static str {
"Number (u32)"
}
fn identifier(&self) -> String {
format!("{self}")
}
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
])]
}
}

impl TableItemLayout for i32 {
fn type_name() -> &'static str {
"Number (i32)"
}
fn identifier(&self) -> String {
format!("{self}")
}
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
])]
macro_rules! impl_table_item_layout_for_number {
($($ty:ty => $type_name:literal),* $(,)?) => {
$(
impl TableItemLayout for $ty {
fn type_name() -> &'static str {
$type_name
}
fn identifier(&self) -> String {
format!("{self}")
}
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![TextLabel::new(self.identifier()).selectable(true).narrow(true).widget_instance()]
}
}
)*
}
}
impl_table_item_layout_for_number!(
f32 => "Number (f32)",
u32 => "Number (u32)",
i32 => "Number (i32)",
u64 => "Number (u64)",
i64 => "Number (i64)",
);

impl TableItemLayout for i64 {
// Denoised so 0.1 + 0.2 reads as 0.3 rather than 0.30000000000000004. We don't do this for f32 because it lacks precision to reliably distinguish between intentional digits and noise.
impl TableItemLayout for f64 {
fn type_name() -> &'static str {
"Number (i64)"
"Number"
}
fn identifier(&self) -> String {
format!("{self}")
format!("{}", round_away_float_noise(*self))
}
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
// TODO: Make this robust for large i64 values that don't fit in f64 (beyond roughly 2^53), as with u64.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
])]
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![TextLabel::new(self.identifier()).selectable(true).narrow(true).widget_instance()]
}
}

impl TableItemLayout for u64 {
impl TableItemLayout for u8 {
fn type_name() -> &'static str {
"Number (u64)"
"Byte"
}
fn identifier(&self) -> String {
format!("{self}")
format!("0x{self:02X} ({self})")
}
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
// TODO: Make this robust for large u64 values that don't fit in f64 (above roughly 2^53). Perhaps using a bigint kind of approach through the widget's data flow.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
])]
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![TextLabel::new(self.identifier()).selectable(true).narrow(true).widget_instance()]
}
}

Expand All @@ -921,20 +868,50 @@ impl TableItemLayout for String {
"String"
}
fn identifier(&self) -> String {
// Show the first line, and if there are more, indicate that with an ellipsis
let first_line = self.lines().next().unwrap_or("");
if self.lines().count() > 1 {
format!("\"{} …\"", first_line)
} else {
format!("\"{}\"", first_line)
}
format!("\"{}\"", string_preview(self))
}
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
data.breadcrumbs.push(character_count_label(self));
self.value_page(data)
}
// The preview truncates for length and line breaks, so a button beside it reaches the full text, labeled by length
fn value_widgets(&self, target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![
TextLabel::new(string_preview(self)).enquote(true).selectable(true).monospace(true).narrow(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
TextButton::new(character_count_label(self))
.on_update(move |_| DataPanelMessage::PushToElementPath { step: target.clone() }.into())
.narrow(true)
.widget_instance(),
]
}
// Values fall back to the default drill-in button (labeled with the truncated quoted preview via `identifier`); the value page shows the full multi-line text in a `TextAreaInput`.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![TextAreaInput::new(self.to_string()).monospace(true).disabled(true).widget_instance()])]
}
}

fn character_count_label(value: &str) -> String {
let character_count = value.chars().count();

format!("{character_count} Char{}", if character_count == 1 { "" } else { "s" })
}

/// Shortens a string to fit a table cell, cutting at the first line break or 40 characters with an ellipsis.
/// The 40 matches `truncate_breadcrumb_label`, so a preview reaching the trail isn't cut twice.
fn string_preview(value: &str) -> String {
const MAX_CHARACTERS: usize = 40;

let first_line = value.lines().next().unwrap_or_default();
let cut_by_line_break = value.contains('\n');
let cut_by_length = first_line.chars().count() > MAX_CHARACTERS;

if !cut_by_line_break && !cut_by_length {
return value.to_string();
}

first_line.chars().take(MAX_CHARACTERS - 1).chain(['…']).collect()
}

impl TableItemLayout for Option<f64> {
fn type_name() -> &'static str {
"Option<f64>"
Expand All @@ -943,7 +920,12 @@ impl TableItemLayout for Option<f64> {
"Option<f64>".to_string()
}
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![TextLabel::new(format!("{self:?}")).narrow(true).widget_instance()]
let text = match self {
Some(value) => format!("Some({})", round_away_float_noise(*value)),
None => "None".to_string(),
};

vec![TextLabel::new(text).selectable(true).narrow(true).widget_instance()]
}
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(self.value_widgets(PathStep::Element(0), _data))]
Expand Down Expand Up @@ -988,7 +970,7 @@ impl TableItemLayout for DAffine2 {
"Transform".to_string()
}
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![TextLabel::new(format_transform_matrix(*self)).narrow(true).widget_instance()]
transform_widgets(*self)
}
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(self.value_widgets(PathStep::Element(0), _data))]
Expand All @@ -1003,8 +985,7 @@ impl TableItemLayout for Affine2 {
"Transform".to_string()
}
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
let matrix = DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64));
vec![TextLabel::new(format_transform_matrix(matrix)).narrow(true).widget_instance()]
transform_widgets(DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64)))
}
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(self.value_widgets(PathStep::Element(0), _data))]
Expand Down Expand Up @@ -1385,7 +1366,8 @@ fn drilldown_attribute_layout(any: &dyn Any, data: &mut LayoutData) -> Option<Ve
None
}

fn format_transform_matrix(transform: DAffine2) -> String {
/// Decomposes a transform into location, rotation, and scale, each headed by the icon of its Layer menu action.
fn transform_widgets(transform: DAffine2) -> Vec<WidgetInstance> {
let (scale, angle, translation) = if transform.matrix2.determinant().abs() <= f64::EPSILON {
let [col_0, col_1] = transform.matrix2.to_cols_array_2d().map(|[x, y]| DVec2::new(x, y));

Expand All @@ -1403,15 +1385,24 @@ fn format_transform_matrix(transform: DAffine2) -> String {
} else {
transform.to_scale_angle_translation()
};
let rotation = format_rounded(angle.to_degrees(), 3);

format!(
"Location: ({} px, {} px) — Rotation: {rotation}° — Scale: ({}x, {}x)",
format_rounded(translation.x, 3),
format_rounded(translation.y, 3),
format_rounded(scale.x, 3),
format_rounded(scale.y, 3)
)

let location_text = format!("({} px, {} px)", format_rounded(translation.x, 3), format_rounded(translation.y, 3));
let rotation_text = format!("{}°", format_rounded(angle.to_degrees(), 3));
let scale_text = format!("({}x, {}x)", format_rounded(scale.x, 3), format_rounded(scale.y, 3));

vec![
IconLabel::new("TransformationGrab").tooltip_label("Location").widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
TextLabel::new(location_text).narrow(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
IconLabel::new("TransformationRotate").tooltip_label("Rotation").widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
TextLabel::new(rotation_text).narrow(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
IconLabel::new("TransformationScale").tooltip_label("Scale").widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
TextLabel::new(scale_text).narrow(true).widget_instance(),
]
}

fn format_dvec2(value: DVec2) -> String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2429,7 +2429,7 @@ impl NodeGraphMessageHandler {
if subgraph_path_names_length >= 2 {
widgets.extend([
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
BreadcrumbTrailButtons::new(subgraph_path_names)
BreadcrumbTrailButtons::new(subgraph_path_names.iter().map(|name| truncate_breadcrumb_label(name)).collect())
.on_update(move |index| {
DocumentMessage::ExitNestedNetwork {
steps_back: subgraph_path_names_length - (*index as usize) - 1,
Expand Down
Loading
Loading