//! CSS 2.1 ยง9.4.3's relative-positioning offset for block-level, non-replaced boxes in //! normal flow -- the smallest untouched geometry rule the engine had left before this //! seam existed (`handoff.md`: "the engine ignores `position: relative` today"). No new //! box kind, no change to `crates/layout/src/boxtree.rs`, isolated beside //! `crate::absolute`'s seam the same way. One plain-data function a generated candidate //! implements: //! //! - [`resolve`]-shaped ([`RelposResolver`]): given the box's own (already //! percentage-resolved) `top`/`right`/`bottom`/`left`, its containing block's //! padding-box size, and its `direction`, returns the horizontal and vertical shift CSS //! 2.1 9.4.3 gives the box. `crates/layout/src/fragment.rs` applies this shift on top of //! the box's ordinary in-flow placement -- never folded into that placement itself, //! since 9.4.3 is explicit that "this may cause it to overlap other boxes, but ... the //! offset does not affect the position of any other box": a relatively positioned box's //! siblings stay exactly where they would sit if it had never moved, and so do its own //! descendants relative to it -- only where the box (and everything inside it) ends up //! on the page changes. A relatively positioned box is still the containing block for //! its absolutely positioned descendants (CSS 2.1 10.1: `position` other than `static` //! qualifies), resolved by `fragment.rs` against this box's own un-shifted padding-box //! origin before this seam's shift is computed for the box itself (`layout_box`'s own //! body) -- the whole already-resolved subtree moves together with the box once the //! shift is applied to its fragment's offset one level further out. //! //! `crates/layout/src/fragment.rs` resolves every percentage in this seam's inputs against //! the containing block's own width/height before calling [`resolve`], the same pattern //! `crate::absolute`'s own module doc gives for the absolute-positioning seam: a candidate //! here is not asked to also resolve percentages, `crate::percentage`'s own job. //! //! Scope. In: `position: relative` on block-level, non-replaced boxes in normal flow; //! `top`/`bottom` and `left`/`right` in `px`, `%` (against the containing block's height //! and width respectively) and `auto`; the over-constrained rules -- horizontally, CSS 2.1 //! 9.4.3 branches on `direction` the same way the absolute-positioning seam's width //! equation does (`crate::absolute::AbsoluteInputs::direction`): `left` wins when both //! `left` and `right` are given in `ltr`, `right` wins in `rtl`; vertically there is no //! such branch -- `top` always wins over `bottom` when both are given, regardless of //! `direction`. Out: `fixed`, `sticky`, inline boxes, text, transforms, scrolling. //! //! [`resolve_baseline`] is the seam's deliberately wrong starting candidate: every offset //! is ignored outright and the box is left exactly where normal-flow placement already put //! it -- the engine's actual behavior before this seam existed (`handoff.md`'s own //! description). Unlike every other seam in this codebase except `crate::absolute`, no //! hand-written `correct` implementation is written before a model has had a chance at //! this one (`roadmap.md` -> "Proof of concept", claim 3; this seam's own spec, //! `specs/relative-positioning.md`, titled "build the seam model-first"). use dom::computed::LengthPercentageOrAuto; use crate::width::Direction; /// Plain-data inputs to one relatively positioned box's shift. Every /// `LengthPercentageOrAuto` field here is already resolved past `Percentage` by /// `fragment.rs` (this module's own doc comment) -- a correct [`resolve`] never needs to /// see the containing block's size to interpret one of these fields, only to know it when /// deciding what an over-constrained `Auto` side falls back to. #[derive(Debug, Clone, Copy, PartialEq)] pub struct RelposInputs { /// This box's own containing block's padding-box width (CSS 2.1 9.4.3's percentage /// basis for `left`/`right`, mirroring `crate::absolute::AbsoluteInputs`'s own field). pub containing_block_width: f64, pub containing_block_height: f64, pub top: LengthPercentageOrAuto, pub right: LengthPercentageOrAuto, pub bottom: LengthPercentageOrAuto, pub left: LengthPercentageOrAuto, /// The box's own `direction`, the property CSS 2.1 9.4.3's horizontal over-constraint /// rule branches on (mirroring `crate::width::WidthInputs::direction` and /// `crate::absolute::AbsoluteInputs::direction`): in `Ltr`, `right` is the offset /// discarded when both `left` and `right` are given; in `Rtl`, `left` is. pub direction: Direction, } /// The horizontal and vertical shift CSS 2.1 9.4.3 gives a relatively positioned box, /// applied by `fragment.rs` on top of the box's ordinary in-flow placement. #[derive(Debug, Clone, Copy, PartialEq)] pub struct RelposOutputs { pub shift_x: f64, pub shift_y: f64, } /// One candidate's implementation of the seam: plain data in, plain data out. Every /// candidate module the generation loop compiles exports a function of this type named /// `resolve`. pub type RelposResolver = fn(&RelposInputs) -> RelposOutputs; /// The seam's deliberately wrong starting candidate (this module's own doc comment): /// every offset (`top`/`right`/`bottom`/`left`) is ignored outright, leaving the box /// exactly where normal-flow placement already put it -- what `fragment.rs` did before /// this seam existed. pub fn resolve_baseline(_inputs: &RelposInputs) -> RelposOutputs { RelposOutputs { shift_x: 0.0, shift_y: 0.0 } } #[cfg(test)] mod tests { use super::*; fn inputs() -> RelposInputs { RelposInputs { containing_block_width: 800.0, containing_block_height: 600.0, top: LengthPercentageOrAuto::Length(50.0), right: LengthPercentageOrAuto::Auto, bottom: LengthPercentageOrAuto::Auto, left: LengthPercentageOrAuto::Length(30.0), direction: Direction::Ltr, } } #[test] fn baseline_ignores_every_offset_and_returns_zero_shift() { let out = resolve_baseline(&inputs()); assert_eq!(out.shift_x, 0.0, "not the explicit left:30px"); assert_eq!(out.shift_y, 0.0, "not the explicit top:50px"); } #[test] fn baseline_ignores_negative_and_percentage_offsets_too() { let mut i = inputs(); i.top = LengthPercentageOrAuto::Length(-20.0); i.left = LengthPercentageOrAuto::Percentage(0.5); let out = resolve_baseline(&i); assert_eq!(out.shift_x, 0.0); assert_eq!(out.shift_y, 0.0); } #[test] fn baseline_ignores_over_constrained_offsets_in_either_direction() { let mut i = inputs(); i.left = LengthPercentageOrAuto::Length(20.0); i.right = LengthPercentageOrAuto::Length(30.0); i.direction = Direction::Rtl; let out = resolve_baseline(&i); assert_eq!(out.shift_x, 0.0, "even rtl's winning side (right) is ignored"); } }