vertra 0.3.0

A cross-platform graphics editor built with Rust and WebAssembly.
Documentation
//! Data types and event enums for the editor subsystem.

use serde::{Deserialize, Serialize};
use winit::keyboard::KeyCode;

use crate::geometry::Geometry;
use crate::objects::Object;
use crate::text_label::TextLabel;

/// A snapshot of a selected scene object exposed by the editor inspector.
///
/// Returned by [`crate::scene::Scene::inspector`] and passed to callbacks.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InspectorData {
    pub id:            usize,
    pub name:          String,
    pub str_id:        String,
    pub position:      [f32; 3],
    pub rotation_deg:  [f32; 3],
    pub scale:         [f32; 3],
    pub color:         [f32; 4],
    pub geometry_type: Option<String>,
    pub texture_path:  Option<String>,
}

impl InspectorData {
    /// Build an [`InspectorData`] snapshot from a live [`Object`].
    pub fn from_object(id: usize, obj: &Object) -> Self {
        Self {
            id,
            name:          obj.name.clone(),
            str_id:        obj.str_id.clone(),
            position:      obj.transform.position,
            rotation_deg:  obj.transform.rotation,
            scale:         obj.transform.scale,
            color:         obj.color,
            geometry_type: obj.geometry.as_ref().map(geometry_type_name),
            texture_path:  obj.texture_path.clone(),
        }
    }
}

/// A snapshot of a selected text label exposed by the editor.
///
/// Returned as part of [`EditorStateEvent::LabelSelectionChanged`].
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct LabelInspectorData {
    /// Unique label ID.
    pub id:        usize,
    /// The string being displayed.
    pub text:      String,
    /// Horizontal pixel position from the left edge.
    pub x:         f32,
    /// Vertical pixel position from the top edge.
    pub y:         f32,
    /// Font size in pixels.
    pub font_size: f32,
    /// RGBA colour `[r, g, b, a]` in `[0.0, 1.0]`.
    pub color:     [f32; 4],
    /// String ID of the font currently used.
    pub font_id:   String,
    /// Draw order (lower = further back).
    pub zindex:    i32,
}

impl LabelInspectorData {
    /// Build a snapshot from a live [`TextLabel`].
    pub fn from_label(label: &TextLabel) -> Self {
        Self {
            id:        label.id,
            text:      label.text.clone(),
            x:         label.x,
            y:         label.y,
            font_size: label.font_size,
            color:     label.color,
            font_id:   label.font_id.clone(),
            zindex:    label.zindex,
        }
    }
}

/// Which label-editor operation is currently active.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LabelDragKind {
    /// User is dragging the label body to reposition it.
    Move,
    /// User is dragging the bottom-right resize handle to change `font_size`.
    Resize,
}

/// State for an in-progress label drag.
#[derive(Debug, Clone)]
pub struct LabelDragState {
    /// ID of the label being edited.
    pub label_id:     usize,
    /// Which operation is active.
    pub kind:         LabelDragKind,
    /// Cursor X at the start of the drag.
    pub start_cursor: [f32; 2],
    /// Label `(x, y)` at the start of the drag.
    pub start_pos:    [f32; 2],
    /// Label `font_size` at the start of the drag.
    pub start_size:   f32,
}

fn geometry_type_name(g: &Geometry) -> String {
    match g {
        Geometry::Cube { .. }    => "Cube",
        Geometry::Box { .. }     => "Box",
        Geometry::Plane { .. }   => "Plane",
        Geometry::Pyramid { .. } => "Pyramid",
        Geometry::Capsule { .. } => "Capsule",
        Geometry::Sphere { .. }  => "Sphere",
    }.to_string()
}

/// Holds the currently-selected object (if any) in the editor inspector.
#[derive(Debug, Clone, Default)]
pub struct Inspector {
    pub selected: Option<InspectorData>,
}

impl Inspector {
    /// Clear the current selection.
    pub fn clear(&mut self) { self.selected = None; }
    /// Returns `true` when an object is currently selected.
    pub fn has_selection(&self) -> bool { self.selected.is_some() }
}

/// Which world axis a gizmo drag is constrained to.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DragAxis { X, Y, Z }

/// Which transformation mode the gizmo is currently in.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum GizmoMode {
    /// Move objects along an axis (arrow + cone tips). Default mode.
    #[default]
    Translate,
    /// Rotate objects around an axis (ring gizmo).
    Rotate,
    /// Scale objects along an axis (arrow + cube tips).
    Scale,
}

/// Which kind of drag transform is currently active.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DragKind { Translate, Rotate, Scale }

/// Live state of an ongoing gizmo axis drag.
#[derive(Debug, Clone)]
pub struct DragState {
    /// ID of the primary object being transformed.
    pub object_id: usize,
    /// Axis the drag is constrained to.
    pub axis:      DragAxis,
    /// World-space gizmo centre captured at drag-start (stable, no jitter).
    pub center:    [f32; 3],
    /// Which transform operation is being applied.
    pub kind:      DragKind,
}

/// Raw platform-agnostic input event fed into the editor subsystem.
///
/// These map 1:1 to low-level platform events (key presses, mouse moves,
/// scroll deltas, etc.).  In most use-cases you do not construct these
/// manually — [`crate::window::Window`] converts winit events and dispatches
/// them automatically while editor mode is active.
///
/// For *state-change* notifications (e.g. "the gizmo mode just changed")
/// see [`EditorStateEvent`] and
/// [`Window::on_editor_event`](crate::window::Window::on_editor_event).
#[derive(Debug, Clone)]
pub enum EditorEvent {
    /// Raw mouse-motion delta from the OS device stream.
    MouseMotionDelta { dx: f32, dy: f32 },
    /// Absolute cursor position in window-space pixels.
    CursorMoved { x: f32, y: f32 },
    /// Mouse-button state change. Each field is `Some(true)` on press,
    /// `Some(false)` on release, and `None` when that button was not involved.
    MouseButton { left: Option<bool>, middle: Option<bool>, right: Option<bool> },
    /// Scroll-wheel delta (positive = scroll up / zoom in).
    Scroll { delta: f32 },
    /// Modifier-key state snapshot (sent on every modifier change).
    ModifiersChanged { alt: bool, ctrl: bool },
    /// Synthetic "focus on selection" event, typically raised by the **F** key.
    FocusKey,
    /// A physical keyboard key was pressed.
    KeyPressed(KeyCode),
    /// A physical keyboard key was released.
    KeyReleased(KeyCode),
}

/// High-level editor state-change event, fired *after* the editor has
/// processed input and its internal state has actually changed.
///
/// Unlike [`EditorEvent`] (raw platform input), `EditorStateEvent` is
/// generated by comparing the editor state before and after each frame and
/// only emitted when something meaningful transitions.  Register a handler
/// with [`Window::on_editor_event`](crate::window::Window::on_editor_event).
///
/// The callback receives both the event **and** a cloned [`Object`] that is
/// contextually relevant (the selected or dragged object), or `None`:
///
/// * [`GizmoModeChanged`](Self::GizmoModeChanged) - the currently selected object.
/// * [`DragStart`](Self::DragStart) - the object being dragged.
/// * [`DragEnd`](Self::DragEnd) - the object that was just being dragged.
/// * [`SelectionChanged`](Self::SelectionChanged) - the newly selected object, or `None` if deselected.
/// * [`LabelDragStart`](Self::LabelDragStart) - fired once when a label drag begins.
/// * [`LabelDragEnd`](Self::LabelDragEnd) - fired once when a label drag ends.
///
/// # Example
///
/// ```rust,ignore
/// use vertra::editor::{EditorStateEvent, GizmoMode};
///
/// Window::new(state)
///     .on_editor_event(|_state, _scene, ev, obj| {
///         let name = obj.as_ref().map(|o| o.name.as_str()).unwrap_or("–");
///         match ev {
///             EditorStateEvent::GizmoModeChanged(mode) =>
///                 println!("[Editor] Gizmo → {:?}  (obj: {name})", mode),
///             EditorStateEvent::DragStart { axis } =>
///                 println!("[Editor] Drag on {:?}  (obj: {name})", axis),
///             EditorStateEvent::DragEnd =>
///                 println!("[Editor] Drag ended  (obj: {name})"),
///             EditorStateEvent::SelectionChanged =>
///                 println!("[Editor] Selection → {name}"),
///         }
///     })
///     .create();
/// ```
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum EditorStateEvent {
    /// The active gizmo mode changed (T / R / E keybinds).
    ///
    /// The callback's `obj` parameter is the currently selected object, if any.
    GizmoModeChanged(GizmoMode),

    /// The user started dragging a gizmo axis handle.
    ///
    /// The callback's `obj` parameter is the object being transformed.
    DragStart {
        /// The world axis being dragged.
        axis: DragAxis,
    },

    /// The user released a gizmo axis drag (mouse button up).
    ///
    /// The callback's `obj` parameter is the object that was being transformed.
    DragEnd,

    /// The editor selection changed, an object was clicked or deselected.
    ///
    /// The callback's `obj` parameter is the newly selected object, or `None`
    /// when the selection was cleared.
    SelectionChanged,

    /// The editor label selection changed, a text label was clicked or deselected.
    ///
    /// Contains a snapshot of the newly selected label, or `None` when the
    /// label selection was cleared.
    LabelSelectionChanged(Option<LabelInspectorData>),

    /// The user started dragging a label (move or resize).
    ///
    /// Fired once when the drag begins, analogous to [`DragStart`].
    LabelDragStart {
        /// Which label operation is starting.
        kind: LabelDragKind,
    },

    /// The user released a label drag (mouse button up).
    ///
    /// Contains a final snapshot of the label's state, analogous to [`DragEnd`].
    LabelDragEnd(LabelInspectorData),
}

/// Per-frame raw input state maintained by the editor.
///
/// Updated on every [`EditorEvent`] by [`crate::editor::EditorState::process`]
/// and consumed during drag, orbit, and pan logic.
#[derive(Debug, Default)]
pub struct EditorInput {
    /// Whether the left mouse button is currently held down.
    pub left_down:   bool,
    /// Whether the middle mouse button is currently held down.
    pub middle_down: bool,
    /// Whether the right mouse button is currently held down.
    pub right_down:  bool,
    /// Whether the Alt modifier key is currently held.
    pub alt_held:    bool,
    /// Whether the Ctrl modifier key is currently held.
    pub ctrl_held:   bool,
    /// Current cursor X position in window-space pixels.
    pub cursor_x:    f32,
    /// Current cursor Y position in window-space pixels.
    pub cursor_y:    f32,
}