7个不稳定版本

0.6.4 2023年11月8日
0.6.3 2023年11月8日
0.6.2 2023年10月22日
0.6.0 2023年9月22日
0.1.0 2021年7月17日

#60 in GUI

Download history 192/week @ 2024-04-14 125/week @ 2024-04-21 99/week @ 2024-04-28 149/week @ 2024-05-05 117/week @ 2024-05-12 139/week @ 2024-05-19 163/week @ 2024-05-26 129/week @ 2024-06-02 76/week @ 2024-06-09 141/week @ 2024-06-16 102/week @ 2024-06-23 81/week @ 2024-06-30 88/week @ 2024-07-07 107/week @ 2024-07-14 93/week @ 2024-07-21 110/week @ 2024-07-28

407次每月下载
12 个crate中使用 (4 直接)

MIT 协议

145KB
1K SLoC

形态

形态是一个crate,用于高效地确定组织成树结构的UI元素的大小和位置。

描述

形态是一个“单遍”算法,它递归地遍历布局树(深度优先),并根据其父节点和子节点确定节点的位置和大小。它可以生成与flexbox相似的布局,但需要学习的概念更少。

布局类型

布局类型属性决定了节点子节点的排列方式。有两种变体

  • LayoutType::Row - 节点将子节点排列成水平行。
  • LayoutType::Column - 节点将子节点排列成垂直列。

大小

节点的大小由其widthheight属性确定。这些属性使用Units指定,它有四种变体

  • Units::Pixels(val) - 设置大小为固定像素数。

  • Units::Percentage(val) - 设置大小为其父节点大小的百分比。

  • Units::Stretch(factor) - 设置大小为其父节点在同一轴上的可用空间的比例。

  • Units::Auto - 设置大小为紧密包裹节点子节点,或继承节点的内容大小

内容大小

内容大小用于确定没有子节点但可能因内容而具有内在大小的节点的大小。例如,包含文本的节点具有文本边界的内在大小,这可能导致宽度和高度之间的依赖关系(即文本换行时)。同样,可以通过将高度限制为宽度的某个比例来使用内容大小为具有特定宽高比的节点设置大小(反之亦然)。

空间

可以通过对每个四边应用间距来调整节点在堆栈中的位置。

  • left - 应应用于节点左侧的空间。这比 right 间距优先。
  • right - 应应用于节点右侧的空间。
  • top - 应应用于节点顶部(上方)的空间。这比 bottom 空间优先。
  • bottom - 应应用于节点底部(下方)的空间。

间距使用 Units 指定,有四种变体

  • Units::Pixels(val) - 将间距设置为固定像素数。

  • Units::Percentage(val) - 将间距设置为节点父大小的一定百分比。

  • Units::Stretch(factor) - 将间距设置为父节点在相同轴上的自由空间的比例。

位置类型

位置类型属性确定节点是否应与其兄弟节点在堆栈中内联定位,或与其兄弟节点无关地外联定位。有两种变体

  • PositionType::ParentDirected - 节点将相对于其与兄弟节点的内联位置进行定位。
  • PositionType::SelfDirected - 节点将外联定位,并相对于其父节点的左上角进行定位。

当父节点大小设置为自动时,自定位节点不会对父节点的大小做出贡献。

子节点空间

节点子节点空间通过覆盖节点子节点的单个自动间距来在其子节点周围应用空间,并也使用 Units 指定。

  • child_left - 应应用于视图左侧与其子节点之间的空间,具有单个 Auto 左间距。适用于垂直堆栈中的所有子节点以及水平堆栈中的第一个子节点。

  • child_right - 应应用于视图右侧与其子节点之间的空间,具有单个 Auto 右间距。适用于垂直堆栈中的所有子节点以及水平堆栈中的第一个子节点。

  • child_top - 应应用于视图顶部与其子节点之间的空间,具有单个 Auto 顶部间距。适用于水平堆栈中的所有子节点以及垂直堆栈中的第一个子节点。

  • child_bottom - 应应用于视图底部和具有单个 Auto 底部间距的子元素之间的空间。适用于水平堆叠中的所有子元素以及垂直堆叠中的第一个子元素。

还有两个额外的子元素间距属性用于设置子节点之间的空间

  • row-between - 应用于 Column 布局中的子元素之间的空间。如果子元素的顶部和底部间距设置为 Auto,则通过覆盖子元素的顶部和底部间距来工作。
  • col-between - 应用于 Row 布局中的子元素之间的空间。如果子元素的左部和右部间距设置为 Auto,则通过覆盖子元素的左部和右部间距来工作。

约束

所有间距和大小属性都可以使用相应的最小和最大属性进行约束,这些属性也使用 Units 进行指定。例如,节点的高度可以使用 min_heightmax_height 属性进行约束。

指定最小大小为 Auto 将使节点至少与其内容一样大。

如何使用

为了使内容尽可能通用,Morphorm 不提供任何容器来表示布局属性或树。相反,用户容器必须实现两个特质才能利用布局算法

  • Node 表示一个可以大小和定位的 UI 元素。节点本身可以包含所需的布局属性,或者属性可以由外部源(如 ECS 组件存储)提供,该源由相关的 Store 类型提供。节点还必须提供其子元素的迭代器,由相关的 ChildIter 类型指定,以及允许子元素外部存储,有一个相关的 Tree 类型。此外,还有一个相关的 SubLayout 类型,可以用于当无子节点的尺寸由其内容决定时提供外部上下文,例如,它可能用于提供计算和缓存节点内文本边界的上下文。
  • Cache 表示布局计算输出的存储库。存储库由节点类型的引用索引,但是,为了允许不能使用节点引用作为键的存储类型,Node 特质还提供了一个相关的 CacheKey 类型。

示例(ECS)

在下面的示例中,节点由一个 ID 类型表示,该类型用作存储布局节点属性的槽映射的键。

创建 ID 类型

首先我们定义一个 Entity 类型作为 ID

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Entity(pub usize);

然后我们创建一个简单的实体管理器,它生成新的实体。在这个简单的示例中,实体 ID 是从计数器生成的,但真实系统可能需要处理实体的删除。

pub struct EntityManager {
    count: usize,
}

impl EntityManager {
    pub fn create(&mut self) -> Entity {
        self.count += 1;
        Entity(self.count - 1)
    }
}

接下来我们将实现 Key 特质和 From<KeyData> 特质,以便实体可以用作 SecondaryMap 的键,来自 slotmap

unsafe impl Key for Entity {
    fn data(&self) -> slotmap::KeyData {
        KeyData::from_ffi(self.0 as u64)
    }

    fn null() -> Self {
        Entity::default()
    }

    fn is_null(&self) -> bool {
        self.0 == usize::MAX
    }
}

impl From<KeyData> for Entity {
    fn from(value: KeyData) -> Self {
        Entity(value.as_ffi() as usize)
    }
}

定义 Tree

使用 ID 类型表示布局树的一种方法是为每个实体存储父元素、第一个子元素和下一个/前一个兄弟元素的 ID

pub struct Tree {
    pub parent: Vec<Option<Entity>>,
    pub first_child: Vec<Option<Entity>>,
    pub next_sibling: Vec<Option<Entity>>,
    pub prev_sibling: Vec<Option<Entity>>,
}

请参阅 ecs/tree.rs 以获取完整实现。

可以构建一个遍历节点子节点的迭代器

/// An iterator for iterating the children of an entity.
pub struct ChildIterator<'a> {
    pub tree: &'a Tree,
    pub current_node: Option<&'a Entity>,
}

impl<'a> Iterator for ChildIterator<'a> {
    type Item = &'a Entity;
    fn next(&mut self) -> Option<Self::Item> {
        if let Some(entity) = self.current_node {
            self.current_node = self.tree.get_next_sibling(entity);
            return Some(entity);
        }

        None
    }
}

定义 Store

现在创建一个存储类型,正如其名,它将存储节点的属性

pub struct PropertyStore {
    pub visible: SecondaryMap<Entity, bool>,

    pub layout_type: SecondaryMap<Entity, LayoutType>,
    pub position_type: SecondaryMap<Entity, PositionType>,

    pub left: SecondaryMap<Entity, Units>,
    pub right: SecondaryMap<Entity, Units>,
    pub top: SecondaryMap<Entity, Units>,
    pub bottom: SecondaryMap<Entity, Units>,

    ...

}

完整实现请参见 ecs/store.rs

定义 Cache

接下来,我们需要一个缓存来存储布局计算的输出,也通过实体ID进行索引

pub struct NodeCache {
    // Computed size and position of nodes.
    pub rect: SecondaryMap<Entity, Rect>,
}

然后我们将在它上面实现 Cache 特性

impl Cache for NodeCache {
    type Node = Entity;

    fn set_bounds(&mut self, node: &Self::Node, posx: f32, posy: f32, width: f32, height: f32) {
        if let Some(rect) = self.rect.get_mut(*node) {
            rect.posx = posx;
            rect.posy = posy;
            rect.width = width;
            rect.height = height;
        }
    }

    fn width(&self, node: &Self::Node) -> f32 {
        if let Some(rect) = self.rect.get(*node) {
            return rect.width;
        }

        0.0
    }

    ...
}

实现 Node 特性

现在我们可以为 Entity 类型实现 Node 特性,并填写关联的类型

impl Node for Entity {
    type Store = Store;
    type Tree = Tree;
    type ChildIter<'t> = ChildIterator<'t>;
    type CacheKey = Entity;
    type SubLayout<'a> = ();

    fn key(&self) -> Self::CacheKey {
        *self
    }

    fn children<'t>(&self, tree: &'t Tree) -> Self::ChildIter<'t> {
        let current_node = tree.get_first_child(self);
        ChildIterator { tree, current_node }
    }

    fn visible(&self, store: &Store) -> bool {
        store.visible.get(*self).copied().unwrap_or(true)
    }

    fn layout_type(&self, store: &Store) -> Option<LayoutType> {
        store.layout_type.get(*self).copied()
    }
}

因为节点只是一个ID,我们可以用它来作为 CacheKey。在这个例子中,我们将 Sublayout 类型留空,但实际系统中可能会使用这个来存储文本上下文。每个 'getter' 函数,例如 layout_type(),都会从 Store 中检索返回值。

执行布局

最后,可以通过根节点对整个树执行布局

root.layout(&mut cache, &tree, &store, &mut sublayout);

这里没有显示在调用 layout 之前构建树的步骤。有关实现细节,请参阅 ecs/world.rsexamples/basic.rs

依赖项