apace-dom 0.3.0-beta.4

Apace DOM implementation
Documentation
use crate::{
    Attribute, BaseDocument, DocumentConfig, NodeId, TopLayerConfig, TopLayerError,
    UserAgentStylesheet, qual_name,
};
use apace_traits::shell::Viewport;

fn class(value: &str) -> Vec<Attribute> {
    vec![Attribute {
        name: qual_name!("class"),
        value: value.into(),
    }]
}

fn fixture() -> (BaseDocument, [NodeId; 5]) {
    let mut doc = BaseDocument::new(DocumentConfig {
        viewport: Some(Viewport::new(400, 300, 1., Default::default())),
        ..Default::default()
    });
    doc.add_user_agent_stylesheet(UserAgentStylesheet::parse(
        "html,body{margin:0;padding:0}.clip{position:relative;transform:translate(100px,80px);overflow:hidden;width:20px;height:20px;color:rgb(12,34,56);font:19px/24px monospace}.layer{position:fixed;left:40px;top:30px;width:120px;height:80px}.clip>.layer{background:rgb(255,0,0)}.layer:not(:popover-open){display:none}.second{left:70px;top:50px;background:blue!important}.peer{width:5px;height:40px}",
    ));
    let root = doc.root_node().id;
    let mut m = doc.mutate();
    let html = m.create_element(qual_name!("html", html), vec![]);
    let body = m.create_element(qual_name!("body", html), vec![]);
    let clip = m.create_element(qual_name!("div", html), class("clip"));
    let layer = m.create_element(qual_name!("div", html), class("layer"));
    let second = m.create_element(qual_name!("div", html), class("layer second"));
    let peer = m.create_element(qual_name!("div", html), class("peer"));
    m.append_children(root, &[html]);
    m.append_children(html, &[body]);
    m.append_children(body, &[clip, peer]);
    m.append_children(clip, &[layer]);
    m.append_children(layer, &[second]);
    drop(m);
    doc.resolve(0.);
    (doc, [body, clip, layer, second, peer])
}

#[test]
fn top_layer_escapes_layout_clip_and_transform_but_keeps_dom_and_stylo_ancestry() {
    let (mut doc, [body, clip, layer, _, peer]) = fixture();
    let peer_before = doc.get_client_bounding_rect(peer).unwrap();
    assert!(
        doc.set_top_layer(layer, Some(TopLayerConfig::default()))
            .unwrap()
    );
    doc.resolve(0.);
    let rect = doc.get_client_bounding_rect(layer).unwrap();
    assert_eq!(
        (rect.x, rect.y, rect.width, rect.height),
        (40., 30., 120., 80.)
    );
    assert_eq!(doc.get_node(layer).unwrap().parent, Some(clip));
    assert_eq!(doc.get_node(clip).unwrap().parent, Some(body));
    assert_eq!(doc.get_client_bounding_rect(peer).unwrap().y, peer_before.y);
    assert_eq!(doc.resolved_style_value(layer, "color"), "rgb(12, 34, 56)");
    assert_eq!(doc.resolved_style_value(layer, "font-size"), "19px");
    assert_eq!(
        doc.resolved_style_value(layer, "background-color"),
        "rgb(255, 0, 0)"
    );
    assert_eq!(doc.hit(60., 45.).unwrap().node_id, layer);
    assert!(
        !doc.get_node(clip)
            .unwrap()
            .layout_children
            .borrow()
            .as_ref()
            .unwrap()
            .contains(&layer)
    );
    doc.set_viewport_scroll(crate::Point { x: 17., y: 90. });
    assert_eq!(doc.get_client_bounding_rect(layer).unwrap().y, 30.);
    assert_eq!(doc.element_from_point(60., 45.), Some(layer));
    assert_eq!(doc.hit(77., 135.).unwrap().node_id, layer);
    assert_eq!(
        doc.get_node(layer).unwrap().absolute_position(0., 0.).y,
        120.
    );
    doc.set_top_layer(layer, None).unwrap();
    doc.resolve(0.);
    assert!(doc.top_layers().is_empty());
    assert_eq!(doc.get_client_bounding_rect(layer).unwrap().height, 0.);
}

#[test]
fn top_layer_order_update_reopen_modal_backdrop_and_nested_roots() {
    let (mut doc, [_, _, a, b, _]) = fixture();
    doc.set_top_layer(a, Some(TopLayerConfig::default()))
        .unwrap();
    doc.set_top_layer(b, Some(TopLayerConfig::default()))
        .unwrap();
    doc.resolve(0.);
    assert_eq!(doc.hit(80., 60.).unwrap().node_id, b);
    assert_eq!(doc.get_client_bounding_rect(b).unwrap().x, 70.);
    assert_eq!(doc.get_node(b).unwrap().parent, Some(a));
    doc.set_top_layer(
        a,
        Some(TopLayerConfig {
            modal: true,
            ..Default::default()
        }),
    )
    .unwrap();
    assert_eq!(doc.top_layers()[1].node_id, b, "update must not reorder");
    doc.resolve(0.);
    assert_eq!(doc.hit(80., 60.).unwrap().node_id, b);
    assert_eq!(
        doc.hit(350., 250.).unwrap().node_id,
        a,
        "modal backdrop blocks background"
    );
    doc.set_top_layer(a, None).unwrap();
    doc.set_top_layer(a, Some(TopLayerConfig::default()))
        .unwrap();
    doc.resolve(0.);
    assert_eq!(doc.hit(80., 60.).unwrap().node_id, a);
}

#[test]
fn top_layer_detach_and_drop_retire_entries_and_reused_slots_never_reopen() {
    let (mut doc, [body, clip, a, b, _]) = fixture();
    doc.set_top_layer(a, Some(TopLayerConfig::default()))
        .unwrap();
    doc.set_top_layer(b, Some(TopLayerConfig::default()))
        .unwrap();
    doc.resolve(0.);
    doc.mutate().remove_node(clip);
    assert!(doc.top_layers().is_empty());
    assert!(!doc.get_node(a).unwrap().flags.is_top_layer());
    assert_eq!(
        doc.set_top_layer(a, Some(TopLayerConfig::default())),
        Err(TopLayerError::InvalidTarget)
    );
    doc.mutate().append_children(body, &[clip]);
    doc.resolve(0.);
    assert!(
        doc.top_layers().is_empty(),
        "reattachment does not resurrect a lease"
    );
    doc.set_top_layer(a, Some(TopLayerConfig::default()))
        .unwrap();
    doc.mutate().remove_and_drop_node(clip);
    assert!(doc.top_layers().is_empty());
    let replacement = doc.mutate().create_element(qual_name!("div", html), vec![]);
    assert_ne!(replacement, a);
    assert_eq!(doc.set_top_layer(a, None), Err(TopLayerError::MissingNode));
}

#[test]
fn top_layer_admission_is_bounded_and_rejection_does_not_change_live_entries() {
    let (mut doc, [body, _, _, _, _]) = fixture();
    assert_eq!(
        doc.set_top_layer(doc.root_element().id, Some(TopLayerConfig::default())),
        Err(TopLayerError::InvalidTarget)
    );
    let mut nodes = Vec::new();
    {
        let mut m = doc.mutate();
        for _ in 0..=crate::MAX_TOP_LAYERS {
            let node = m.create_element(qual_name!("div", html), vec![]);
            m.append_children(body, &[node]);
            nodes.push(node);
        }
    }
    for &id in &nodes[..crate::MAX_TOP_LAYERS] {
        doc.set_top_layer(id, Some(TopLayerConfig::default()))
            .unwrap();
    }
    let last = nodes[crate::MAX_TOP_LAYERS];
    assert_eq!(
        doc.set_top_layer(last, Some(TopLayerConfig::default())),
        Err(TopLayerError::Capacity)
    );
    assert!(!doc.get_node(last).unwrap().flags.is_top_layer());
    assert_eq!(doc.top_layers().len(), crate::MAX_TOP_LAYERS);
    doc.set_top_layer(nodes[0], None).unwrap();
    doc.set_top_layer(last, Some(TopLayerConfig::default()))
        .unwrap();
    assert_eq!(doc.top_layers().last().unwrap().node_id, last);
}

#[test]
fn top_layer_hidden_ancestors_suppress_box_and_backdrop_until_style_recovers() {
    let (mut doc, [_, clip, a, _, _]) = fixture();
    doc.set_top_layer(
        a,
        Some(TopLayerConfig {
            modal: true,
            ..Default::default()
        }),
    )
    .unwrap();
    doc.resolve(0.);
    assert!(doc.top_layer_is_rendered(a));
    doc.mutate()
        .set_attribute(clip, qual_name!("style"), "display:none");
    doc.resolve(0.);
    assert!(!doc.top_layer_is_rendered(a));
    let hidden = doc.get_client_bounding_rect(a).unwrap();
    assert_eq!(
        (hidden.x, hidden.y, hidden.width, hidden.height),
        (0., 0., 0., 0.)
    );
    assert_ne!(doc.hit(350., 250.).map(|hit| hit.node_id), Some(a));
    assert_eq!(
        doc.top_layers().len(),
        1,
        "only the current lease survives hiding"
    );
    doc.mutate().clear_attribute(clip, qual_name!("style"));
    doc.resolve(0.);
    assert!(doc.top_layer_is_rendered(a));
    assert_eq!(doc.hit(350., 250.).unwrap().node_id, a);
    let before = doc.top_layers().to_vec();
    let mut color = crate::util::Color::from_rgb8(0, 0, 0);
    color.components[3] = f32::NAN;
    assert_eq!(
        doc.set_top_layer(
            a,
            Some(TopLayerConfig {
                modal: true,
                backdrop: Some(color),
                contain_scroll: true
            })
        ),
        Err(TopLayerError::InvalidBackdrop)
    );
    assert_eq!(doc.top_layers(), before);
}

#[test]
fn top_layer_scroll_containment_stops_wheel_editor_and_fling_chains_at_the_visual_root() {
    let (mut doc, [body, _, a, _, _]) = fixture();
    let mut m = doc.mutate();
    m.set_attribute(body, qual_name!("style"), "height:1600px");
    m.set_attribute(a, qual_name!("style"), "overflow:auto");
    let child = m.create_element(
        qual_name!("div", html),
        vec![Attribute {
            name: qual_name!("style"),
            value: "height:900px".into(),
        }],
    );
    m.append_children(a, &[child]);
    drop(m);
    doc.set_top_layer(
        a,
        Some(TopLayerConfig {
            contain_scroll: true,
            ..Default::default()
        }),
    )
    .unwrap();
    doc.resolve(0.);
    doc.set_viewport_scroll(crate::Point { x: 0., y: 240. });
    doc.scroll_node_by(child, 0., -2000., &mut |_| {});
    assert!(doc.get_node(a).unwrap().scroll_offset().y > 0.);
    assert_eq!(doc.viewport_scroll().y, 240.);
    doc.set_top_layer(a, Some(TopLayerConfig::default()))
        .unwrap();
    doc.resolve(0.);
    doc.scroll_node_by(child, 0., -2000., &mut |_| {});
    assert!(
        doc.viewport_scroll().y > 240.,
        "explicit Chain policy keeps ordinary DOM scroll propagation"
    );
}

#[test]
fn top_layer_auto_insets_follow_viewport_resize_and_retirement_reclaims_layout_roots() {
    let (mut doc, [body, _, a, _, peer]) = fixture();
    doc.mutate().set_attribute(
        a,
        qual_name!("style"),
        "left:auto;right:7px;top:auto;bottom:9px",
    );
    doc.set_top_layer(a, Some(TopLayerConfig::default()))
        .unwrap();
    doc.resolve(0.);
    let first = doc.get_client_bounding_rect(a).unwrap();
    assert_eq!((first.x, first.y), (273., 211.));
    doc.viewport_mut().window_size = (500, 400);
    doc.resolve(0.);
    let resized = doc.get_client_bounding_rect(a).unwrap();
    assert_eq!((resized.x, resized.y), (373., 311.));
    doc.mutate().append_children(body, &[a]);
    // A connected move retains the current lease and its viewport geometry,
    // while selectors and inheritance follow the new logical parent.
    assert_eq!(doc.top_layers()[0].node_id, a);
    doc.resolve(0.);
    assert_eq!(doc.get_node(a).unwrap().parent, Some(body));
    assert_eq!(doc.get_client_bounding_rect(a).unwrap().x, 373.);
    assert_eq!(doc.get_client_bounding_rect(peer).unwrap().y, 20.);
}

#[cfg(feature = "writing-mode")]
#[test]
fn vertical_top_layer_uses_its_own_axes_for_layout_cssom_and_viewport_scroll() {
    let (mut doc, [_, _, a, _, _]) = fixture();
    doc.mutate()
        .set_attribute(a, qual_name!("style"), "writing-mode:vertical-rl");
    doc.set_top_layer(a, Some(TopLayerConfig::default()))
        .unwrap();
    doc.resolve(0.);
    let rect = doc.get_client_bounding_rect(a).unwrap();
    assert_eq!(
        (rect.x, rect.y, rect.width, rect.height),
        (40., 30., 120., 80.)
    );
    doc.set_viewport_scroll(crate::Point { x: 13., y: 90. });
    assert_eq!(doc.get_client_bounding_rect(a).unwrap(), rect);
    assert_eq!(doc.element_from_point(50., 40.), Some(a));
}