#bevy #gui-framework #sickle

sickle_ui

Bevy 的一个组件库,在 Bevy 中构建

3 个版本

新版本 0.2.3 2024年8月17日
0.2.2 2024年8月17日
0.2.1 2024年7月20日

66GUI 中排名

Download history 284/week @ 2024-07-20 138/week @ 2024-07-27 51/week @ 2024-08-03 82/week @ 2024-08-10 306/week @ 2024-08-17

604 每月下载量
3 crate 中使用

MIT/Apache

2.5MB
17K SLoC

Sickle UI

Crates.io Downloads

基于 Bevy 内部 bevy_ui 的组件库。

Screenshot of the simple_editor example

示例

如果您克隆了仓库,可以简单地构建并运行主示例

cargo build
cargo run --example simple_editor

[!WARNING] sickle_ui 仍在开发中。以下列出的是框架的稳定性。

主要缺失的功能

  • 集中式焦点管理
  • 文本/文本区域输入组件

它已经能做什么

  • 可调整大小的布局
    • 行/列
    • 滚动视图
    • 停靠区
    • 标签页容器
    • 浮动面板
    • 大小区域
    • 可折叠的
  • 输入
    • 滑块
    • 下拉列表
    • 复选框
    • 单选按钮组
  • 菜单
    • 菜单项(带有前导/尾随图标和键盘快捷方式支持)
    • 切换菜单项
    • 子菜单
    • 上下文菜单(基于组件)
  • 静态
    • 图标
    • 标签
  • 实用工具
    • 基于命令的样式
    • 交互的时序跟踪
    • 动画交互
    • 基于上下文的扩展
    • 拖放交互
    • 滚动交互
  • 主题化
    • 基于 Material 3 的调色板(深色/浅色,每个主题 3 个对比级别)
    • 集中式大小控制
    • 集中式字体控制
    • 自动主题更新
    • 主题覆盖

入门

首先,您需要将 sickle_ui 添加到项目的依赖中

[dependencies]
sickle_ui = "0.2.1"
# sickle_ui = { rev = "a548517", git = "https://github.com/UmbraLuminosa/sickle_ui" }

[!NOTE] 如果您想直接依赖仓库,请取消注释行并将 rev = "..." 改为您选择的版本。主版本由 git 标签标记。

一旦有了新的依赖项,使用 cargo build 下载它。现在您可以开始使用了,所以将其添加到您的应用程序作为插件

use bevy::prelude::*;
use sickle_ui::{prelude::*, SickleUiPlugin};

fn main() {
  App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(SickleUiPlugin)
        // ... your actual app plugins and systems can go here of course
        .run();
}

主要的 SickleUiPlugin 负责添加 sickle_ui 提供的所有便利功能,而 sickle_ui::prelude::* 则引入所有可用的扩展。请查看 simple_editor 示例(如上截图所示)以了解不同部分如何协同工作。

前言

[!重要] Sickle UI 主要使用 CommandsEntityCommands 来创建、样式化和配置小部件。使用这些小部件的系统需要考虑,直到执行下一个 apply_deferred 命令之前,更改不会反映在 ECS world 中。从 bevy 0.13 开始,这通常是自动的。内部 sickle_ui 使用在良好定义的集合和顺序中的系统,以确保所有小部件都能良好地协同工作。

基本用例

在最简单的用例中,您只想使用现有的小部件来构建您的用户界面。Sickle UI 为 CommandsEntityCommands 添加了扩展,因此在常规系统上下文中,您可以通过调用一系列函数来快速创建布局。比较原始 bevy 和 Sickle UI

原始 bevy

在 Bevy 中,您可以使用 commands.spawn(bundle)commands.entity(entity).with_children(builder) 来创建实体。通常,您会传递一个 NodeBundleButtonBundle,或者是一些其他类型,比如 ImageBundle。然后,您可以使用 .with_children(builder) 扩展来创建子实体。这会很快变得冗长且复杂,因为 Rust 的借用规则。创建具有父子、兄弟或更深层次元素之间双向引用的实体将变得困难。

fn setup(mut commands: Commands) {
  commands.spawn(NodeBundle {
      style: Style {
          height: Val::Percent(100.),
          flex_direction: FlexDirection::Column,
          ..default()
      },
      background_color: Color::NONE.into(),
      ..default()
  }).with_children(|parent|{
    parent.spawn(NodeBundle::default()).with_children(|parent|{
      // ...
    });
  });
}

Sickle UI

库通过抽象构建器扩展,如

fn setup(mut commands: Commands) {
  commands.ui_builder(UiRoot).column(|column|{
    column.row(|row|{
      // ... etc.
    });
  });
}

虽然这可能看起来只是一个简单的缩写,但关键区别在于,回调中的 columnrow 本身就是上下文构建器,并且它们允许您访问 commands 和(如果有),entity_commands。您可以轻松跳转到另一个实体以添加组件、样式或创建新的子实体,而不会触发 Rust 的借用检查器。

我提到样式了吗?

是的,您还可以使用命令链来样式化由命令创建的实体,就像这样简单

fn setup(mut commands: Commands) {
  commands.ui_builder(UiRoot).column(|column|{
    // ...
  })
  .style()
  .width(Val::Percent(100.));
}

[!注意] 建筑函数的返回值可能与内部构建器不同。一个很好的例子是 scroll_view,其中外部返回值是外部最外层实体的构建器,而内部构建器是其内容视图的构建器(内容视图将被裁剪到框架内)!

[!注意] 使用这种方式无法进行样式交互。这些只是静态样式。有关应用交互式样式的信息,请参阅 StyleBuilder

这意味着在某些情况下,这也按预期工作

fn setup(mut commands: Commands) {
  commands.ui_builder(UiRoot).column(|column|{
    column
      .style()
      .width(Val::Percent(100.));
  });
}

上面的区别仅仅是开发者(无意中)的样式选择。

[!重要] 样式作为链中的常规命令应用,因此组件的渲染将在下一次 bevy 在其 PostUpdate 系统中计算 UI 布局时发生变化。样式命令映射到 Style 组件字段以及影响 Node 总体显示的一些其他组件字段,例如 BackgroundColorBorderColor 等。

![警告] 主题设置可能会覆盖这种方式应用的风格。请阅读下方的主题设置部分以了解主题设置如何工作。

值得注意的上下文

如前所述,所有构建函数都有一个上下文。

  • 根上下文是 UiRoot。在 UiRoot 上下文中创建的实体没有 Parent 实体,因此它将是一个根 Node
  • 最常见的常规上下文是 Entity,可以通过调用 commands.ui_builder(entity) 获取。其中 entity 是通过某种方式获取的实体 - ID,例如通过创建或查询。

![提示] 其他上下文针对特定用例,例如标签容器或菜单系统的上下文。您在使用这些小部件时最终会找到它们,但它们通常是透明的。使用编辑器的自动完成功能来查看每个上下文可用的扩展!

![注意] UiRoot 不要与 UiContextRoot 混淆。前者是一个标记,表示我们在没有 Parent 的情况下创建,而后者是一个表示小部件子树 逻辑 根的组件。它被如 ContextMenuTabContainer 等小部件用于找到动态创建的小部件的挂载点。ContextMenu 将菜单容器放置在 UiContextRoot,而当标签弹出时,TabContainer 将在树中的此位置创建 FloatingPanel

好的,现在我想要找到我的 column

不要担心你的列,或您创建的任何其他小部件,都可以像您周围的任何实体一样使用。只需添加一个组件

fn setup(mut commands: Commands) {
  commands.ui_builder(UiRoot).column(|_|{}).insert(MyMarkerComponent);
}

您还可以捕获其ID

fn setup(mut commands: Commands) {
  let my_column = commands.ui_builder(UiRoot).column(|_|{}).id();

  // ... OR

  let my_column = commands.ui_builder(UiRoot).column(|_|{}).insert(MyMarkerComponent).id();
}

![提示] 这与样式相同。回调可能指向与框架相同的实体,因此 insert 也可以在回调中调用

fn setup(mut commands: Commands) {
  commands.ui_builder(UiRoot).column(|column|{
    column.insert(MyMarkerComponent); 
  });
}

好的,但我没有找到我需要的小部件

如果您只需要在树中的某个地方创建一个简单的组件,您可以使用 spawn 或容器小部件,如 container 来创建或链式创建您的一次性节点。因此,转换我们开始时的 bevy 示例

fn setup(mut commands: Commands) {
  commands.ui_builder(UiRoot).column(|column|{
    column.container(NodeBundle {
        style: Style {
            height: Val::Percent(100.),
            flex_direction: FlexDirection::Column,
            ..default()
        },
        background_color: Color::NONE.into(),
        ..default()
    }, |my_container|{
      // ... etc. my_container is an `Entity` context UiBuilder
    });
  });
}

如果您甚至不需要为此小部件创建子节点

fn setup(mut commands: Commands) {
  commands.ui_builder(UiRoot).column(|column|{
    column.spawn(NodeBundle {
        style: Style {
            height: Val::Percent(100.),
            flex_direction: FlexDirection::Column,
            ..default()
        },
        background_color: Color::NONE.into(),
        ..default()
    });
  });
}

![提示] 由于我们使用 CommandsEntityCommands 以及常规 bevy_ui Node,您还可以将此语法与纯 Bevy 创建混合使用

fn setup(mut commands: Commands) {
  let mut inner_id = Entity::PLACEHOLDER;
  
  commands.spawn(NodeBundle {
      style: Style {
          height: Val::Percent(100.),
          flex_direction: FlexDirection::Column,
          ..default()
      },
      background_color: Color::NONE.into(),
      ..default()
  }).with_children(|parent|{
    inner_id = parent.spawn(NodeBundle::default()).with_children(|parent|{
      // ...
    }).id();
  });

  commands.ui_builder(inner_id).column(|column|{
    // Add a column into the inner entity and continue.
  });
}

![提示] 反之亦然!

fn setup(mut commands: Commands) {
  commands.ui_builder(UiRoot).column(|column|{
    column.row(|row|{
      let mut row_commands = row.entity_commands();
      row_commands.with_children(|parent| {
        // ... etc.
      });
    });
  });
}

但是我的小部件并不简单

然后您可以进入下一部分,扩展 Sickle UI

扩展 Sickle UI

Sickle UI 可以在多个级别上进行扩展。从最简单的开始

  • 结构扩展
  • 功能扩展
  • 主题小部件
  • 上下文相关主题小部件

然而,这些并不是独立的扩展。相反,这些是您可以应用于您创建的小部件的自定义级别。如果您不需要动态主题设置,您不需要实现所有这些。

![提示] sickle_ui 包含了上述每个场景的代码片段以供您开始使用。这些是 VSCode 代码片段,位于 .vscode 文件夹中。您可以将 sickle_ui.code-snippets 复制到您工作区的 .vscode 文件夹,或者将文件内容复制到您的 Rust 代码片段(文件 -> 首选项 -> 配置用户代码片段 -> [从列表中选择 Rust 语言]

结构扩展

这些是不需要系统支持的 widget,它们仅创建一个预定义的子树,您可以在定义的上下文中轻松注入。在这种情况下,您只需创建相关的扩展并使用 OK, but I didn't find a widget I need 下描述的技术来描述您的插件结构。

例如:

#[derive(Component, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct MyWidget;

impl MyWidget {
    fn frame() -> impl Bundle {
        (Name::new("My Widget"), NodeBundle::default())
    }
}

pub trait UiMyWidgetExt {
    fn my_widget(
        &mut self,
        spawn_children: impl FnOnce(&mut UiBuilder<Entity>),
    ) -> UiBuilder<Entity>;
}

impl UiMyWidgetExt for UiBuilder<'_, Entity> {
    fn my_widget(
        &mut self,
        spawn_children: impl FnOnce(&mut UiBuilder<Entity>),
    ) -> UiBuilder<Entity> {
        self.container((MyWidget::frame(), MyWidget), spawn_children)
    }
}

![提示] 上述内容是通过代码片段 Sickle UI Widget 生成的,如果您在 VSCode 中的 .rs 文件中开始输入 sickle(如果您已经添加了代码片段),则可以使用该片段。您可以在片段文件中自定义建议触发器,但建议避免使用 widget 作为触发器(它与常用 width 冲突)。

![提示] 这些代码片段支持 3 个制表符点:widget 组件名称、方便的 Name 组件字符串以及实际的扩展函数名称。

然后您可以在将其引入作用域后使用您的 widget

use my_widget::UiMyWidgetExt;

fn setup(mut commands: Commands) {
  // TODO: get your root entity where your widget will be added.
  // This could come from a query for example.
  let root_entity: Entity;
  commands.ui_builder(root_entity).my_widget(|my_widget|{
    // ... do more here!
  });
}

您可能已经注意到,该片段扩展了 UiBuilderEntity 上下文。只要您添加了 use my_widget::UiMyWidgetExt; 以将其引入作用域,您的 widget 就会可用。

![提示] VSCode 带有常规 Rust 扩展足够智能,可以在您输入扩展名称并按下 Ctrl + .(或 Mac 的等效键 Command + .)时建议导入。

您可能还注意到,该片段使用 self 来生成 containerself 将只是一个 UiBuilderEntity 上下文,所以您通过 use 引入作用域的任何其他扩展都将是可用的。这也意味着,只要您已导入它们,style 命令也是可用的。

功能扩展

功能扩展简单地说就是您的 widget 不仅仅创建一个预定义的结构。您可以使用代码片段 Sickle UI plugin widget 生成类似于 结构扩展 中概述的代码,并添加一个插件

pub struct MyWidgetPlugin;

impl Plugin for MyWidgetPlugin {
    fn build(&self, _app: &mut App) {
        // TODO
    }
}

#[derive(Component, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct MyWidget;

impl MyWidget {
    fn frame() -> impl Bundle {
        (Name::new("My Widget"), NodeBundle::default())
    }
}

pub trait UiMyWidgetExt {
    fn my_widget(
        &mut self,
        spawn_children: impl FnOnce(&mut UiBuilder<Entity>),
    ) -> UiBuilder<Entity>;
}

impl UiMyWidgetExt for UiBuilder<'_, Entity> {
    fn my_widget(
        &mut self,
        spawn_children: impl FnOnce(&mut UiBuilder<Entity>),
    ) -> UiBuilder<Entity> {
        self.container((MyWidget::frame(), MyWidget), spawn_children)
    }
}

![提示] 这些代码片段也支持制表符点,因此您可以使用一致的方式快速命名 widget 和插件。

剩下要做的就是实现 widget 的核心以及对其执行操作的系统。不要忘记将生成的插件添加到您的应用程序中!

主题小部件

现在,真正的乐趣开始了。

主题小部件指的是在中央位置为其定义了样式的部件。然而,主题小部件也允许根据其在部件树中的位置或其伪状态来覆盖其样式。

[!IMPORTANT] 主题小部件只对其最外层的 Node 应用样式,而不是对其子节点。这些是上下文主题小部件

与前述情况类似,有一个生成主题小部件外壳的代码片段:即 Sickle UI 主题插件部件

![提示] 这些代码片段也支持制表符点,因此您可以使用一致的方式快速命名 widget 和插件。

pub struct MyWidgetPlugin;

impl Plugin for MyWidgetPlugin {
    fn build(&self, app: &mut App) {
        app.add_plugins(ComponentThemePlugin::<MyWidget>::default());
    }
}

#[derive(Component, Clone, Debug, Default, Reflect, UiContext)]
#[reflect(Component)]
pub struct MyWidget;

impl DefaultTheme for MyWidget {
    fn default_theme() -> Option<Theme<MyWidget>> {
        MyWidget::theme().into()
    }
}

impl MyWidget {
    pub fn theme() -> Theme<MyWidget> {
        let base_theme = PseudoTheme::deferred(None, MyWidget::primary_style);
        Theme::new(vec![base_theme])
    }

    fn primary_style(style_builder: &mut StyleBuilder, theme_data: &ThemeData) {
        let theme_spacing = theme_data.spacing;
        let colors = theme_data.colors();

        style_builder
            .background_color(colors.surface(Surface::Surface))
            .padding(UiRect::all(Val::Px(theme_spacing.gaps.small)));
    }

    fn frame() -> impl Bundle {
        (Name::new("My Widget"), NodeBundle::default())
    }
}

pub trait UiMyWidgetExt {
    fn my_widget(
        &mut self,
        spawn_children: impl FnOnce(&mut UiBuilder<Entity>),
    ) -> UiBuilder<Entity>;
}

impl UiMyWidgetExt for UiBuilder<'_, Entity> {
    fn my_widget(
        &mut self,
        spawn_children: impl FnOnce(&mut UiBuilder<Entity>),
    ) -> UiBuilder<Entity> {
        self.container((MyWidget::frame(), MyWidget), spawn_children)
    }
}

虽然我们已经从之前的代码片段中看到了大部分内容,但还有一些新增内容。

ComponentThemePlugin

首先,在我们的应用程序的部件插件定义中注入了一个附加插件

impl Plugin for MyWidgetPlugin {
    fn build(&self, app: &mut App) {
        // This here is very important!
        app.add_plugins(ComponentThemePlugin::<MyWidget>::default());
    }
}

ComponentThemePlugin 被添加以处理组件的主题计算和重新加载。在这种情况下,我们为示例组件 MyWidget 添加了它。

[!IMPORTANT] MyWidget 现在必须 继承 UiContext。这个继承提供了在上下文主题小部件中稍后要查看的上下文的默认实现。

接下来,我们看 DefaultTheme 的实现

DefaultTheme

impl DefaultTheme for MyWidget {
    fn default_theme() -> Option<Theme<MyWidget>> {
        MyWidget::theme().into()
    }
}

这是将被应用的主题(除非它返回 None),到任何没有在其祖先上覆盖样式的部件树中的任何部件。我们将在主题部分中查看这是如何工作的。

目前,关键点是,通常最好将部件的默认主题作为此实现的一部分来实现,这样就不需要显式注入或提供合理的回退。

最后一部分是主题的实际定义,作为部件的 impl 块的一部分

impl MyWidget {
    pub fn theme() -> Theme<MyWidget> {
        let base_theme = PseudoTheme::deferred(None, MyWidget::primary_style);
        Theme::new(vec![base_theme])
    }

    fn primary_style(style_builder: &mut StyleBuilder, theme_data: &ThemeData) {
        let theme_spacing = theme_data.spacing;
        let colors = theme_data.colors();

        style_builder
            .background_color(colors.surface(Surface::Surface))
            .padding(UiRect::all(Val::Px(theme_spacing.gaps.small)));
    }

    // ...
}

上面的两个函数定义了主题本身以及作为 PseudoThemeNone 部分应用的样式。这只是在部件没有特殊 伪状态 时应用的样式。它是基本主题和始终应用于任何添加到部件树的新实体的回退样式。它也是任何覆盖的基础。

在最简单的使用情况下,定义样式只是调用提供的 style_builder 上的样式函数。这里可用的方法与在我提到样式吗?中概述的 UiStyle 扩展提供的方法相同,还有一些新增内容。

[!TIP] 请参阅下面的样式构建器以获取更多信息。

有了这个,我们就有了一个方便的地方来实现所有的样式需求。

[!IMPORTANT] 在设置 DynamicStylePostUpdate 系统的 PostUpdate 中应用主题中定义的样式。这意味着任何作为(在spawn捆绑中的)覆盖创建的节点样式或通过 .style() 命令应用的样式都可能在这里被覆盖。

上下文相关主题小部件

上下文主题小部件通过允许将样式应用到定义为主部件一部分的子小部件,将主题小部件提升了一步。代码片段 Sickle UI 上下文主题插件小部件 生成以下壳体:

pub struct MyWidgetPlugin;

impl Plugin for MyWidgetPlugin {
    fn build(&self, app: &mut App) {
        app.add_plugins(ComponentThemePlugin::<MyWidget>::default());
    }
}

#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct MyWidget {
    label: Entity,
}

impl Default for MyWidget {
    fn default() -> Self {
        Self {
            label: Entity::PLACEHOLDER,
        }
    }
}

impl DefaultTheme for MyWidget {
    fn default_theme() -> Option<Theme<MyWidget>> {
        MyWidget::theme().into()
    }
}

impl UiContext for MyWidget {
    fn get(&self, target: &str) -> Result<Entity, String> {
        match target {
            MyWidget::LABEL => Ok(self.label),
            _ => Err(format!(
                "{} doesn't exist for MyWidget. Possible contexts: {:?}",
                target,
                self.contexts()
            )),
        }
    }

    fn contexts(&self) -> Vec<&'static str> {
        vec![MyWidget::LABEL]
    }
}

impl MyWidget {
    pub const LABEL: &'static str = "Label";

    pub fn theme() -> Theme<MyWidget> {
        let base_theme = PseudoTheme::deferred(None, MyWidget::primary_style);
        Theme::new(vec![base_theme])
    }

    fn primary_style(style_builder: &mut StyleBuilder, theme_data: &ThemeData) {
        let theme_spacing = theme_data.spacing;
        let colors = theme_data.colors();
        let font = theme_data
            .text
            .get(FontStyle::Body, FontScale::Medium, FontType::Regular);

        style_builder
            .background_color(colors.surface(Surface::Surface))
            .padding(UiRect::all(Val::Px(theme_spacing.gaps.small)));

        style_builder
            .switch_target(MyWidget::LABEL)
            .sized_font(font);
    }

    fn frame() -> impl Bundle {
        (Name::new("My Widget"), NodeBundle::default())
    }
}

pub trait UiMyWidgetExt {
    fn my_widget(
        &mut self,
        spawn_children: impl FnOnce(&mut UiBuilder<Entity>),
    ) -> UiBuilder<Entity>;
}

impl UiMyWidgetExt for UiBuilder<'_, Entity> {
    fn my_widget(
        &mut self,
        spawn_children: impl FnOnce(&mut UiBuilder<Entity>),
    ) -> UiBuilder<Entity> {
        let label = self
            .label(LabelConfig {
                label: "MyWidget".into(),
                ..default()
            })
            .id();

        self.container((MyWidget::frame(), MyWidget { label }), spawn_children)
    }
}

[!TIP] 此代码片段还支持标签点,因此您可以快速以一致的方式命名小部件和插件。

现在,我们的部件组件不再只是一个标签。它现在有一个对标签子部件的引用

#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct MyWidget {
    label: Entity,
}

impl Default for MyWidget {
    fn default() -> Self {
        Self {
            label: Entity::PLACEHOLDER,
        }
    }
}

// ...

impl UiMyWidgetExt for UiBuilder<'_, Entity> {
    fn my_widget(
        &mut self,
        spawn_children: impl FnOnce(&mut UiBuilder<Entity>),
    ) -> UiBuilder<Entity> {
        let label = self
            .label(LabelConfig {
                label: "MyWidget".into(),
                ..default()
            })
            .id();

        self.container((MyWidget::frame(), MyWidget { label }), spawn_children)
    }
}

我们需要手动实现 默认,因为 实体 没有默认值。只要我们确保始终将实际实体分配给它(否则会恐慌!),使用 实体::占位符 就可以。

UiContext

但这不是唯一的添加。现在我们的代码片段为之前从简单的 derive 获取的 UiContext 定义了一个实现

impl UiContext for MyWidget {
    fn get(&self, target: &str) -> Result<Entity, String> {
        match target {
            MyWidget::LABEL => Ok(self.label),
            _ => Err(format!(
                "{} doesn't exist for MyWidget. Possible contexts: {:?}",
                target,
                self.contexts()
            )),
        }
    }

    fn contexts(&self) -> Vec<&'static str> {
        vec![MyWidget::LABEL]
    }
}

这告诉主题系统,MyWidget 除了主实体外还有一个额外的上下文。可以通过添加到 MyWidget::标签impl 块的常量来访问额外的上下文

impl MyWidget {
    pub const LABEL: &'static str = "Label";

    // ...
}

向下看我们还可以看到一个变化:现在 primary_style 将样式应用到标签上!

impl MyWidget {
    // ...

    fn primary_style(style_builder: &mut StyleBuilder, theme_data: &ThemeData) {
        let theme_spacing = theme_data.spacing;
        let colors = theme_data.colors();
        let font = theme_data
            .text
            .get(FontStyle::Body, FontScale::Medium, FontType::Regular);

        style_builder
            .background_color(colors.surface(Surface::Surface))
            .padding(UiRect::all(Val::Px(theme_spacing.gaps.small)));

        style_builder
            .switch_target(MyWidget::LABEL)
            .sized_font(font);
    }

    // ...
}

在上面的代码中,有一个对 style_builder 的调用,调用 switch_target 到我们的标签并设置其字体大小。有关如何详细工作的信息,请参阅 样式构建器

[!CAUTION] 一旦设置了目标,所有后续对 style_builder 的调用都将应用于该目标。您可以在构建器上调用 reset_target 来切换回主小部件,但将每个目标放在单独的链/组中更易于阅读。

就这样吗?

总的来说,是的。如果您使用这些代码片段,您就可以快速设置一个复杂的小部件树,并通过将调用链接到 style_builder 来定义每个子小部件的样式。当然,还有其他与主题过程交互的方式,例如访问世界或当前小部件组件,但其核心是相同的:一个主题,由伪主题组成,构建小部件及其子小部件的样式。

主题化

主题化是将样式应用于实体的过程(Node),基于其在小部件树中的位置、功能和当前状态。主题是一个 主题 的集合,其中 伪主题 定义了当实体在其实体组件的 伪状态 集合中具有相关的 PseudoState 时其样式。

样式是按属性进行的,这意味着每个可样式化属性在最终的 动态样式 组件中都有自己的条目,该组件附加到实体。每个 主题 和它们的 伪主题 都会严格按顺序进行评估,以计算每个属性的最终样式。

[!IMPORTANT] 动态样式 组件也可以手动生成并附加到实体。如果开发人员想要有交互式/动画样式的功能,但不想为通常的主题查找付费,这很有用。它还适用于一次性小部件。

[注意] 主题系统会移除标记在 UiContextcleared_contexts 中的任何实体的 DynamicStyle 组件。默认情况下,这是其 contexts 的完整列表。

评估顺序

当主题组件被添加到层次结构中时,系统将查找其祖先链(包括自身)中的所有 主题 组件,直到达到根实体。最后检查的是 默认主题 实现。一旦找到适用的 主题 列表,它们将按相反的顺序进行评估。这意味着 默认主题 将首先被评估,然后是从根实体开始到主题实体的任何覆盖。

一旦我们有了 主题 列表,每个主题都将按其 specificity 顺序展开,以收集适用的 伪主题。只有在实体上定义的所有 PseudoState 都存在时,才会考虑 伪主题。然而,如果它只定义了 PseudoState 的子集,它仍然会被考虑,但在此之前将考虑完全覆盖状态的那些。

[备注] specificity伪主题 定义为的 PseudoState 的数量。唯一的例外是当 伪主题 定义为 None 时,它被视为实体的基本伪主题。

示例

如果一个实体有 PseudoState [Checked, Disabled, FirstChild],则定义了 None[Checked][Disabled][FirstChild][Checked, Disabled][Checked, FirstChild][Checked, Disabled, FirstChild]伪主题,按此顺序考虑。

如果实体只有 [Checked] 状态,则应用定义了 None[Checked]伪主题,但不会应用其他主题,因为它们要么定义了一个不相交的集合,要么不是实体状态的完整子集。

[重要] 定义为 None 或空集合 []伪主题 被视为基本伪主题。这意味着它们将始终在更具体的 伪主题 之前应用。

[!注意] 当伪主题(PseudoTheme)的specificity与其他伪主题相同时,它们将按照添加到主题(Theme)中的顺序应用!

什么触发了主题化?

如果设置了ComponentThemePlugin::,以下更改将触发对受管理组件C的主题处理:

  • 添加了带有C的实体:将为每个新实体评估并应用主题
  • 主题数据资源更改:将处理所有带有C的实体
  • 添加、更改或删除任何Theme<C>:将处理所有带有C的实体
  • 如果具有组件C的实体的PseudoStates发生变化(或已被删除),则将重新处理这些实体。

[!提示] 如果未使用ComponentThemePlugin,可以通过调用commands.entity(entity).refresh_theme::<C>();来手动触发主题处理。

[!提示] ComponentThemePlugin可以被设置为“自定义”主题。这仅仅意味着处理将在名为CustomThemeUpdate的系统集合中进行。这个集合在ThemeUpdateDynamicStyleUpdate之间安排,并允许开发者修改或使用常规主题步骤的结果。

我可以用CSS吗?

不。

但技术上,如果我编写自己的解析器呢?

仍然不行。主题与组件相关,并且没有跨组件的主题合并。这是因为sickle_ui不支持定义组件主题之间的关系以实现这一点(有多种原因)。

但是!如果我们讨论的是开发者不再在CSS中使用C的情况,这是可能的。

现代Web开发通常遵循某种样式简化,以避免遇到模糊的特定性或深层嵌套样式的性能成本(更不用说最小化了)。一种普遍的方法是使用BEM(块、元素、修饰符)表示法来组合类名。结合SASS等预处理器和一些纪律,大多数单页应用都有一个单级嵌套样式表。

解析这样的样式表,为每个类生成一个bevy组件,然后将样式转换为主题应该是完全可能的。由于主题覆盖的工作方式,只要两个主题不针对相同的实体进行样式化,就可以实现一些嵌套。为了使这有效,勇敢的开发者需要实现以下内容:

  • 一种从原始CSS中删除嵌套的设置(BEM + SASS是一个好的起点)
  • 某种*解析上述提到的“平坦”CSS以生成组件和主题的方法
  • 自动注入主题覆盖以实现嵌套的系统
  • 最后,一些系统可以自动应用与CSS中匹配的PseudoState。有关已实现的内容和使用方法,请参阅PseudoStates

[!注意] 要支持热重载,解析器需要与一个单一、预定义的组件协同工作,该组件包含有关它对应于任何给定实体的CSS class的信息。主题可以使用这些信息来恢复此类实体的样式表。允许这种方法的脚手架已在sickle_ui中存在。

你会...

不。

主题

Theme<C + DefaultTheme>是一个用于存储PseudoThemebevy组件。在部件树中插入一个Theme::<C>组件将覆盖其下(或在其上)的C组件的样式。

伪主题

PseudoTheme<C>是一个载体结构,用于将一组PseudoState映射到样式构建器。虽然可以使用DynamicStyleBuilder变体直接创建此结构,但建议使用公开的函数之一

build

build需要一个简单的回调,该回调接受一个StyleBuilder实例来设置实体样式。此样式构建器立即评估以生成将被克隆到实体的DynamicStyle。在样式构建器上切换上下文会发出警告。这是因为目标上下文在编译时是无法知道的。

deferred变体

延迟构建器作为回调存储,并在主题系统应用样式时评估。根据您使用的变体,回调将接收不同的参数集

  • deferred将接收样式构建器和主题数据资源
  • deferred_context将另外接收&C,这是对样式化组件实例的引用。
  • deferred_world将接收实体(ID),&C,以及对World的只读引用。
  • deferred_info_world将另外接收正在应用的主题的ID以及一组PseudoState。这两个都是可选的,因为主题可以从为基本伪主题(为None定义)定义的DefaultTheme中进行。此回调在需要将回调映射到外部样式表实现时很有用。

[!重要] 即使最终生成的样式将被完全丢弃,回调也可能被评估。这是因为覆盖是按属性计算的,而不是按伪主题计算的!

伪状态

PseudoStates是一个仅用于存储PseudoState变体的bevy组件。此组件由ComponentThemePlugin监视,有关如何将其与其关联的信息,请参阅什么触发了主题化?

EntityCommands扩展提供了具有ManagePseudoStateExt特征的特性,以下是如何管理列表的

  • add_pseudo_state:用于添加一个PseudoState
  • remove_pseudo_state:用于删除一个PseudoState

有几个系统会自动将某些 PseudoState 应用到实体上,但这些都需要手动选择。

  • 带有 FlexDirectionToPseudoState 标签的实体将被处理,根据其 Styleflex_direction 设置为 PseudoState::LayoutRowPseudoState::LayoutColumn。此更新在 PostUpdate 中完成,在 ThemeUpdate 之前,因此主题将自动处理布局更改。
  • 带有 VisibilityToPseudoState 标签的实体将被处理,以设置或从其 PseudoStates 列表中移除 PseudoState::Visible。此更新根据实体的 VisibilityInheritedVisibility 的实际可见性进行,仅在其中一个更改时更新。此更新在 PostUpdate 中完成,在 VisibilitySystems::VisibilityPropagate 之后,但在 ThemeUpdate 之前。
  • 具有组件 C 的实体的位置可以通过 HierarchyToPseudoState::<C> 插件跟踪。此插件将根据需要设置 PseudoState::FirstChildPseudoState::LastChildPseudoState::NthChild(i)PseudoState::SingleChildPseudoState::EvenChildPseudoState::OddChild。这也将在 PostUpdate 中完成,在 ThemeUpdate 之前。

大多数内置小部件也会根据用户交互设置 PseudoState,例如,当选项列表应该可见时,Dropdown 将设置 PseudoState::Open 等。这些在 UiBuilder 扩展本身上有文档说明。

哎呀!我没有可以滥用的伪状态!

别担心,有一个 PseudoState::Custom(String) 专门用于此类用例。

样式构建器

样式构建器是生成 DynamicStyle 组件的推荐方法,因为它允许开发者以一致的方式链式连接样式属性。除了 UiStyle 扩展允许的内容之外,样式构建器还添加了 interactiveanimated 属性。这些属性将用户交互(如悬停、按下等)与样式属性的改变联系起来。

interactiveanimated 之间的区别在于,interactive 样式在交互发生时立即应用。而 animated 属性会在起始值和结束值之间执行一些缓动插值以应用最终样式。

[!注意] animated 属性会中断属性链。这是因为动画属性是在链中控制的。

如果您想在没有伪主题的情况下使用StyleBuilder,您只需创建一个实例即可。

let mut style_builder = StyleBuilder::new();
style_builder
    .width(Val::Percent(100.))
    .height(Val::Percent(100.));

let dynamic_style: DynamicStyle = style_builder.into();
// Insert the dynamic stlye to your entity as any other `bevy` component.

[!WARNING] 将上下文切换到样式构建器时,直接转换为DynamicStyle会发出警告。这是因为目标上下文在编译时无法确定。

[!NOTE] 您可以使用style_builder.convert_with(&context)并将相关组件实例引用传递进去。这将像主题化一样构建样式,并返回一个Vec<(Option<Entity>, DynamicStyle)>。列表中的每个DynamicStyle都伴随着一个可选的放置实体。当它是None时,DynamicStyle应放置在&context组件放置的实体上。

交互式样式的示例

fn interactive_style(style_builder: &mut StyleBuilder, theme_data: &ThemeData) {
    style_builder
        .interactive()
        .width(InteractiveVals {
            idle: Val::Px(100.),
            hover: Val::Px(120.).into(),
            ..default()
        })
        .height(InteractiveVals {
            idle: Val::Px(100.),
            press: Val::Px(120.).into(),
            ..default()
        });
}

动画样式的示例

fn animated_style(style_builder: &mut StyleBuilder, theme_data: &ThemeData) {
    let theme_spacing = theme_data.spacing;

    style_builder
        .animated()
        .margin(AnimatedVals {
            idle: UiRect::all(Val::Px(theme_spacing.gaps.medium)),
            hover: UiRect::all(Val::Px(0.)).into(),
            ..default()
        })
        .copy_from(theme_data.interaction_animation);

    style_builder
        .animated()
        .scale(AnimatedVals {
            idle: 1.,
            enter_from: Some(0.),
            ..default()
        })
        .copy_from(theme_data.enter_animation);
}

[!CAUTION] interactive样式可以在任何枚举属性变体之间切换,如Val,因为这会立即生效。然而,animated属性需要与Lerp计算兼容的变体。无法在Val::Px(50.)Val::Percent(100.)之间进行插值,因为这需要昂贵的计算,所以`sickle_ui`默认不这样做。但这并不意味着开发者不能这样做,他们需要使用deferred回调来访问world,并使用UiUtils中提供的工具在创建样式时将一个变体转换为另一个(以便变体匹配)。他们还可以根据自己的选择进行转换,我们不做出判断。

切换目标

checkbox retargeting

上下文主题小部件中所述,小部件组件可以手动实现UiContext来提供额外的样式目标/放置。

在样式构建器配置中切换目标会将后续调用目标设置为除了主实体之外的实体。用于标识目标的字符串标签在主题构建过程中映射到实体。目标只能是小部件组件的实体属性。要恢复到对主小部件的样式,请在样式构建器上调用reset_target。内部,目标是一个Option,而None始终意味着“将目标设置为DynamicStyle组件所在的实体”。

[!CAUTION] 手动构建DynamicStyle组件允许直接设置目标实体,但这样做不推荐。

[!重要] 切换目标可以代理主组件的交互!animatedinteractive 样式都考虑了它们的 放置 交互,即持有 DynamicStyle 组件的实体。然而,如果样式的目标不是主实体,交互将显示在目标上。

一个例子是复选框,当整个容器被交互时,它会将样式应用到框和标签上。

切换放置

切换放置,而不是切换样式的 target,会改变 DynamicStyle 组件将被注入的位置。这意味着交互将被检测在放置上,从而可以将交互区域缩小到子实体。调用 reset_placement 将构建器重置为向主实体的 DynamicStyle 添加属性。内部 placement 是一个 Option,而 None 表示为新样式的主实体收集新属性。

[!注意] 在切换后,之前设置的 target 仍然存在。因此,建议在样式构建器上使用 switch_context 来显式设置放置和目标。None 目标将继续针对 DynamicStyle 所放置的实体,因此不可能从子小部件中样式化主实体。

[!提示] 上述操作的(预期)副作用之一是,可以使用一个子小部件上的交互,同时使用反馈样式针对不同的子小部件。

切换上下文

切换上下文同时设置 placementtarget,并且是建议与构建器交互以更改 placement 的方法。这是因为目标可能来自之前的调用并导致不希望的样式。在样式构建器上调用 reset_context 以删除 placementtarget 设置。

我缺少我想样式的属性!

别担心,sickle_ui 为您解决了这个问题!

所有属性类型(静态、交互、动画)都支持一个 custom 变体,可以作为回调提供。 static 属性接收 EntityWorld 作为参数。 interactive 回调将接收 EntityFluxInteractionWorld,而 animated 回调将接收 EntityAnimationStateWorld

在这些回调中,您可以随心所欲地搞乱整个世界,没有任何保证它不会在你面前炸毁 :D

动态样式

DynamicStyle 是一个用于存储应在其实例的 PostUpdate 执行期间应用的属性列表的 bevy 组件。可用于调度目的的 DynamicStylePostUpdate 系统总是会在主题化之后但 UiSystem::Layout 之前运行。

作用在此组件上的系统会在 DynamicStyle 发生变化时立即应用静态样式,并跟踪执行 interactiveanimated 属性应用。

![注意] DynamicStyle 将尽可能地自行清理。这意味着,仅包含静态样式的样式表在应用样式后将被删除。interactiveanimated 样式将强制组件保持。

![警告] animated 样式可以设置一个控制器,在进入动画执行完毕后删除属性。如果进入动画针对的是组件自身系统(例如 FloatingPanel 的大小)后来控制的属性,这非常有用。

主题数据

主题数据是一个具有意见的结构,包含对通用 UI 样式有用的值。目前,它具有与 Material3 主题松散匹配的颜色、一组间隙大小、区域大小、输入大小、字体配置等。

任何具有可变值的 sickle_ui 小部件将依赖于默认的主题数据。

![小心] 更新 ThemeData 资源将触发所有主题重新评估!

![注意] 建议为编辑器目的(甚至游戏!)创建的任何小部件都使用提供的主题数据资源。这是保持应用程序中 UI 小部件一致性的好方法。

![提示] 在 ThemeData 结构中支持自定义值,以存储应用程序特定的异常。

工具

有一些工具构成了 sickle_ui 小部件的基础,并且可以像新小部件一样重用。

FluxInteraction & FluxInteractionStopwatch

FluxInteractionsickle_ui 中所有交互的基础。它是内置的 Interaction 的包装,但它跟踪的是从一个状态到另一个状态的 转换 而不是当前 状态

FluxInteractionStopwatch 跟踪实体 FluxInteraction 上最后更改以来经过的时间。出于性能原因,这个计时器默认在每次更改后可用 1 秒。可以通过 FluxInteractionConfig 资源来控制它可用的时长,尽管不建议更改此设置。相反,可以使用 FluxInteractionStopwatchLock 来扩展或精确计时器需要可用的时长。

FluxInteraction 添加到实体的推荐方法是使用提供的 TrackedInteraction 套件。

![注意] FluxInteraction 依赖于 Interaction,但它不会将其添加到实体中。它必须按需添加。这是因为各种内置的 bevy 套件可能会或可能不会添加 Interaction

ScrollInteraction

滚动交互可以被附加到实体的 Scrollable 组件拦截。依赖于它的系统可以使用 Changed<Scrollable> 作为过滤器来优化更新。

![注意] 目前仅支持鼠标滚动,而 shift 键用于更改滚动轴。

DragInteraction

Draggable 组件中构建了 FluxInteractionRelativeCursorPosition,以将交互转换为拖动意图。该组件本身不执行任何操作,除了跟踪拖动意图。希望利用它的开发者可以依赖于更改检测,并使用提供的值来更新小部件的各个方面,例如,可以拖动滚动条来滚动内容,可以拖动调整大小句柄来更改其父元素的大小等。

DropInteraction

DraggableDroppableDropZone 一起构成了拖放交互。当一个 Droppable 正在被拖动到 DropZone 上时,区域会保留被拖动实体的引用。开发者需要检查源实体是否可接受,并指示拖放动作的状态。

[!NOTE] DropZone 依赖于 Interaction 来检测是否有东西在其上。

ResizeInteraction

可以轻松地将调整大小处理程序添加到任何小部件中,但这些都只是预样式化的可拖动项。它们不会自动更新其容器的大小。

PseudoState::Resizable(_) 状态可以添加到最外层容器,以控制哪个处理程序可以交互。

UiContextRoot

UiContextRoot 是一个标记组件,旨在发出 逻辑 UI 根的信号。该组件被上下文菜单和选项卡容器用于找到已生成实体的挂载点。在上下文菜单的情况下,菜单本身将挂载到这个根。在选项卡容器的情况下,当选项卡“弹出”时打开的浮动面板将挂载到上下文根。

[!TIP] 将 UiContextRoot 添加到可能被动态替换的 UI 根部分,如屏幕根。切换屏幕将清除所有可能由用户动态生成的上下文菜单和浮动面板,无需额外逻辑。

UiUtils

UiUtils 是一组有用的 UI 逻辑,例如在 Val 的不同变体之间进行转换或找到 UI 视口。

Ui 命令

有一些 Commands 扩展可以简化操作,例如管理 PseudoStatesFluxInteractionStopwatchLock 或记录实体的层次结构和组件。

上下文菜单

sickle_ui 中的上下文菜单系统依赖于反射来允许任何组件生成其自己的条目到上下文菜单。

为小部件实现上下文菜单需要两个步骤

  • 需要将具有 GenerateContextMenu 组件的 Interaction 实体标记为支持。
  • 实体上的至少一个组件需要实现 ContextMenuGenerator 并注册其类型。
impl Plugin for MyContextMenuPlugin {
    fn build(&self, app: &mut App) {
        app.register_type::<MyComponent>();
    }
}

#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component, ContextMenuGenerator)]
pub struct MyComponent;


impl ContextMenuGenerator for MyComponent {
    fn build_context_menu(&self, context: Entity, container: &mut UiBuilder<ContextMenu>) {
        // Add menu items as needed to the container.
        
        container
            .menu_item(MenuItemConfig {
                name: "Open in new".into(),
                trailing_icon: icons.open_in_new,
                ..default()
            })
            // OpenInNewFromContextMenu is an examle component that another system could track
            // and execute the operation when the menu item is interacted.
            .insert(OpenInNewFromContextMenu { context });
    }

    fn placement_index(&self) -> usize {
        0
    }
}

上面的示例是填充上下文菜单的准备,但菜单项只有在实体具有 InteractionGenerateContextMenu::default() 时才会显示在上下文菜单中。

[!NOTE] 如果实体具有多个具有 ContextMenuGenerator 实现的组件,它们都将用于生成最终的上下文菜单。

[!TIP] 放置索引中的间隙会导致菜单中添加分隔符。

锁定样式属性

样式属性有时可能被锁定。这是为了防止意外地样式化具有功能和由小部件自己的系统驱动的部分。尝试样式化或主题化锁定属性会导致发出警告(并且不会应用到属性上)。

[!NOTE] 当然,直接更改 Style(或任何其他与样式相关的组件)将绕过检查。

[!TIP] 如果小部件在锁定属性的同时仍然想使用样式 API,则提供 style_unchecked 命令行来跳过锁定检查。

跟踪样式状态

有时追踪样式应用的整体状态是有用的。sickle_ui提供了一个名为TrackedStyleState的“元”样式属性,可以用来从DynamicStyle驱动其他小部件。它实际上并不应用任何样式。

[!注意] TrackedStyleState是可Lerp的,因此它可以被动画化。下拉菜单使用这个功能来控制其选项面板中的滚动视图,在过渡期间禁用它以避免视觉上的突兀。

依赖关系

~44–81MB
~1.5M SLoC