826 lines
31 KiB
Rust
826 lines
31 KiB
Rust
/// Info Panel pane - displays and edits properties of selected objects
|
|
///
|
|
/// Shows context-sensitive property editors based on current selection:
|
|
/// - Tool options (when a tool is active)
|
|
/// - Transform properties (when shapes are selected)
|
|
/// - Shape properties (fill/stroke for selected shapes)
|
|
/// - Document settings (when nothing is selected)
|
|
|
|
use eframe::egui::{self, DragValue, Sense, Ui};
|
|
use lightningbeam_core::actions::{
|
|
InstancePropertyChange, SetDocumentPropertiesAction, SetInstancePropertiesAction,
|
|
SetShapePropertiesAction,
|
|
};
|
|
use lightningbeam_core::layer::AnyLayer;
|
|
use lightningbeam_core::shape::ShapeColor;
|
|
use lightningbeam_core::tool::{SimplifyMode, Tool};
|
|
use super::{NodePath, PaneRenderer, SharedPaneState};
|
|
use uuid::Uuid;
|
|
|
|
/// Info panel pane state
|
|
pub struct InfopanelPane {
|
|
/// Whether the tool options section is expanded
|
|
tool_section_open: bool,
|
|
/// Whether the transform section is expanded
|
|
transform_section_open: bool,
|
|
/// Whether the shape properties section is expanded
|
|
shape_section_open: bool,
|
|
}
|
|
|
|
impl InfopanelPane {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
tool_section_open: true,
|
|
transform_section_open: true,
|
|
shape_section_open: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Aggregated info about the current selection
|
|
struct SelectionInfo {
|
|
/// True if nothing is selected
|
|
is_empty: bool,
|
|
/// Number of selected shape instances
|
|
shape_count: usize,
|
|
/// Layer ID of selected shapes (assumes single layer selection for now)
|
|
layer_id: Option<Uuid>,
|
|
/// Selected shape instance IDs
|
|
instance_ids: Vec<Uuid>,
|
|
/// Shape IDs referenced by selected instances
|
|
shape_ids: Vec<Uuid>,
|
|
|
|
// Transform values (None = mixed values across selection)
|
|
x: Option<f64>,
|
|
y: Option<f64>,
|
|
rotation: Option<f64>,
|
|
scale_x: Option<f64>,
|
|
scale_y: Option<f64>,
|
|
skew_x: Option<f64>,
|
|
skew_y: Option<f64>,
|
|
opacity: Option<f64>,
|
|
|
|
// Shape property values (None = mixed)
|
|
fill_color: Option<Option<ShapeColor>>,
|
|
stroke_color: Option<Option<ShapeColor>>,
|
|
stroke_width: Option<f64>,
|
|
}
|
|
|
|
impl Default for SelectionInfo {
|
|
fn default() -> Self {
|
|
Self {
|
|
is_empty: true,
|
|
shape_count: 0,
|
|
layer_id: None,
|
|
instance_ids: Vec::new(),
|
|
shape_ids: Vec::new(),
|
|
x: None,
|
|
y: None,
|
|
rotation: None,
|
|
scale_x: None,
|
|
scale_y: None,
|
|
skew_x: None,
|
|
skew_y: None,
|
|
opacity: None,
|
|
fill_color: None,
|
|
stroke_color: None,
|
|
stroke_width: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl InfopanelPane {
|
|
/// Gather info about the current selection
|
|
fn gather_selection_info(&self, shared: &SharedPaneState) -> SelectionInfo {
|
|
let mut info = SelectionInfo::default();
|
|
|
|
let selected_instances = shared.selection.shape_instances();
|
|
info.shape_count = selected_instances.len();
|
|
info.is_empty = info.shape_count == 0;
|
|
|
|
if info.is_empty {
|
|
return info;
|
|
}
|
|
|
|
info.instance_ids = selected_instances.to_vec();
|
|
|
|
// Find the layer containing the selected instances
|
|
let document = shared.action_executor.document();
|
|
let active_layer_id = *shared.active_layer_id;
|
|
|
|
if let Some(layer_id) = active_layer_id {
|
|
info.layer_id = Some(layer_id);
|
|
|
|
if let Some(layer) = document.get_layer(&layer_id) {
|
|
if let AnyLayer::Vector(vector_layer) = layer {
|
|
// Gather values from all selected instances
|
|
let mut first = true;
|
|
|
|
for instance_id in &info.instance_ids {
|
|
if let Some(instance) = vector_layer.get_object(instance_id) {
|
|
info.shape_ids.push(instance.shape_id);
|
|
|
|
if first {
|
|
// First instance - set initial values
|
|
info.x = Some(instance.transform.x);
|
|
info.y = Some(instance.transform.y);
|
|
info.rotation = Some(instance.transform.rotation);
|
|
info.scale_x = Some(instance.transform.scale_x);
|
|
info.scale_y = Some(instance.transform.scale_y);
|
|
info.skew_x = Some(instance.transform.skew_x);
|
|
info.skew_y = Some(instance.transform.skew_y);
|
|
info.opacity = Some(instance.opacity);
|
|
|
|
// Get shape properties
|
|
if let Some(shape) = vector_layer.shapes.get(&instance.shape_id) {
|
|
info.fill_color = Some(shape.fill_color);
|
|
info.stroke_color = Some(shape.stroke_color);
|
|
info.stroke_width = shape
|
|
.stroke_style
|
|
.as_ref()
|
|
.map(|s| Some(s.width))
|
|
.unwrap_or(Some(1.0));
|
|
}
|
|
|
|
first = false;
|
|
} else {
|
|
// Check if values differ (set to None if mixed)
|
|
if info.x != Some(instance.transform.x) {
|
|
info.x = None;
|
|
}
|
|
if info.y != Some(instance.transform.y) {
|
|
info.y = None;
|
|
}
|
|
if info.rotation != Some(instance.transform.rotation) {
|
|
info.rotation = None;
|
|
}
|
|
if info.scale_x != Some(instance.transform.scale_x) {
|
|
info.scale_x = None;
|
|
}
|
|
if info.scale_y != Some(instance.transform.scale_y) {
|
|
info.scale_y = None;
|
|
}
|
|
if info.skew_x != Some(instance.transform.skew_x) {
|
|
info.skew_x = None;
|
|
}
|
|
if info.skew_y != Some(instance.transform.skew_y) {
|
|
info.skew_y = None;
|
|
}
|
|
if info.opacity != Some(instance.opacity) {
|
|
info.opacity = None;
|
|
}
|
|
|
|
// Check shape properties
|
|
if let Some(shape) = vector_layer.shapes.get(&instance.shape_id) {
|
|
// Compare fill colors - set to None if mixed
|
|
if let Some(current_fill) = &info.fill_color {
|
|
if *current_fill != shape.fill_color {
|
|
info.fill_color = None;
|
|
}
|
|
}
|
|
// Compare stroke colors - set to None if mixed
|
|
if let Some(current_stroke) = &info.stroke_color {
|
|
if *current_stroke != shape.stroke_color {
|
|
info.stroke_color = None;
|
|
}
|
|
}
|
|
let stroke_w = shape
|
|
.stroke_style
|
|
.as_ref()
|
|
.map(|s| s.width)
|
|
.unwrap_or(1.0);
|
|
if info.stroke_width != Some(stroke_w) {
|
|
info.stroke_width = None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
info
|
|
}
|
|
|
|
/// Render tool-specific options section
|
|
fn render_tool_section(&mut self, ui: &mut Ui, path: &NodePath, shared: &mut SharedPaneState) {
|
|
let tool = *shared.selected_tool;
|
|
|
|
// Only show tool options for tools that have options
|
|
let has_options = matches!(
|
|
tool,
|
|
Tool::Draw | Tool::Rectangle | Tool::Ellipse | Tool::PaintBucket | Tool::Polygon | Tool::Line
|
|
);
|
|
|
|
if !has_options {
|
|
return;
|
|
}
|
|
|
|
egui::CollapsingHeader::new("Tool Options")
|
|
.id_salt(("tool_options", path))
|
|
.default_open(self.tool_section_open)
|
|
.show(ui, |ui| {
|
|
self.tool_section_open = true;
|
|
ui.add_space(4.0);
|
|
|
|
match tool {
|
|
Tool::Draw => {
|
|
// Stroke width
|
|
ui.horizontal(|ui| {
|
|
ui.label("Stroke Width:");
|
|
ui.add(DragValue::new(shared.stroke_width).speed(0.1).range(0.1..=100.0));
|
|
});
|
|
|
|
// Simplify mode
|
|
ui.horizontal(|ui| {
|
|
ui.label("Simplify:");
|
|
egui::ComboBox::from_id_salt(("draw_simplify", path))
|
|
.selected_text(match shared.draw_simplify_mode {
|
|
SimplifyMode::Corners => "Corners",
|
|
SimplifyMode::Smooth => "Smooth",
|
|
SimplifyMode::Verbatim => "Verbatim",
|
|
})
|
|
.show_ui(ui, |ui| {
|
|
ui.selectable_value(
|
|
shared.draw_simplify_mode,
|
|
SimplifyMode::Corners,
|
|
"Corners",
|
|
);
|
|
ui.selectable_value(
|
|
shared.draw_simplify_mode,
|
|
SimplifyMode::Smooth,
|
|
"Smooth",
|
|
);
|
|
ui.selectable_value(
|
|
shared.draw_simplify_mode,
|
|
SimplifyMode::Verbatim,
|
|
"Verbatim",
|
|
);
|
|
});
|
|
});
|
|
|
|
// Fill shape toggle
|
|
ui.checkbox(shared.fill_enabled, "Fill Shape");
|
|
}
|
|
|
|
Tool::Rectangle | Tool::Ellipse => {
|
|
// Stroke width
|
|
ui.horizontal(|ui| {
|
|
ui.label("Stroke Width:");
|
|
ui.add(DragValue::new(shared.stroke_width).speed(0.1).range(0.1..=100.0));
|
|
});
|
|
|
|
// Fill shape toggle
|
|
ui.checkbox(shared.fill_enabled, "Fill Shape");
|
|
}
|
|
|
|
Tool::PaintBucket => {
|
|
// Gap tolerance
|
|
ui.horizontal(|ui| {
|
|
ui.label("Gap Tolerance:");
|
|
ui.add(
|
|
DragValue::new(shared.paint_bucket_gap_tolerance)
|
|
.speed(0.1)
|
|
.range(0.0..=50.0),
|
|
);
|
|
});
|
|
}
|
|
|
|
Tool::Polygon => {
|
|
// Number of sides
|
|
ui.horizontal(|ui| {
|
|
ui.label("Sides:");
|
|
let mut sides = *shared.polygon_sides as i32;
|
|
if ui.add(DragValue::new(&mut sides).range(3..=20)).changed() {
|
|
*shared.polygon_sides = sides.max(3) as u32;
|
|
}
|
|
});
|
|
|
|
// Stroke width
|
|
ui.horizontal(|ui| {
|
|
ui.label("Stroke Width:");
|
|
ui.add(DragValue::new(shared.stroke_width).speed(0.1).range(0.1..=100.0));
|
|
});
|
|
|
|
// Fill shape toggle
|
|
ui.checkbox(shared.fill_enabled, "Fill Shape");
|
|
}
|
|
|
|
Tool::Line => {
|
|
// Stroke width
|
|
ui.horizontal(|ui| {
|
|
ui.label("Stroke Width:");
|
|
ui.add(DragValue::new(shared.stroke_width).speed(0.1).range(0.1..=100.0));
|
|
});
|
|
}
|
|
|
|
_ => {}
|
|
}
|
|
|
|
ui.add_space(4.0);
|
|
});
|
|
}
|
|
|
|
/// Render transform properties section
|
|
fn render_transform_section(
|
|
&mut self,
|
|
ui: &mut Ui,
|
|
path: &NodePath,
|
|
shared: &mut SharedPaneState,
|
|
info: &SelectionInfo,
|
|
) {
|
|
egui::CollapsingHeader::new("Transform")
|
|
.id_salt(("transform", path))
|
|
.default_open(self.transform_section_open)
|
|
.show(ui, |ui| {
|
|
self.transform_section_open = true;
|
|
ui.add_space(4.0);
|
|
|
|
let layer_id = match info.layer_id {
|
|
Some(id) => id,
|
|
None => return,
|
|
};
|
|
|
|
// Position X
|
|
self.render_transform_field(
|
|
ui,
|
|
"X:",
|
|
info.x,
|
|
1.0,
|
|
f64::NEG_INFINITY..=f64::INFINITY,
|
|
|value| InstancePropertyChange::X(value),
|
|
layer_id,
|
|
&info.instance_ids,
|
|
shared,
|
|
);
|
|
|
|
// Position Y
|
|
self.render_transform_field(
|
|
ui,
|
|
"Y:",
|
|
info.y,
|
|
1.0,
|
|
f64::NEG_INFINITY..=f64::INFINITY,
|
|
|value| InstancePropertyChange::Y(value),
|
|
layer_id,
|
|
&info.instance_ids,
|
|
shared,
|
|
);
|
|
|
|
ui.add_space(4.0);
|
|
|
|
// Rotation
|
|
self.render_transform_field(
|
|
ui,
|
|
"Rotation:",
|
|
info.rotation,
|
|
1.0,
|
|
-360.0..=360.0,
|
|
|value| InstancePropertyChange::Rotation(value),
|
|
layer_id,
|
|
&info.instance_ids,
|
|
shared,
|
|
);
|
|
|
|
ui.add_space(4.0);
|
|
|
|
// Scale X
|
|
self.render_transform_field(
|
|
ui,
|
|
"Scale X:",
|
|
info.scale_x,
|
|
0.01,
|
|
0.01..=100.0,
|
|
|value| InstancePropertyChange::ScaleX(value),
|
|
layer_id,
|
|
&info.instance_ids,
|
|
shared,
|
|
);
|
|
|
|
// Scale Y
|
|
self.render_transform_field(
|
|
ui,
|
|
"Scale Y:",
|
|
info.scale_y,
|
|
0.01,
|
|
0.01..=100.0,
|
|
|value| InstancePropertyChange::ScaleY(value),
|
|
layer_id,
|
|
&info.instance_ids,
|
|
shared,
|
|
);
|
|
|
|
ui.add_space(4.0);
|
|
|
|
// Skew X
|
|
self.render_transform_field(
|
|
ui,
|
|
"Skew X:",
|
|
info.skew_x,
|
|
1.0,
|
|
-89.0..=89.0,
|
|
|value| InstancePropertyChange::SkewX(value),
|
|
layer_id,
|
|
&info.instance_ids,
|
|
shared,
|
|
);
|
|
|
|
// Skew Y
|
|
self.render_transform_field(
|
|
ui,
|
|
"Skew Y:",
|
|
info.skew_y,
|
|
1.0,
|
|
-89.0..=89.0,
|
|
|value| InstancePropertyChange::SkewY(value),
|
|
layer_id,
|
|
&info.instance_ids,
|
|
shared,
|
|
);
|
|
|
|
ui.add_space(4.0);
|
|
|
|
// Opacity
|
|
self.render_transform_field(
|
|
ui,
|
|
"Opacity:",
|
|
info.opacity,
|
|
0.01,
|
|
0.0..=1.0,
|
|
|value| InstancePropertyChange::Opacity(value),
|
|
layer_id,
|
|
&info.instance_ids,
|
|
shared,
|
|
);
|
|
|
|
ui.add_space(4.0);
|
|
});
|
|
}
|
|
|
|
/// Render a single transform property field with drag-to-adjust
|
|
fn render_transform_field<F>(
|
|
&self,
|
|
ui: &mut Ui,
|
|
label: &str,
|
|
value: Option<f64>,
|
|
speed: f64,
|
|
range: std::ops::RangeInclusive<f64>,
|
|
make_change: F,
|
|
layer_id: Uuid,
|
|
instance_ids: &[Uuid],
|
|
shared: &mut SharedPaneState,
|
|
) where
|
|
F: Fn(f64) -> InstancePropertyChange,
|
|
{
|
|
ui.horizontal(|ui| {
|
|
// Label with drag sense for drag-to-adjust
|
|
let label_response = ui.add(egui::Label::new(label).sense(Sense::drag()));
|
|
|
|
match value {
|
|
Some(mut v) => {
|
|
// Handle drag on label
|
|
if label_response.dragged() {
|
|
let delta = label_response.drag_delta().x as f64 * speed;
|
|
v = (v + delta).clamp(*range.start(), *range.end());
|
|
|
|
// Create action for each selected instance
|
|
for instance_id in instance_ids {
|
|
let action = SetInstancePropertiesAction::new(
|
|
layer_id,
|
|
*instance_id,
|
|
make_change(v),
|
|
);
|
|
shared.pending_actions.push(Box::new(action));
|
|
}
|
|
}
|
|
|
|
// DragValue widget
|
|
let response = ui.add(
|
|
DragValue::new(&mut v)
|
|
.speed(speed)
|
|
.range(range.clone()),
|
|
);
|
|
|
|
if response.changed() {
|
|
// Create action for each selected instance
|
|
for instance_id in instance_ids {
|
|
let action = SetInstancePropertiesAction::new(
|
|
layer_id,
|
|
*instance_id,
|
|
make_change(v),
|
|
);
|
|
shared.pending_actions.push(Box::new(action));
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
// Mixed values - show placeholder
|
|
ui.label("--");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Render shape properties section (fill/stroke)
|
|
fn render_shape_section(
|
|
&mut self,
|
|
ui: &mut Ui,
|
|
path: &NodePath,
|
|
shared: &mut SharedPaneState,
|
|
info: &SelectionInfo,
|
|
) {
|
|
egui::CollapsingHeader::new("Shape")
|
|
.id_salt(("shape", path))
|
|
.default_open(self.shape_section_open)
|
|
.show(ui, |ui| {
|
|
self.shape_section_open = true;
|
|
ui.add_space(4.0);
|
|
|
|
let layer_id = match info.layer_id {
|
|
Some(id) => id,
|
|
None => return,
|
|
};
|
|
|
|
// Fill color
|
|
ui.horizontal(|ui| {
|
|
ui.label("Fill:");
|
|
match info.fill_color {
|
|
Some(Some(color)) => {
|
|
let mut egui_color = egui::Color32::from_rgba_unmultiplied(
|
|
color.r, color.g, color.b, color.a,
|
|
);
|
|
|
|
if ui.color_edit_button_srgba(&mut egui_color).changed() {
|
|
let new_color = Some(ShapeColor::new(
|
|
egui_color.r(),
|
|
egui_color.g(),
|
|
egui_color.b(),
|
|
egui_color.a(),
|
|
));
|
|
|
|
// Create action for each selected shape
|
|
for shape_id in &info.shape_ids {
|
|
let action = SetShapePropertiesAction::set_fill_color(
|
|
layer_id,
|
|
*shape_id,
|
|
new_color,
|
|
);
|
|
shared.pending_actions.push(Box::new(action));
|
|
}
|
|
}
|
|
}
|
|
Some(None) => {
|
|
if ui.button("Add Fill").clicked() {
|
|
// Add default black fill
|
|
let default_fill = Some(ShapeColor::rgb(0, 0, 0));
|
|
for shape_id in &info.shape_ids {
|
|
let action = SetShapePropertiesAction::set_fill_color(
|
|
layer_id,
|
|
*shape_id,
|
|
default_fill,
|
|
);
|
|
shared.pending_actions.push(Box::new(action));
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
ui.label("--");
|
|
}
|
|
}
|
|
});
|
|
|
|
// Stroke color
|
|
ui.horizontal(|ui| {
|
|
ui.label("Stroke:");
|
|
match info.stroke_color {
|
|
Some(Some(color)) => {
|
|
let mut egui_color = egui::Color32::from_rgba_unmultiplied(
|
|
color.r, color.g, color.b, color.a,
|
|
);
|
|
|
|
if ui.color_edit_button_srgba(&mut egui_color).changed() {
|
|
let new_color = Some(ShapeColor::new(
|
|
egui_color.r(),
|
|
egui_color.g(),
|
|
egui_color.b(),
|
|
egui_color.a(),
|
|
));
|
|
|
|
// Create action for each selected shape
|
|
for shape_id in &info.shape_ids {
|
|
let action = SetShapePropertiesAction::set_stroke_color(
|
|
layer_id,
|
|
*shape_id,
|
|
new_color,
|
|
);
|
|
shared.pending_actions.push(Box::new(action));
|
|
}
|
|
}
|
|
}
|
|
Some(None) => {
|
|
if ui.button("Add Stroke").clicked() {
|
|
// Add default black stroke
|
|
let default_stroke = Some(ShapeColor::rgb(0, 0, 0));
|
|
for shape_id in &info.shape_ids {
|
|
let action = SetShapePropertiesAction::set_stroke_color(
|
|
layer_id,
|
|
*shape_id,
|
|
default_stroke,
|
|
);
|
|
shared.pending_actions.push(Box::new(action));
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
ui.label("--");
|
|
}
|
|
}
|
|
});
|
|
|
|
// Stroke width
|
|
ui.horizontal(|ui| {
|
|
ui.label("Stroke Width:");
|
|
match info.stroke_width {
|
|
Some(mut width) => {
|
|
let response = ui.add(
|
|
DragValue::new(&mut width)
|
|
.speed(0.1)
|
|
.range(0.1..=100.0),
|
|
);
|
|
|
|
if response.changed() {
|
|
for shape_id in &info.shape_ids {
|
|
let action = SetShapePropertiesAction::set_stroke_width(
|
|
layer_id,
|
|
*shape_id,
|
|
width,
|
|
);
|
|
shared.pending_actions.push(Box::new(action));
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
ui.label("--");
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.add_space(4.0);
|
|
});
|
|
}
|
|
|
|
/// Render document settings section (shown when nothing is selected)
|
|
fn render_document_section(&self, ui: &mut Ui, path: &NodePath, shared: &mut SharedPaneState) {
|
|
egui::CollapsingHeader::new("Document")
|
|
.id_salt(("document", path))
|
|
.default_open(true)
|
|
.show(ui, |ui| {
|
|
ui.add_space(4.0);
|
|
|
|
let document = shared.action_executor.document();
|
|
|
|
// Get current values for editing
|
|
let mut width = document.width;
|
|
let mut height = document.height;
|
|
let mut duration = document.duration;
|
|
let mut framerate = document.framerate;
|
|
let layer_count = document.root.children.len();
|
|
|
|
// Canvas width
|
|
ui.horizontal(|ui| {
|
|
ui.label("Width:");
|
|
if ui
|
|
.add(DragValue::new(&mut width).speed(1.0).range(1.0..=10000.0))
|
|
.changed()
|
|
{
|
|
let action = SetDocumentPropertiesAction::set_width(width);
|
|
shared.pending_actions.push(Box::new(action));
|
|
}
|
|
});
|
|
|
|
// Canvas height
|
|
ui.horizontal(|ui| {
|
|
ui.label("Height:");
|
|
if ui
|
|
.add(DragValue::new(&mut height).speed(1.0).range(1.0..=10000.0))
|
|
.changed()
|
|
{
|
|
let action = SetDocumentPropertiesAction::set_height(height);
|
|
shared.pending_actions.push(Box::new(action));
|
|
}
|
|
});
|
|
|
|
// Duration
|
|
ui.horizontal(|ui| {
|
|
ui.label("Duration:");
|
|
if ui
|
|
.add(
|
|
DragValue::new(&mut duration)
|
|
.speed(0.1)
|
|
.range(0.1..=3600.0)
|
|
.suffix("s"),
|
|
)
|
|
.changed()
|
|
{
|
|
let action = SetDocumentPropertiesAction::set_duration(duration);
|
|
shared.pending_actions.push(Box::new(action));
|
|
}
|
|
});
|
|
|
|
// Framerate
|
|
ui.horizontal(|ui| {
|
|
ui.label("Framerate:");
|
|
if ui
|
|
.add(
|
|
DragValue::new(&mut framerate)
|
|
.speed(1.0)
|
|
.range(1.0..=120.0)
|
|
.suffix(" fps"),
|
|
)
|
|
.changed()
|
|
{
|
|
let action = SetDocumentPropertiesAction::set_framerate(framerate);
|
|
shared.pending_actions.push(Box::new(action));
|
|
}
|
|
});
|
|
|
|
// Layer count (read-only)
|
|
ui.horizontal(|ui| {
|
|
ui.label("Layers:");
|
|
ui.label(format!("{}", layer_count));
|
|
});
|
|
|
|
ui.add_space(4.0);
|
|
});
|
|
}
|
|
}
|
|
|
|
impl PaneRenderer for InfopanelPane {
|
|
fn render_content(
|
|
&mut self,
|
|
ui: &mut egui::Ui,
|
|
rect: egui::Rect,
|
|
path: &NodePath,
|
|
shared: &mut SharedPaneState,
|
|
) {
|
|
// Background
|
|
ui.painter().rect_filled(
|
|
rect,
|
|
0.0,
|
|
egui::Color32::from_rgb(30, 35, 40),
|
|
);
|
|
|
|
// Create scrollable area for content
|
|
let content_rect = rect.shrink(8.0);
|
|
let mut content_ui = ui.new_child(
|
|
egui::UiBuilder::new()
|
|
.max_rect(content_rect)
|
|
.layout(egui::Layout::top_down(egui::Align::LEFT)),
|
|
);
|
|
|
|
egui::ScrollArea::vertical()
|
|
.id_salt(("infopanel_scroll", path))
|
|
.show(&mut content_ui, |ui| {
|
|
ui.set_min_width(content_rect.width() - 16.0);
|
|
|
|
// 1. Tool options section (always shown if tool has options)
|
|
self.render_tool_section(ui, path, shared);
|
|
|
|
// 2. Gather selection info
|
|
let info = self.gather_selection_info(shared);
|
|
|
|
// 3. Transform section (if shapes selected)
|
|
if info.shape_count > 0 {
|
|
self.render_transform_section(ui, path, shared, &info);
|
|
}
|
|
|
|
// 4. Shape properties section (if shapes selected)
|
|
if info.shape_count > 0 {
|
|
self.render_shape_section(ui, path, shared, &info);
|
|
}
|
|
|
|
// 5. Document settings (if nothing selected)
|
|
if info.is_empty {
|
|
self.render_document_section(ui, path, shared);
|
|
}
|
|
|
|
// Show selection count at bottom
|
|
if info.shape_count > 0 {
|
|
ui.add_space(8.0);
|
|
ui.separator();
|
|
ui.add_space(4.0);
|
|
ui.label(format!(
|
|
"{} object{} selected",
|
|
info.shape_count,
|
|
if info.shape_count == 1 { "" } else { "s" }
|
|
));
|
|
}
|
|
});
|
|
}
|
|
|
|
fn name(&self) -> &str {
|
|
"Info Panel"
|
|
}
|
|
}
|