146 lines
5.2 KiB
Rust
146 lines
5.2 KiB
Rust
//! Text layer for Lightningbeam
|
|
//!
|
|
//! A text layer holds a single editable text field inside a resizable box,
|
|
//! with editable size, color, and font. Text is rendered as vector glyphs
|
|
//! (via parley + Vello) so it composites through the same path as vector art.
|
|
//!
|
|
//! The text/style fields are grouped in [`TextContent`] so they can move to a
|
|
//! per-keyframe model later without touching call sites; v1 stores a single
|
|
//! static instance and reads it through [`TextLayer::content_at`].
|
|
|
|
use crate::layer::{Layer, LayerTrait, LayerType};
|
|
use kurbo::Point;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::hash::{Hash, Hasher};
|
|
use uuid::Uuid;
|
|
|
|
/// Horizontal alignment of text within the box.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum TextAlign {
|
|
Left,
|
|
Center,
|
|
Right,
|
|
Justify,
|
|
}
|
|
|
|
impl Default for TextAlign {
|
|
fn default() -> Self {
|
|
TextAlign::Left
|
|
}
|
|
}
|
|
|
|
/// The text content + styling for a text layer.
|
|
///
|
|
/// Grouped as its own struct so a future keyframed model can store
|
|
/// `Vec<TextKeyframe>` of these without changing the layer's public shape.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct TextContent {
|
|
/// The text string.
|
|
pub text: String,
|
|
/// Font size in pixels (document space).
|
|
pub font_size: f64,
|
|
/// Fill color, linear RGBA in 0..=1.
|
|
pub color: [f32; 4],
|
|
/// Logical font family name. Empty string = the bundled default font.
|
|
pub font_family: String,
|
|
/// Horizontal alignment within the box.
|
|
pub align: TextAlign,
|
|
}
|
|
|
|
impl Default for TextContent {
|
|
fn default() -> Self {
|
|
Self {
|
|
text: String::new(),
|
|
font_size: 48.0,
|
|
color: [1.0, 1.0, 1.0, 1.0],
|
|
font_family: String::new(),
|
|
align: TextAlign::Left,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TextContent {
|
|
/// A stable hash of everything that affects shaped layout (everything except
|
|
/// `color`, which only affects the brush, not glyph positions). Used to key
|
|
/// the renderer's external parley-layout cache, including the wrap width.
|
|
pub fn layout_hash(&self, box_width: f64) -> u64 {
|
|
let mut h = std::collections::hash_map::DefaultHasher::new();
|
|
self.text.hash(&mut h);
|
|
self.font_size.to_bits().hash(&mut h);
|
|
self.font_family.hash(&mut h);
|
|
self.align.hash(&mut h);
|
|
box_width.to_bits().hash(&mut h);
|
|
h.finish()
|
|
}
|
|
}
|
|
|
|
impl Hash for TextAlign {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
(*self as u8).hash(state);
|
|
}
|
|
}
|
|
|
|
/// A text layer: a single resizable text box.
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct TextLayer {
|
|
/// Base layer properties (id, name, opacity, visibility, animation data, …).
|
|
pub layer: Layer,
|
|
/// Top-left of the text box, in layer/clip-local space.
|
|
pub box_origin: Point,
|
|
/// Box width in document units (drives text wrapping).
|
|
pub box_width: f64,
|
|
/// Box height in document units.
|
|
pub box_height: f64,
|
|
/// The text content + styling (single static instance for v1).
|
|
pub content: TextContent,
|
|
}
|
|
|
|
impl TextLayer {
|
|
/// Default text-box dimensions for a freshly created layer.
|
|
pub const DEFAULT_WIDTH: f64 = 300.0;
|
|
pub const DEFAULT_HEIGHT: f64 = 100.0;
|
|
|
|
/// Create a new, empty text layer with the given name, positioned at `origin`.
|
|
pub fn new(name: impl Into<String>, origin: Point) -> Self {
|
|
Self {
|
|
layer: Layer::new(LayerType::Text, name),
|
|
box_origin: origin,
|
|
box_width: Self::DEFAULT_WIDTH,
|
|
box_height: Self::DEFAULT_HEIGHT,
|
|
content: TextContent::default(),
|
|
}
|
|
}
|
|
|
|
/// Read the active text content at `time`. v1 is static, so `time` is unused;
|
|
/// a future keyframed model will select the active keyframe here.
|
|
pub fn content_at(&self, _time: f64) -> &TextContent {
|
|
&self.content
|
|
}
|
|
|
|
/// Mutable access to the active text content at `time`.
|
|
pub fn content_at_mut(&mut self, _time: f64) -> &mut TextContent {
|
|
&mut self.content
|
|
}
|
|
}
|
|
|
|
// Delegate all LayerTrait methods to self.layer (mirrors RasterLayer).
|
|
impl LayerTrait for TextLayer {
|
|
fn id(&self) -> Uuid { self.layer.id }
|
|
fn name(&self) -> &str { &self.layer.name }
|
|
fn set_name(&mut self, name: String) { self.layer.name = name; }
|
|
fn has_custom_name(&self) -> bool { self.layer.has_custom_name }
|
|
fn set_has_custom_name(&mut self, custom: bool) { self.layer.has_custom_name = custom; }
|
|
fn visible(&self) -> bool { self.layer.visible }
|
|
fn set_visible(&mut self, visible: bool) { self.layer.visible = visible; }
|
|
fn opacity(&self) -> f64 { self.layer.opacity }
|
|
fn set_opacity(&mut self, opacity: f64) { self.layer.opacity = opacity; }
|
|
fn volume(&self) -> f64 { self.layer.volume }
|
|
fn set_volume(&mut self, volume: f64) { self.layer.volume = volume; }
|
|
fn muted(&self) -> bool { self.layer.muted }
|
|
fn set_muted(&mut self, muted: bool) { self.layer.muted = muted; }
|
|
fn soloed(&self) -> bool { self.layer.soloed }
|
|
fn set_soloed(&mut self, soloed: bool) { self.layer.soloed = soloed; }
|
|
fn locked(&self) -> bool { self.layer.locked }
|
|
fn set_locked(&mut self, locked: bool) { self.layer.locked = locked; }
|
|
}
|