dc_layout 0.39.1

Figma node layout library for Android
Documentation
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use taffy::{
    tree::{NodeId, TaffyTree},
    AlignContent, AlignItems, Dimension, Display, FlexDirection, FlexWrap, LengthPercentage,
    LengthPercentageAuto, Position, Style, TaffyError,
};

trait DumpAsCss {
    fn to_css_value(&self) -> String;
}
impl DumpAsCss for LengthPercentageAuto {
    fn to_css_value(&self) -> String {
        match self {
            LengthPercentageAuto::Auto => "auto".to_string(),
            LengthPercentageAuto::Length(v) => format!("{}px", v),
            LengthPercentageAuto::Percent(v) => format!("{}%", v),
        }
    }
}
impl DumpAsCss for LengthPercentage {
    fn to_css_value(&self) -> String {
        match self {
            LengthPercentage::Length(x) => format!("{}px", x),
            LengthPercentage::Percent(x) => format!("{}%", x),
        }
    }
}
impl DumpAsCss for Dimension {
    fn to_css_value(&self) -> String {
        match self {
            Dimension::Auto => "auto".to_string(),
            Dimension::Length(x) => format!("{}px", x),
            Dimension::Percent(x) => format!("{}%", x),
        }
    }
}
impl DumpAsCss for Option<AlignItems> {
    fn to_css_value(&self) -> String {
        match self {
            None => "normal",
            Some(AlignItems::Baseline) => "baseline",
            Some(AlignItems::Center) => "center",
            Some(AlignItems::End) => "end",
            Some(AlignItems::FlexEnd) => "flex-end",
            Some(AlignItems::FlexStart) => "flex-start",
            Some(AlignItems::Start) => "start",
            Some(AlignItems::Stretch) => "stretch",
        }
        .to_string()
    }
}
impl DumpAsCss for Option<AlignContent> {
    fn to_css_value(&self) -> String {
        match self {
            None => "normal",
            Some(AlignContent::Center) => "center",
            Some(AlignContent::End) => "end",
            Some(AlignContent::FlexEnd) => "flex-end",
            Some(AlignContent::FlexStart) => "flex-start",
            Some(AlignContent::SpaceAround) => "space-around",
            Some(AlignContent::SpaceBetween) => "space-between",
            Some(AlignContent::SpaceEvenly) => "space-evenly",
            Some(AlignContent::Start) => "start",
            Some(AlignContent::Stretch) => "stretch",
        }
        .to_string()
    }
}
impl DumpAsCss for FlexDirection {
    fn to_css_value(&self) -> String {
        match self {
            FlexDirection::Column => "column",
            FlexDirection::ColumnReverse => "column-reverse",
            FlexDirection::Row => "row",
            FlexDirection::RowReverse => "row-reverse",
        }
        .to_string()
    }
}
impl DumpAsCss for FlexWrap {
    fn to_css_value(&self) -> String {
        match self {
            FlexWrap::NoWrap => "nowrap",
            FlexWrap::Wrap => "wrap",
            FlexWrap::WrapReverse => "wrap-reverse",
        }
        .to_string()
    }
}
impl DumpAsCss for taffy::Display {
    fn to_css_value(&self) -> String {
        match self {
            Display::Flex => "flex",
            Display::None => "none",
            _ => "unknown",
        }
        .to_string()
    }
}
impl DumpAsCss for taffy::Overflow {
    fn to_css_value(&self) -> String {
        match self {
            taffy::Overflow::Hidden => "hidden",
            taffy::Overflow::Scroll => "scroll",
            taffy::Overflow::Clip => "clip",
            taffy::Overflow::Visible => "visible",
        }
        .to_string()
    }
}
impl DumpAsCss for Position {
    fn to_css_value(&self) -> String {
        match self {
            Position::Absolute => "absolute",
            Position::Relative => "relative",
        }
        .to_string()
    }
}
impl DumpAsCss for Style {
    fn to_css_value(&self) -> String {
        let mut css = Vec::new();

        css.push(format!("display: {}", self.display.to_css_value()));
        css.push(format!("overflow-x: {}", self.overflow.x.to_css_value()));
        css.push(format!("overflow-y: {}", self.overflow.y.to_css_value()));
        css.push(format!("position: {}", self.position.to_css_value()));
        css.push(format!("top: {}", self.inset.top.to_css_value()));
        css.push(format!("left: {}", self.inset.left.to_css_value()));
        css.push(format!("bottom: {}", self.inset.bottom.to_css_value()));
        css.push(format!("right: {}", self.inset.right.to_css_value()));
        css.push(format!("width: {}", self.size.width.to_css_value()));
        css.push(format!("height: {}", self.size.height.to_css_value()));
        css.push(format!("min-width: {}", self.min_size.width.to_css_value()));
        css.push(format!("min-height: {}", self.min_size.height.to_css_value()));
        css.push(format!("max-width: {}", self.max_size.width.to_css_value()));
        css.push(format!("max-height: {}", self.max_size.height.to_css_value()));
        // skip aspect-ratio
        css.push(format!("margin-top: {}", self.margin.top.to_css_value()));
        css.push(format!("margin-left: {}", self.margin.left.to_css_value()));
        css.push(format!("margin-bottom: {}", self.margin.bottom.to_css_value()));
        css.push(format!("margin-right: {}", self.margin.right.to_css_value()));
        css.push(format!("padding-top: {}", self.padding.top.to_css_value()));
        css.push(format!("padding-left: {}", self.padding.left.to_css_value()));
        css.push(format!("padding-bottom: {}", self.padding.bottom.to_css_value()));
        css.push(format!("padding-right: {}", self.padding.right.to_css_value()));
        // skip border (unused by DC)
        css.push(format!("align-items: {}", self.align_items.to_css_value()));
        css.push(format!("align-self: {}", self.align_self.to_css_value()));
        css.push(format!("align-content: {}", self.align_content.to_css_value()));
        // skip justify_items, justify_self (CSS grid, unused by DC)
        css.push(format!("justify-content: {}", self.justify_content.to_css_value()));
        css.push(format!(
            "gap: {} {}",
            self.gap.height.to_css_value(),
            self.gap.width.to_css_value()
        ));
        css.push(format!("flex-direction: {}", self.flex_direction.to_css_value()));
        css.push(format!("flex-wrap: {}", self.flex_wrap.to_css_value()));
        css.push(format!("flex-basis: {}", self.flex_basis.to_css_value()));
        css.push(format!("flex-grow: {}", self.flex_grow));
        css.push(format!("flex-shrink: {}", self.flex_shrink));

        css.join("; ")
    }
}

fn dump_node_as_html<T>(
    tree: &TaffyTree<T>,
    node_id: NodeId,
    depth: String,
) -> Result<String, TaffyError> {
    let css = tree.style(node_id)?.to_css_value();
    let mut output = Vec::new();
    output.push(format!("{}<div style=\"{}\">", depth, css));
    for child_id in tree.children(node_id)? {
        output.push(dump_node_as_html(tree, child_id, format!("{} ", depth))?);
    }
    output.push(format!("{}</div>", depth));
    Ok(output.join("\n"))
}

pub(crate) fn print_tree_as_html<T>(
    tree: &TaffyTree<T>,
    root_node_id: NodeId,
    print_func: fn(String) -> (),
) {
    // We use a simple prelude that outlines boxes in red.
    print_func("<html><style>div {{ box-sizing: border-box; border: 1px solid red; }}; body {{ margin: 20px; }}</style><body>".to_string());
    print_func(dump_node_as_html(tree, root_node_id, String::new()).unwrap());
    print_func("</body></html>".to_string());
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_dump_as_css() {
        assert_eq!(LengthPercentageAuto::Auto.to_css_value(), "auto");
        assert_eq!(LengthPercentageAuto::Length(10.0).to_css_value(), "10px");
        assert_eq!(LengthPercentageAuto::Percent(0.5).to_css_value(), "0.5%");
        assert_eq!(LengthPercentage::Length(10.0).to_css_value(), "10px");
        assert_eq!(LengthPercentage::Percent(0.5).to_css_value(), "0.5%");
        assert_eq!(Dimension::Auto.to_css_value(), "auto");
        assert_eq!(Dimension::Length(10.0).to_css_value(), "10px");
        assert_eq!(Dimension::Percent(0.5).to_css_value(), "0.5%");
        assert_eq!(Some(AlignItems::Center).to_css_value(), "center");
        assert_eq!(Some(AlignContent::Center).to_css_value(), "center");
        assert_eq!(FlexDirection::Column.to_css_value(), "column");
        assert_eq!(FlexWrap::Wrap.to_css_value(), "wrap");
        assert_eq!(Display::Flex.to_css_value(), "flex");
        assert_eq!(taffy::Overflow::Hidden.to_css_value(), "hidden");
        assert_eq!(Position::Absolute.to_css_value(), "absolute");
    }

    #[test]
    fn test_dump_node_as_html() {
        let mut taffy: TaffyTree<()> = TaffyTree::new();
        let style = Style { ..Default::default() };
        let node = taffy.new_leaf(style).unwrap();
        let html = dump_node_as_html(&taffy, node, "".to_string()).unwrap();
        assert!(html.starts_with("<div style="));
        assert!(html.ends_with("</div>"));
    }
}