use oxitext_core::{FlowDirection, TextAlignment, TextDecoration};
#[derive(Debug, Clone)]
pub struct TruncationMode {
pub max_width: f32,
pub ellipsis_advance: f32,
pub ellipsis_glyph_id: u16,
}
#[derive(Debug, Clone)]
pub struct TabStops {
pub positions: Vec<f32>,
pub default_interval: f32,
}
impl TabStops {
pub fn with_interval(interval: f32) -> Self {
Self {
positions: Vec::new(),
default_interval: interval,
}
}
pub fn next_stop(&self, cursor_x: f32) -> f32 {
for &pos in &self.positions {
if pos > cursor_x + 0.5 {
return pos;
}
}
let next = ((cursor_x / self.default_interval).floor() + 1.0) * self.default_interval;
next.max(cursor_x + 1.0)
}
}
impl Default for TabStops {
fn default() -> Self {
Self {
positions: Vec::new(),
default_interval: 80.0,
}
}
}
#[derive(Debug, Clone)]
pub struct LayoutOptions {
pub alignment: TextAlignment,
pub flow_direction: FlowDirection,
pub truncation: Option<TruncationMode>,
pub tab_stops: TabStops,
pub paragraph_spacing: f32,
pub hanging_punctuation: bool,
pub decoration: Option<TextDecoration>,
pub inline_objects: Vec<oxitext_core::InlineObject>,
}
impl Default for LayoutOptions {
fn default() -> Self {
Self {
alignment: TextAlignment::Left,
flow_direction: FlowDirection::Horizontal,
truncation: None,
tab_stops: TabStops::default(),
paragraph_spacing: 0.0,
hanging_punctuation: false,
decoration: None,
inline_objects: Vec::new(),
}
}
}
impl LayoutOptions {
pub fn builder() -> LayoutOptionsBuilder {
LayoutOptionsBuilder(Self::default())
}
}
pub struct LayoutOptionsBuilder(LayoutOptions);
impl LayoutOptionsBuilder {
pub fn alignment(mut self, a: TextAlignment) -> Self {
self.0.alignment = a;
self
}
pub fn flow_direction(mut self, d: FlowDirection) -> Self {
self.0.flow_direction = d;
self
}
pub fn truncation(mut self, t: TruncationMode) -> Self {
self.0.truncation = Some(t);
self
}
pub fn tab_stops(mut self, ts: TabStops) -> Self {
self.0.tab_stops = ts;
self
}
pub fn paragraph_spacing(mut self, s: f32) -> Self {
self.0.paragraph_spacing = s;
self
}
pub fn hanging_punctuation(mut self, hp: bool) -> Self {
self.0.hanging_punctuation = hp;
self
}
pub fn decoration(mut self, d: TextDecoration) -> Self {
self.0.decoration = Some(d);
self
}
pub fn inline_objects(mut self, objects: Vec<oxitext_core::InlineObject>) -> Self {
self.0.inline_objects = objects;
self
}
pub fn build(self) -> LayoutOptions {
self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tab_stops_with_interval_default() {
let ts = TabStops::with_interval(80.0);
assert!(ts.positions.is_empty());
assert_eq!(ts.default_interval, 80.0);
}
#[test]
fn tab_stops_next_stop_interval() {
let ts = TabStops::with_interval(80.0);
let stop = ts.next_stop(10.0);
assert!((stop - 80.0).abs() < 1.0, "expected ~80.0, got {stop}");
let stop2 = ts.next_stop(80.0);
assert!((stop2 - 160.0).abs() < 1.0, "expected ~160.0, got {stop2}");
}
#[test]
fn tab_stops_explicit_positions() {
let ts = TabStops {
positions: vec![50.0, 120.0, 200.0],
default_interval: 80.0,
};
assert!((ts.next_stop(0.0) - 50.0).abs() < 1.0);
assert!((ts.next_stop(50.5) - 120.0).abs() < 1.0);
let stop = ts.next_stop(210.0);
assert!((stop - 240.0).abs() < 1.0, "expected ~240.0, got {stop}");
}
#[test]
fn tab_stops_default_impl() {
let ts = TabStops::default();
assert_eq!(ts.default_interval, 80.0);
}
#[test]
fn layout_options_default() {
let opts = LayoutOptions::default();
assert_eq!(opts.alignment, TextAlignment::Left);
assert_eq!(opts.flow_direction, FlowDirection::Horizontal);
assert!(opts.truncation.is_none());
assert_eq!(opts.paragraph_spacing, 0.0);
}
#[test]
fn layout_options_builder_sets_fields() {
let opts = LayoutOptions::builder()
.alignment(TextAlignment::Center)
.flow_direction(FlowDirection::Vertical)
.paragraph_spacing(12.0)
.build();
assert_eq!(opts.alignment, TextAlignment::Center);
assert_eq!(opts.flow_direction, FlowDirection::Vertical);
assert_eq!(opts.paragraph_spacing, 12.0);
}
#[test]
fn layout_options_builder_with_truncation() {
let trunc = TruncationMode {
max_width: 100.0,
ellipsis_advance: 10.0,
ellipsis_glyph_id: 42,
};
let opts = LayoutOptions::builder().truncation(trunc).build();
let t = opts.truncation.as_ref().expect("truncation should be set");
assert_eq!(t.max_width, 100.0);
assert_eq!(t.ellipsis_glyph_id, 42);
}
#[test]
fn layout_options_builder_with_tab_stops() {
let ts = TabStops::with_interval(40.0);
let opts = LayoutOptions::builder().tab_stops(ts).build();
assert_eq!(opts.tab_stops.default_interval, 40.0);
}
#[test]
fn layout_options_with_decoration() {
let opts = LayoutOptions::builder()
.decoration(TextDecoration::Underline {
color: oxitext_core::Rgba8 {
r: 0,
g: 0,
b: 0,
a: 255,
},
thickness: 1.0,
offset: 2.0,
})
.build();
assert!(opts.decoration.is_some());
match opts.decoration {
Some(TextDecoration::Underline {
thickness, offset, ..
}) => {
assert_eq!(thickness, 1.0);
assert_eq!(offset, 2.0);
}
_ => panic!("expected Underline decoration"),
}
}
#[test]
fn layout_options_decoration_none_by_default() {
let opts = LayoutOptions::default();
assert!(opts.decoration.is_none());
}
#[test]
fn test_layout_options_with_inline_objects() {
use oxitext_core::InlineObject;
let obj = InlineObject {
id: 1,
width: 20.0,
height: 20.0,
baseline_offset: 0.0,
advance: 20.0,
};
let opts = LayoutOptions::builder().inline_objects(vec![obj]).build();
assert_eq!(opts.inline_objects.len(), 1);
}
#[test]
fn test_styled_run_vertical_position_default() {
use oxitext_core::VerticalPosition;
let vp = VerticalPosition::Normal;
assert_eq!(vp.effective_size(16.0), 16.0);
}
}