use std::{fmt, sync::Arc};
use bitflags::bitflags;
use unicode_bidi::BidiDataSource as _;
use zng_app_context::context_local;
use zng_unit::{Factor, Px, PxRect, PxSize, about_eq, about_eq_hash, euclid};
use zng_var::context_var;
use atomic::{Atomic, Ordering::Relaxed};
use crate::unit::{LayoutAxis, Ppi, PxConstraints, PxConstraints2d};
pub struct LAYOUT;
impl LAYOUT {
pub fn pass_id(&self) -> LayoutPassId {
LAYOUT_PASS_CTX.get_clone()
}
pub fn with_root_context<R>(&self, pass_id: LayoutPassId, metrics: LayoutMetrics, f: impl FnOnce() -> R) -> R {
let mut pass = Some(Arc::new(pass_id));
LAYOUT_PASS_CTX.with_context(&mut pass, || self.with_context(metrics, f))
}
pub fn with_context<R>(&self, metrics: LayoutMetrics, f: impl FnOnce() -> R) -> R {
let mut ctx = Some(Arc::new(LayoutCtx { metrics }));
LAYOUT_CTX.with_context(&mut ctx, f)
}
pub fn with_no_context<R>(&self, f: impl FnOnce() -> R) -> R {
LAYOUT_CTX.with_default(f)
}
pub fn metrics(&self) -> LayoutMetrics {
LAYOUT_CTX.get().metrics.clone()
}
pub fn capture_metrics_use<R>(&self, f: impl FnOnce() -> R) -> (LayoutMask, R) {
METRICS_USED_CTX.with_context(&mut Some(Arc::new(Atomic::new(LayoutMask::empty()))), || {
let r = f();
let uses = METRICS_USED_CTX.get().load(Relaxed);
(uses, r)
})
}
pub fn register_metrics_use(&self, uses: LayoutMask) {
let ctx = METRICS_USED_CTX.get();
let m = ctx.load(Relaxed);
ctx.store(m | uses, Relaxed);
}
pub fn constraints(&self) -> PxConstraints2d {
LAYOUT_CTX.get().metrics.constraints()
}
pub fn z_constraints(&self) -> PxConstraints {
LAYOUT_CTX.get().metrics.z_constraints()
}
pub fn constraints_for(&self, axis: LayoutAxis) -> PxConstraints {
match axis {
LayoutAxis::X => self.constraints().x,
LayoutAxis::Y => self.constraints().y,
LayoutAxis::Z => self.z_constraints(),
}
}
pub fn with_constraints<R>(&self, constraints: PxConstraints2d, f: impl FnOnce() -> R) -> R {
self.with_context(self.metrics().with_constraints(constraints), f)
}
pub fn with_z_constraints<R>(&self, constraints: PxConstraints, f: impl FnOnce() -> R) -> R {
self.with_context(self.metrics().with_z_constraints(constraints), f)
}
pub fn with_constraints_for<R>(&self, axis: LayoutAxis, constraints: PxConstraints, f: impl FnOnce() -> R) -> R {
match axis {
LayoutAxis::X => {
let mut c = self.constraints();
c.x = constraints;
self.with_constraints(c, f)
}
LayoutAxis::Y => {
let mut c = self.constraints();
c.y = constraints;
self.with_constraints(c, f)
}
LayoutAxis::Z => self.with_z_constraints(constraints, f),
}
}
pub fn with_sub_size(&self, removed: PxSize, f: impl FnOnce() -> PxSize) -> PxSize {
self.with_constraints(self.constraints().with_less_size(removed), f) + removed
}
pub fn with_add_size(&self, added: PxSize, f: impl FnOnce() -> PxSize) -> PxSize {
self.with_constraints(self.constraints().with_more_size(added), f) - added
}
pub fn inline_constraints(&self) -> Option<InlineConstraints> {
LAYOUT_CTX.get().metrics.inline_constraints()
}
pub fn with_no_inline<R>(&self, f: impl FnOnce() -> R) -> R {
let metrics = self.metrics();
if metrics.inline_constraints().is_none() {
f()
} else {
self.with_context(metrics.with_inline_constraints(None), f)
}
}
pub fn root_font_size(&self) -> Px {
LAYOUT_CTX.get().metrics.root_font_size()
}
pub fn font_size(&self) -> Px {
LAYOUT_CTX.get().metrics.font_size()
}
pub fn with_font_size<R>(&self, font_size: Px, f: impl FnOnce() -> R) -> R {
self.with_context(self.metrics().with_font_size(font_size), f)
}
pub fn viewport(&self) -> PxSize {
LAYOUT_CTX.get().metrics.viewport()
}
pub fn viewport_min(&self) -> Px {
LAYOUT_CTX.get().metrics.viewport_min()
}
pub fn viewport_max(&self) -> Px {
LAYOUT_CTX.get().metrics.viewport_max()
}
pub fn viewport_for(&self, axis: LayoutAxis) -> Px {
let vp = self.viewport();
match axis {
LayoutAxis::X => vp.width,
LayoutAxis::Y => vp.height,
LayoutAxis::Z => Px::MAX,
}
}
pub fn with_viewport<R>(&self, viewport: PxSize, f: impl FnOnce() -> R) -> R {
self.with_context(self.metrics().with_viewport(viewport), f)
}
pub fn scale_factor(&self) -> Factor {
LAYOUT_CTX.get().metrics.scale_factor()
}
pub fn with_scale_factor<R>(&self, scale_factor: Factor, f: impl FnOnce() -> R) -> R {
self.with_context(self.metrics().with_scale_factor(scale_factor), f)
}
pub fn screen_ppi(&self) -> Ppi {
LAYOUT_CTX.get().metrics.screen_ppi()
}
pub fn with_screen_ppi<R>(&self, screen_ppi: Ppi, f: impl FnOnce() -> R) -> R {
self.with_context(self.metrics().with_screen_ppi(screen_ppi), f)
}
pub fn direction(&self) -> LayoutDirection {
LAYOUT_CTX.get().metrics.direction()
}
pub fn with_direction<R>(&self, direction: LayoutDirection, f: impl FnOnce() -> R) -> R {
self.with_context(self.metrics().with_direction(direction), f)
}
pub fn leftover(&self) -> euclid::Size2D<Option<Px>, ()> {
LAYOUT_CTX.get().metrics.leftover()
}
pub fn leftover_for(&self, axis: LayoutAxis) -> Option<Px> {
let l = self.leftover();
match axis {
LayoutAxis::X => l.width,
LayoutAxis::Y => l.height,
LayoutAxis::Z => None,
}
}
pub fn with_leftover<R>(&self, width: Option<Px>, height: Option<Px>, f: impl FnOnce() -> R) -> R {
self.with_context(self.metrics().with_leftover(width, height), f)
}
}
context_local! {
static LAYOUT_CTX: LayoutCtx = LayoutCtx::no_context();
static LAYOUT_PASS_CTX: LayoutPassId = LayoutPassId::new();
static METRICS_USED_CTX: Atomic<LayoutMask> = Atomic::new(LayoutMask::empty());
}
struct LayoutCtx {
metrics: LayoutMetrics,
}
impl LayoutCtx {
fn no_context() -> Self {
panic!("no layout context")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct LayoutPassId(u32);
impl LayoutPassId {
pub const fn new() -> Self {
LayoutPassId(0)
}
pub const fn next(self) -> LayoutPassId {
LayoutPassId(self.0.wrapping_add(1))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
pub struct InlineConstraintsMeasure {
pub first_max: Px,
pub mid_clear_min: Px,
}
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub struct InlineSegmentPos {
pub x: f32,
}
impl PartialEq for InlineSegmentPos {
fn eq(&self, other: &Self) -> bool {
about_eq(self.x, other.x, 0.001)
}
}
impl Eq for InlineSegmentPos {}
impl std::hash::Hash for InlineSegmentPos {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
about_eq_hash(self.x, 0.001, state);
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
pub struct InlineConstraintsLayout {
pub first: PxRect,
pub mid_clear: Px,
pub last: PxRect,
pub first_segs: Arc<Vec<InlineSegmentPos>>,
pub last_segs: Arc<Vec<InlineSegmentPos>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum InlineConstraints {
Measure(InlineConstraintsMeasure),
Layout(InlineConstraintsLayout),
}
impl InlineConstraints {
pub fn measure(self) -> InlineConstraintsMeasure {
match self {
InlineConstraints::Measure(m) => m,
InlineConstraints::Layout(l) => InlineConstraintsMeasure {
first_max: l.first.width(),
mid_clear_min: l.mid_clear,
},
}
}
pub fn layout(self) -> InlineConstraintsLayout {
match self {
InlineConstraints::Layout(m) => m,
InlineConstraints::Measure(_) => Default::default(),
}
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct LayoutMetricsSnapshot {
pub constraints: PxConstraints2d,
pub inline_constraints: Option<InlineConstraints>,
pub z_constraints: PxConstraints,
pub font_size: Px,
pub root_font_size: Px,
pub scale_factor: Factor,
pub viewport: PxSize,
pub screen_ppi: Ppi,
pub direction: LayoutDirection,
pub leftover: euclid::Size2D<Option<Px>, ()>,
}
impl LayoutMetricsSnapshot {
pub fn masked_eq(&self, other: &Self, mask: LayoutMask) -> bool {
(!mask.contains(LayoutMask::CONSTRAINTS)
|| (self.constraints == other.constraints
&& self.z_constraints == other.z_constraints
&& self.inline_constraints == other.inline_constraints))
&& (!mask.contains(LayoutMask::FONT_SIZE) || self.font_size == other.font_size)
&& (!mask.contains(LayoutMask::ROOT_FONT_SIZE) || self.root_font_size == other.root_font_size)
&& (!mask.contains(LayoutMask::SCALE_FACTOR) || self.scale_factor == other.scale_factor)
&& (!mask.contains(LayoutMask::VIEWPORT) || self.viewport == other.viewport)
&& (!mask.contains(LayoutMask::SCREEN_PPI) || self.screen_ppi == other.screen_ppi)
&& (!mask.contains(LayoutMask::DIRECTION) || self.direction == other.direction)
&& (!mask.contains(LayoutMask::LEFTOVER) || self.leftover == other.leftover)
}
}
impl PartialEq for LayoutMetricsSnapshot {
fn eq(&self, other: &Self) -> bool {
self.constraints == other.constraints
&& self.z_constraints == other.z_constraints
&& self.inline_constraints == other.inline_constraints
&& self.font_size == other.font_size
&& self.root_font_size == other.root_font_size
&& self.scale_factor == other.scale_factor
&& self.viewport == other.viewport
&& self.screen_ppi == other.screen_ppi
}
}
impl std::hash::Hash for LayoutMetricsSnapshot {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.constraints.hash(state);
self.inline_constraints.hash(state);
self.font_size.hash(state);
self.root_font_size.hash(state);
self.scale_factor.hash(state);
self.viewport.hash(state);
self.screen_ppi.hash(state);
}
}
#[derive(Debug, Clone)]
pub struct LayoutMetrics {
s: LayoutMetricsSnapshot,
}
impl LayoutMetrics {
pub fn new(scale_factor: Factor, viewport: PxSize, font_size: Px) -> Self {
LayoutMetrics {
s: LayoutMetricsSnapshot {
constraints: PxConstraints2d::new_fill_size(viewport),
z_constraints: PxConstraints::new_unbounded().with_min(Px(1)),
inline_constraints: None,
font_size,
root_font_size: font_size,
scale_factor,
viewport,
screen_ppi: Ppi::default(),
direction: LayoutDirection::default(),
leftover: euclid::size2(None, None),
},
}
}
pub fn constraints(&self) -> PxConstraints2d {
LAYOUT.register_metrics_use(LayoutMask::CONSTRAINTS);
self.s.constraints
}
pub fn z_constraints(&self) -> PxConstraints {
LAYOUT.register_metrics_use(LayoutMask::CONSTRAINTS);
self.s.z_constraints
}
pub fn inline_constraints(&self) -> Option<InlineConstraints> {
LAYOUT.register_metrics_use(LayoutMask::CONSTRAINTS);
self.s.inline_constraints.clone()
}
pub fn direction(&self) -> LayoutDirection {
LAYOUT.register_metrics_use(LayoutMask::DIRECTION);
self.s.direction
}
pub fn font_size(&self) -> Px {
LAYOUT.register_metrics_use(LayoutMask::FONT_SIZE);
self.s.font_size
}
pub fn root_font_size(&self) -> Px {
LAYOUT.register_metrics_use(LayoutMask::ROOT_FONT_SIZE);
self.s.root_font_size
}
pub fn scale_factor(&self) -> Factor {
LAYOUT.register_metrics_use(LayoutMask::SCALE_FACTOR);
self.s.scale_factor
}
pub fn viewport(&self) -> PxSize {
LAYOUT.register_metrics_use(LayoutMask::VIEWPORT);
self.s.viewport
}
pub fn viewport_min(&self) -> Px {
self.s.viewport.width.min(self.s.viewport.height)
}
pub fn viewport_max(&self) -> Px {
self.s.viewport.width.max(self.s.viewport.height)
}
pub fn screen_ppi(&self) -> Ppi {
self.s.screen_ppi
}
pub fn leftover(&self) -> euclid::Size2D<Option<Px>, ()> {
LAYOUT.register_metrics_use(LayoutMask::LEFTOVER);
self.s.leftover
}
pub fn with_constraints(mut self, constraints: PxConstraints2d) -> Self {
self.s.constraints = constraints;
self
}
pub fn with_z_constraints(mut self, constraints: PxConstraints) -> Self {
self.s.z_constraints = constraints;
self
}
pub fn with_inline_constraints(mut self, inline_constraints: Option<InlineConstraints>) -> Self {
self.s.inline_constraints = inline_constraints;
self
}
pub fn with_font_size(mut self, font_size: Px) -> Self {
self.s.font_size = font_size;
self
}
pub fn with_viewport(mut self, viewport: PxSize) -> Self {
self.s.viewport = viewport;
self
}
pub fn with_scale_factor(mut self, scale_factor: Factor) -> Self {
self.s.scale_factor = scale_factor;
self
}
pub fn with_screen_ppi(mut self, screen_ppi: Ppi) -> Self {
self.s.screen_ppi = screen_ppi;
self
}
pub fn with_direction(mut self, direction: LayoutDirection) -> Self {
self.s.direction = direction;
self
}
pub fn with_leftover(mut self, width: Option<Px>, height: Option<Px>) -> Self {
self.s.leftover = euclid::size2(width, height);
self
}
pub fn snapshot(&self) -> LayoutMetricsSnapshot {
self.s.clone()
}
}
context_var! {
pub static DIRECTION_VAR: LayoutDirection = LayoutDirection::LTR;
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum LayoutDirection {
LTR,
RTL,
}
impl LayoutDirection {
pub fn is_ltr(self) -> bool {
matches!(self, Self::LTR)
}
pub fn is_rtl(self) -> bool {
matches!(self, Self::RTL)
}
}
impl Default for LayoutDirection {
fn default() -> Self {
Self::LTR
}
}
impl fmt::Debug for LayoutDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "LayoutDirection::")?;
}
match self {
Self::LTR => write!(f, "LTR"),
Self::RTL => write!(f, "RTL"),
}
}
}
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub struct InlineSegment {
pub width: f32,
pub kind: TextSegmentKind,
}
impl PartialEq for InlineSegment {
fn eq(&self, other: &Self) -> bool {
about_eq(self.width, other.width, 0.001) && self.kind == other.kind
}
}
impl Eq for InlineSegment {}
impl std::hash::Hash for InlineSegment {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
about_eq_hash(self.width, 0.001, state);
self.kind.hash(state);
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub enum TextSegmentKind {
LeftToRight,
RightToLeft,
ArabicLetter,
EuropeanNumber,
EuropeanSeparator,
EuropeanTerminator,
ArabicNumber,
CommonSeparator,
NonSpacingMark,
BoundaryNeutral,
Emoji,
LineBreak,
Tab,
Space,
OtherNeutral,
Bracket(char),
BidiCtrl(char),
}
impl TextSegmentKind {
pub fn is_word(self) -> bool {
use TextSegmentKind::*;
matches!(
self,
LeftToRight
| RightToLeft
| ArabicLetter
| EuropeanNumber
| EuropeanSeparator
| EuropeanTerminator
| ArabicNumber
| CommonSeparator
| NonSpacingMark
| BoundaryNeutral
| OtherNeutral
| Bracket(_)
| Emoji
)
}
pub fn is_space(self) -> bool {
matches!(self, Self::Space | Self::Tab)
}
pub fn is_line_break(self) -> bool {
matches!(self, Self::LineBreak)
}
pub fn can_merge(self) -> bool {
use TextSegmentKind::*;
!matches!(self, Bracket(_) | BidiCtrl(_))
}
pub fn bracket_info(self) -> Option<unicode_bidi::data_source::BidiMatchedOpeningBracket> {
if let TextSegmentKind::Bracket(c) = self {
unicode_bidi::HardcodedBidiData.bidi_matched_opening_bracket(c)
} else {
None
}
}
pub fn strong_direction(self) -> Option<LayoutDirection> {
use TextSegmentKind::*;
match self {
LeftToRight => Some(LayoutDirection::LTR),
RightToLeft | ArabicLetter => Some(LayoutDirection::RTL),
BidiCtrl(_) => {
use unicode_bidi::BidiClass::*;
match unicode_bidi::BidiClass::from(self) {
LRE | LRO | LRI => Some(LayoutDirection::LTR),
RLE | RLO | RLI => Some(LayoutDirection::RTL),
_ => None,
}
}
_ => None,
}
}
}
impl From<char> for TextSegmentKind {
fn from(c: char) -> Self {
use unicode_bidi::*;
unicode_bidi::HardcodedBidiData.bidi_class(c).into()
}
}
impl From<unicode_bidi::BidiClass> for TextSegmentKind {
fn from(value: unicode_bidi::BidiClass) -> Self {
use TextSegmentKind::*;
use unicode_bidi::BidiClass::*;
match value {
WS => Space,
L => LeftToRight,
R => RightToLeft,
AL => ArabicLetter,
AN => ArabicNumber,
CS => CommonSeparator,
B => LineBreak,
EN => EuropeanNumber,
ES => EuropeanSeparator,
ET => EuropeanTerminator,
S => Tab,
ON => OtherNeutral,
BN => BoundaryNeutral,
NSM => NonSpacingMark,
RLE => BidiCtrl('\u{202B}'),
LRI => BidiCtrl('\u{2066}'),
RLI => BidiCtrl('\u{2067}'),
LRO => BidiCtrl('\u{202D}'),
FSI => BidiCtrl('\u{2068}'),
PDF => BidiCtrl('\u{202C}'),
LRE => BidiCtrl('\u{202A}'),
PDI => BidiCtrl('\u{2069}'),
RLO => BidiCtrl('\u{202E}'),
}
}
}
impl From<TextSegmentKind> for unicode_bidi::BidiClass {
fn from(value: TextSegmentKind) -> Self {
use TextSegmentKind::*;
use unicode_bidi::BidiClass::*;
match value {
Space => WS,
LeftToRight => L,
RightToLeft => R,
ArabicLetter => AL,
ArabicNumber => AN,
CommonSeparator => CS,
LineBreak => B,
EuropeanNumber => EN,
EuropeanSeparator => ES,
EuropeanTerminator => ET,
Tab => S,
OtherNeutral | Emoji | Bracket(_) => ON,
BoundaryNeutral => BN,
NonSpacingMark => NSM,
BidiCtrl(c) => match c {
'\u{202A}' => LRE,
'\u{202D}' => LRO,
'\u{202B}' => RLE,
'\u{202E}' => RLO,
'\u{202C}' => PDF,
'\u{2066}' => LRI,
'\u{2067}' => RLI,
'\u{2068}' => FSI,
'\u{2069}' => PDI,
_c => {
#[cfg(debug_assertions)]
{
tracing::error!("invalid bidi ctrl char '{_c}'");
}
ON
}
},
}
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, bytemuck::NoUninit)]
#[repr(transparent)]
pub struct LayoutMask: u32 {
const DEFAULT_VALUE = 1 << 31;
const CONSTRAINTS = 1 << 30;
const FONT_SIZE = 1;
const ROOT_FONT_SIZE = 1 << 1;
const SCALE_FACTOR = 1 << 2;
const VIEWPORT = 1 << 3;
const SCREEN_PPI = 1 << 4;
const DIRECTION = 1 << 5;
const LEFTOVER = 1 << 6;
}
}
impl Default for LayoutMask {
fn default() -> Self {
LayoutMask::empty()
}
}