use std::path::PathBuf;
use crate::animations::{
AnimationCommand, AnimationSet, AnimationSettled, ReactUiAnimationsPlugin,
};
use bevy::asset::embedded_asset;
use bevy::prelude::*;
#[cfg(feature = "custom_cursor")]
use bevy::window::CustomCursorImage;
use crate::bridge::{JsBridge, OpReceiver, OutboundResource, OutboundSender};
use crate::elements::editable::{
apply_pending_selections, on_focus_gained, on_focus_lost, on_text_edit_change,
sync_editable_a11y,
};
use crate::event::ReactEventRegistry;
use crate::host::{self, HostConfig, HostSenders};
use crate::message::{ReactAppExt, ReactMessage, ReactRegistry};
use crate::protocol::{op::Op, outbound::Outbound};
use crate::reconcile::{
OpApplyStats, apply_interaction_styles, apply_js_ops, apply_virtual_interaction_styles,
collect_hover_events, collect_pointer_events, collect_scroll_events, collect_ui_events,
collect_virtual_clicks, collect_virtual_hover_events, collect_virtual_pointer_events,
};
use crate::request::{RawRequest, ReactRequestRegistry, RequestReceiver, dispatch_react_requests};
#[derive(Resource, Default, Debug, Clone, Copy)]
pub struct PointerCapture {
pub dragging: bool,
pub over_ui: bool,
pub wheel_captured: bool,
}
impl PointerCapture {
pub fn is_captured(&self) -> bool {
self.dragging || self.over_ui || self.wheel_captured
}
}
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PointerCaptureSet;
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ReactApplySet;
pub struct ReactUiPlugin {
bundle: PathBuf,
hot_reload: bool,
default_font: Option<PathBuf>,
named_fonts: Vec<(String, PathBuf)>,
#[cfg(feature = "custom_cursor")]
custom_cursors: Vec<(String, PathBuf, (u16, u16))>,
#[cfg(feature = "devtools")]
devtools: crate::devtools::DevtoolsConfig,
precompile_filters: PrecompileFilters,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum FilterSelection {
#[default]
All,
Names(Vec<String>),
Off,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PrecompileFilters {
pub builtins: FilterSelection,
pub filters: FilterSelection,
pub morphs: FilterSelection,
}
impl ReactUiPlugin {
pub fn new(bundle: impl Into<PathBuf>) -> Self {
Self {
bundle: bundle.into(),
hot_reload: true,
default_font: None,
named_fonts: Vec::new(),
#[cfg(feature = "custom_cursor")]
custom_cursors: Vec::new(),
#[cfg(feature = "devtools")]
devtools: Default::default(),
precompile_filters: Default::default(),
}
}
pub const DEFAULT_BUNDLE: &'static str = "ui/dist/app.js";
#[cfg(feature = "devtools")]
pub fn devtools(mut self, config: crate::devtools::DevtoolsConfig) -> Self {
self.devtools = config;
self
}
pub fn precompile_filters(mut self, config: PrecompileFilters) -> Self {
self.precompile_filters = config;
self
}
pub fn hot_reload(mut self, yes: bool) -> Self {
self.hot_reload = yes;
self
}
pub fn default_font(mut self, path: impl Into<PathBuf>) -> Self {
self.default_font = Some(path.into());
self
}
pub fn font(mut self, name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
self.named_fonts.push((name.into(), path.into()));
self
}
#[cfg(feature = "custom_cursor")]
#[cfg_attr(docsrs, doc(cfg(feature = "custom_cursor")))]
pub fn cursor(
mut self,
name: impl Into<String>,
path: impl Into<PathBuf>,
hotspot: (u16, u16),
) -> Self {
self.custom_cursors
.push((name.into(), path.into(), hotspot));
self
}
}
pub(crate) fn register_layer_shader_assets(app: &mut App) {
bevy::shader::load_shader_library!(app, "layer/filter_prelude.wgsl");
embedded_asset!(app, "layer/composite.wgsl");
embedded_asset!(app, "layer/render/mip_blit.wgsl");
embedded_asset!(app, "layer/render/backdrop_blit.wgsl");
embedded_asset!(app, "filters/builtin/color_matrix.wgsl");
embedded_asset!(app, "filters/builtin/blur.wgsl");
embedded_asset!(app, "filters/builtin/bloom.wgsl");
embedded_asset!(app, "filters/builtin/chromatic_aberration.wgsl");
embedded_asset!(app, "filters/builtin/gradient_map.wgsl");
embedded_asset!(app, "filters/builtin/outline.wgsl");
embedded_asset!(app, "filters/builtin/pinch.wgsl");
embedded_asset!(app, "filters/builtin/shadow.wgsl");
embedded_asset!(app, "filters/builtin/crossfade.wgsl");
embedded_asset!(app, "filters/builtin/linear_wipe.wgsl");
embedded_asset!(app, "filters/builtin/pixelize.wgsl");
}
pub(crate) fn add_camera_gate_systems(app: &mut App) {
app.add_systems(
PostUpdate,
crate::render_target::drive_target_cameras
.after(bevy::ui::UiSystems::PostLayout)
.after(bevy::camera::visibility::VisibilitySystems::VisibilityPropagate)
.before(bevy::camera::visibility::VisibilitySystems::CheckVisibility)
.before(bevy::light::SimulationLightSystems::UpdateDirectionalLightCascades),
);
}
impl Default for ReactUiPlugin {
fn default() -> Self {
Self::new(Self::DEFAULT_BUNDLE)
}
}
pub(crate) fn register_builtin_bindings(app: &mut App) {
app.add_react_message::<crate::gamepad::GamepadRumble>()
.add_react_message::<crate::gamepad::GamepadStopRumble>()
.add_react_request::<crate::window::WindowSizeGet>()
.add_react_request::<crate::gamepad::GamepadGetAll>()
.add_react_event::<crate::keyboard::KeyDown>()
.add_react_event::<crate::keyboard::KeyUp>()
.add_react_event::<crate::window::Resize>()
.add_react_event::<crate::gamepad::GamepadConnected>()
.add_react_event::<crate::gamepad::GamepadDisconnected>()
.add_react_event::<crate::gamepad::GamepadInputEvent>();
crate::filters::register_builtin_filters(app);
}
impl Plugin for ReactUiPlugin {
fn finish(&self, app: &mut App) {
let registry = app.world().resource::<crate::ext::ExtRegistry>().clone();
registry.styles().validate();
app.world()
.resource::<crate::ext::ExtRegistrySlot>()
.fill(registry);
}
fn build(&self, app: &mut App) {
if app.is_plugin_added::<bevy::render::RenderPlugin>() {
register_layer_shader_assets(app);
if let Some(render_app) = app.get_sub_app_mut(bevy::render::RenderApp) {
use crate::layer::render as lr;
use bevy::core_pipeline::schedule::{Core2d, Core2dSystems, Core3d, Core3dSystems};
use bevy::render::render_phase::{AddRenderCommand, sort_phase_system};
use bevy::render::render_resource::SpecializedRenderPipelines;
use bevy::render::{
ExtractSchedule, GpuResourceAppExt, Render, RenderStartup, RenderSystems,
};
use bevy::ui_render::{
RenderUiSystems, TransparentUi, extract_ui_camera_view, ui_pass,
};
render_app
.init_resource::<lr::ExtractedUiLayers>()
.init_resource::<lr::LayerTextureStore>()
.init_gpu_resource::<SpecializedRenderPipelines<lr::LayerCompositePipeline>>()
.init_gpu_resource::<lr::LayerCompositeMeta>()
.init_gpu_resource::<lr::transform3d::CompositeUniformsMeta>()
.init_gpu_resource::<SpecializedRenderPipelines<lr::LayerFilterPipeline>>()
.init_gpu_resource::<lr::LayerFilterMeta>()
.init_gpu_resource::<SpecializedRenderPipelines<lr::mips::LayerBlitPipeline>>()
.init_gpu_resource::<lr::mips::LayerMipMeta>()
.init_gpu_resource::<SpecializedRenderPipelines<lr::backdrop::BackdropBlitPipeline>>()
.init_gpu_resource::<lr::backdrop::BackdropMeta>()
.init_gpu_resource::<lr::morph::MorphMeta>()
.add_render_command::<TransparentUi, lr::DrawLayerComposite>()
.add_systems(
RenderStartup,
(
lr::init_layer_composite_pipeline,
lr::init_layer_filter_pipeline,
lr::mips::init_layer_blit_pipeline,
lr::backdrop::init_backdrop_blit_pipeline,
),
)
.init_resource::<lr::clip::SwappedClips>()
.init_resource::<lr::cached::HiddenMembers>()
.init_resource::<lr::warm::WarmShaders>()
.add_systems(ExtractSchedule, lr::warm::extract_warm_shaders)
.add_systems(
Render,
lr::warm::warm_layer_pipelines.in_set(RenderSystems::PrepareResources),
)
.configure_sets(
ExtractSchedule,
(
RenderUiSystems::ExtractBoxShadows,
RenderUiSystems::ExtractBackgrounds,
RenderUiSystems::ExtractImages,
RenderUiSystems::ExtractTextureSlice,
RenderUiSystems::ExtractBorders,
RenderUiSystems::ExtractViewportNodes,
RenderUiSystems::ExtractTextBackgrounds,
RenderUiSystems::ExtractTextShadows,
RenderUiSystems::ExtractText,
RenderUiSystems::ExtractCursor,
RenderUiSystems::ExtractDebug,
RenderUiSystems::ExtractGradient,
)
.in_set(lr::ExtractUiItems),
)
.add_systems(
ExtractSchedule,
(
lr::extract_ui_layers.after(extract_ui_camera_view),
lr::clip::swap_interior_clips_in
.before(RenderUiSystems::ExtractCameraViews)
.before(lr::ExtractUiItems),
lr::clip::swap_interior_clips_out
.after(RenderUiSystems::ExtractCameraViews)
.after(lr::ExtractUiItems),
lr::cached::hide_cached_layer_members
.after(lr::extract_ui_layers)
.before(lr::ExtractUiItems),
lr::cached::restore_cached_layer_members
.after(lr::clip::swap_interior_clips_out),
),
)
.add_systems(
Render,
(
lr::redistribute_ui_layers
.in_set(RenderSystems::PhaseSort)
.before(sort_phase_system::<TransparentUi>),
lr::prepare_layer_textures.in_set(RenderSystems::PrepareResources),
lr::morph::prepare_layer_morphs
.in_set(RenderSystems::PrepareBindGroups)
.after(lr::prepare_layer_textures)
.before(lr::prepare_layer_filters)
.before(lr::prepare_layer_composites),
lr::prepare_layer_filters
.in_set(RenderSystems::PrepareBindGroups)
.after(lr::prepare_layer_textures)
.before(lr::prepare_layer_composites),
lr::backdrop::prepare_layer_backdrops
.in_set(RenderSystems::PrepareBindGroups)
.after(lr::prepare_layer_textures)
.before(lr::prepare_layer_composites),
lr::mips::prepare_layer_mips
.in_set(RenderSystems::PrepareBindGroups)
.after(lr::prepare_layer_filters)
.before(lr::prepare_layer_composites),
lr::prepare_layer_composites.in_set(RenderSystems::PrepareBindGroups),
),
)
.add_systems(
Core2d,
lr::ui_layer_capture_pass
.after(Core2dSystems::PostProcess)
.before(ui_pass),
)
.add_systems(
Core3d,
lr::ui_layer_capture_pass
.after(Core3dSystems::PostProcess)
.before(ui_pass),
);
}
}
let (ops_tx, ops_rx) = crossbeam_channel::unbounded::<Vec<Op>>();
let (flush_tx, flush_rx) = crossbeam_channel::unbounded();
let (emit_tx, emit_rx) = crossbeam_channel::unbounded::<ReactMessage>();
let (request_tx, request_rx) = crossbeam_channel::unbounded::<RawRequest>();
let (anim_tx, anim_rx) = crossbeam_channel::unbounded::<AnimationCommand>();
app.init_resource::<crate::ext::ExtRegistry>();
app.init_resource::<crate::ext::VirtualPointers>();
crate::style::add_core_styles(app);
app.add_react_elements(crate::elements::CORE_ELEMENTS);
let ext_slot = crate::ext::ExtRegistrySlot::default();
app.insert_resource(ext_slot.clone());
let outbound_tx = host::spawn(
app,
HostConfig {
ext: ext_slot,
bundle: self.bundle.clone(),
hot_reload: self.hot_reload,
},
HostSenders {
ops: ops_tx,
flush: flush_tx,
emit: emit_tx,
request: request_tx,
anim: anim_tx,
},
);
app.insert_resource(crate::reconcile::FlushInfos(flush_rx));
app.insert_resource(BridgeChannels {
ops_rx: Some(ops_rx),
outbound_tx: outbound_tx.clone(),
})
.insert_resource(OutboundResource(outbound_tx))
.insert_resource(EmitReceiver(emit_rx))
.insert_resource(RequestReceiver(request_rx))
.insert_resource(ReactUiConfig {
default_font: self.default_font.clone(),
named_fonts: self.named_fonts.clone(),
#[cfg(feature = "custom_cursor")]
custom_cursors: self.custom_cursors.clone(),
precompile_filters: self.precompile_filters.clone(),
})
.init_resource::<crate::filters::WarmShaderList>()
.add_systems(Startup, crate::filters::build_warm_shader_list)
.init_resource::<ReactRegistry>()
.init_resource::<ReactRequestRegistry>()
.init_resource::<ReactEventRegistry>()
.init_resource::<PointerCapture>()
.init_resource::<crate::reconcile::ActiveDrag>()
.init_resource::<crate::touch_scroll::TouchScrollState>()
.init_resource::<OpApplyStats>()
.init_resource::<crate::reconcile::FrameStamp>()
.init_resource::<crate::ui_map::AtlasLayoutCache>()
.init_resource::<crate::scrollbar::ScrollbarTracks>()
.init_resource::<Fonts>()
.init_resource::<crate::cursor::CustomCursors>()
.init_asset::<crate::svg::SvgDocument>()
.register_asset_loader(crate::svg::SvgAssetLoader)
.init_resource::<crate::render_target::RenderTargets>()
.add_systems(Startup, crate::layer::pick3d::init_transform3d_pointer)
.add_systems(Startup, setup)
.add_systems(
PreUpdate,
(dispatch_react_messages, dispatch_react_requests),
)
.add_systems(
PreUpdate,
crate::layer::pick3d::drive_transform3d_pointer
.before(bevy::picking::PickingSystems::ProcessInput),
)
.add_systems(
PreUpdate,
crate::pick_clip::filter_clipped_pointer_hits
.after(bevy::picking::PickingSystems::Backend)
.before(bevy::picking::PickingSystems::Hover),
)
.add_systems(
PreUpdate,
crate::layer::pick3d::suppress_transformed_layer_hits
.after(bevy::picking::PickingSystems::Backend)
.after(crate::pick_clip::filter_clipped_pointer_hits)
.before(bevy::picking::PickingSystems::Hover),
)
.configure_sets(
PreUpdate,
crate::ext::PickRefineSet
.after(bevy::picking::PickingSystems::Backend)
.after(crate::pick_clip::filter_clipped_pointer_hits)
.after(crate::layer::pick3d::suppress_transformed_layer_hits)
.before(bevy::picking::PickingSystems::Hover),
)
.add_systems(
Update,
(
apply_js_ops.in_set(ReactApplySet),
collect_ui_events,
crate::keyboard::collect_keyboard_events,
collect_hover_events,
collect_pointer_events.in_set(PointerCaptureSet),
(
crate::scroll::apply_scroll
.in_set(PointerCaptureSet)
.after(collect_pointer_events),
crate::touch_scroll::apply_touch_scroll
.in_set(PointerCaptureSet)
.after(collect_pointer_events),
crate::scroll::collect_wheel_events
.in_set(PointerCaptureSet)
.after(collect_pointer_events),
crate::scrollbar::bridge_scrollbar_capture
.in_set(PointerCaptureSet)
.after(collect_pointer_events),
),
crate::transition::drive_scroll_transition
.after(apply_js_ops)
.after(PointerCaptureSet),
collect_scroll_events
.after(crate::transition::drive_scroll_transition)
.after(PointerCaptureSet)
.after(apply_js_ops),
apply_interaction_styles.after(apply_js_ops),
crate::transition::drive_transitions.after(apply_interaction_styles),
(
crate::svg::update_svg_surfaces.in_set(crate::ext::ElementRasterSet),
crate::cursor::drive_cursor_icon.after(apply_js_ops),
crate::scrollbar::sync_scrollbars.after(apply_js_ops),
crate::scrollbar::position_scrollbars
.after(apply_js_ops)
.after(crate::scrollbar::sync_scrollbars),
crate::scrollbar::style_scrollbar_states
.after(crate::scrollbar::sync_scrollbars),
),
crate::render_target::bind_target_views.after(apply_js_ops),
crate::render_target::drive_render_targets
.after(crate::render_target::bind_target_views),
collect_virtual_clicks,
collect_virtual_pointer_events,
collect_virtual_hover_events,
apply_virtual_interaction_styles,
),
);
register_builtin_bindings(app);
app.add_systems(
Update,
crate::window::send_resize_events.after(apply_js_ops),
);
app.init_resource::<crate::gamepad::GamepadRegistry>();
app.add_message::<bevy::input::gamepad::GamepadEvent>();
app.add_message::<bevy::input::gamepad::GamepadConnectionEvent>();
app.add_message::<bevy::input::gamepad::GamepadRumbleRequest>();
app.add_systems(
Update,
crate::gamepad::collect_gamepad_events.after(apply_js_ops),
);
app.add_systems(
Update,
(
crate::background_image::bind_background_textures
.after(apply_js_ops)
.after(apply_interaction_styles),
crate::background_image::sync_background_tile_scale
.after(apply_js_ops)
.after(apply_interaction_styles),
),
);
app.insert_resource(crate::image_rendering::ImageVariants::from_app(app));
app.add_systems(
Update,
crate::image_rendering::bind_image_rendering
.after(apply_js_ops)
.after(apply_interaction_styles)
.after(crate::background_image::bind_background_textures),
);
app.init_resource::<crate::layer::LayerMembership>();
app.init_resource::<crate::layer::LayersRegistry>();
app.init_resource::<crate::layer::LayerContentDirt>();
app.init_resource::<crate::layer::LayerRepaintState>();
app.init_resource::<crate::layer::clip::LayerClips>();
app.add_systems(
Update,
(
crate::layer::evaluate_layer_promotions
.after(apply_js_ops)
.before(apply_interaction_styles)
.before(AnimationSet::Apply),
crate::layer::watch_layer_image_assets,
crate::layer::pick3d::correct_transformed_interactions
.before(apply_interaction_styles)
.before(collect_hover_events)
.before(crate::reconcile::collect_pointer_events),
),
);
app.configure_sets(
Update,
crate::ext::InteractionSyncSet
.after(crate::layer::pick3d::correct_transformed_interactions)
.before(apply_interaction_styles)
.before(collect_hover_events)
.before(crate::reconcile::collect_pointer_events),
);
app.configure_sets(
Update,
crate::ext::ElementRasterSet
.after(apply_js_ops)
.after(AnimationSet::Apply)
.after(crate::transition::drive_transitions),
);
app.configure_sets(
Update,
crate::ext::ElementOverrideSet
.after(apply_js_ops)
.after(AnimationSet::Apply)
.after(crate::transition::drive_transitions),
);
app.add_systems(
Update,
(
crate::filters::resolve_chains::<
crate::filters::FilterInput,
crate::filters::ResolvedFilterChain,
>,
crate::filters::resolve_chains::<
crate::filters::BackdropInput,
crate::filters::ResolvedBackdropChain,
>,
crate::filters::resolve_chains::<
crate::filters::MorphInput,
crate::filters::ResolvedMorphChain,
>,
)
.after(apply_interaction_styles)
.before(crate::transition::drive_transitions)
.before(AnimationSet::Apply),
);
app.add_systems(
PostUpdate,
(
crate::transition::layout::drive_layout_transitions
.after(bevy::ui::UiSystems::Layout)
.before(bevy::ui::UiSystems::PostLayout)
.before(crate::layer::sync_layer_geometry),
crate::layer::sync_layer_geometry.after(bevy::ui::UiSystems::Layout),
crate::layer::resolve_layer_repaints
.after(crate::layer::sync_layer_geometry)
.after(bevy::ui::UiSystems::PostLayout),
crate::layer::transform3d::sync_transform3d_matrices
.after(crate::layer::sync_layer_geometry)
.before(crate::layer::resolve_layer_repaints),
crate::layer::clip::sync_layer_clips
.after(crate::layer::sync_layer_geometry)
.after(bevy::ui::UiSystems::PostLayout),
crate::scroll::settle_controlled_scroll.after(bevy::ui::UiSystems::Layout),
),
);
add_camera_gate_systems(app);
app.configure_sets(
PostUpdate,
crate::ext::MeasureStampSet
.after(bevy::ui::widget::update_image_content_size_system)
.before(bevy::ui::UiSystems::Layout),
);
app.add_systems(
PostUpdate,
crate::svg::stamp_svg_measures.in_set(crate::ext::MeasureStampSet),
);
app.add_observer(crate::window::handle_window_size_request);
app.add_observer(crate::gamepad::on_rumble);
app.add_observer(crate::gamepad::on_stop_rumble);
app.add_observer(crate::gamepad::handle_gamepad_get_all);
#[cfg(not(target_arch = "wasm32"))]
app.add_systems(bevy::app::First, crate::reconcile::mark_frame_start);
app.add_observer(on_text_edit_change);
app.add_observer(on_focus_gained);
app.add_observer(on_focus_lost);
app.add_systems(
PostUpdate,
(apply_pending_selections, sync_editable_a11y).after(bevy::text::EditableTextSystems),
);
app.add_plugins(ReactUiAnimationsPlugin::new(anim_rx))
.configure_sets(Update, AnimationSet::Apply.after(apply_js_ops))
.add_systems(Update, forward_animation_settled.after(AnimationSet::Tick));
#[cfg(feature = "devtools")]
if self.devtools.enabled {
app.add_plugins(crate::devtools::DevtoolsPlugin::new(self.devtools.clone()));
}
}
}
fn forward_animation_settled(
mut settled: MessageReader<AnimationSettled>,
outbound: Res<OutboundResource>,
) {
for s in settled.read() {
let _ = outbound.0.send(Outbound::AnimationFinished {
id: s.id,
token: s.token,
finished: s.finished,
});
}
}
#[derive(Component)]
pub(crate) struct UiRoot;
#[derive(Resource)]
pub(crate) struct ReactUiConfig {
default_font: Option<PathBuf>,
named_fonts: Vec<(String, PathBuf)>,
#[cfg(feature = "custom_cursor")]
custom_cursors: Vec<(String, PathBuf, (u16, u16))>,
pub(crate) precompile_filters: PrecompileFilters,
}
#[derive(Resource, Default)]
pub struct Fonts {
pub default: Option<Handle<Font>>,
pub named: std::collections::HashMap<String, Handle<Font>>,
}
#[derive(Resource)]
struct BridgeChannels {
ops_rx: Option<OpReceiver>,
outbound_tx: OutboundSender,
}
#[derive(Resource)]
struct EmitReceiver(crossbeam_channel::Receiver<ReactMessage>);
fn dispatch_react_messages(
rx: Res<EmitReceiver>,
registry: Res<ReactRegistry>,
mut commands: Commands,
) {
while let Ok(msg) = rx.0.try_recv() {
registry.dispatch(msg, &mut commands);
}
}
fn setup(
mut commands: Commands,
mut channels: ResMut<BridgeChannels>,
config: Res<ReactUiConfig>,
assets: Res<AssetServer>,
ext_registry: Res<crate::ext::ExtRegistry>,
ext_slot: Res<crate::ext::ExtRegistrySlot>,
) {
ext_registry.styles().validate();
ext_slot.fill(ext_registry.clone());
commands.insert_resource(Fonts {
default: config.default_font.as_ref().map(|p| assets.load(p.clone())),
named: config
.named_fonts
.iter()
.map(|(name, path)| (name.clone(), assets.load(path.clone())))
.collect(),
});
#[cfg(feature = "custom_cursor")]
commands.insert_resource(crate::cursor::CustomCursors(
config
.custom_cursors
.iter()
.map(|(name, path, hotspot)| {
(
name.clone(),
CustomCursorImage {
handle: assets.load(path.clone()),
hotspot: *hotspot,
..default()
},
)
})
.collect(),
));
let root = commands
.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::FlexStart,
align_items: AlignItems::Center,
row_gap: Val::Px(16.0),
..default()
},
bevy::picking::Pickable {
should_block_lower: false,
..default()
},
UiRoot,
))
.id();
let ops_rx = channels.ops_rx.take().expect("setup runs once");
let mut bridge = JsBridge::new(ops_rx, channels.outbound_tx.clone(), root);
bridge.ext = std::sync::Arc::new(ext_registry.clone());
commands.insert_resource(bridge);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_captured_covers_all_channels() {
assert!(!PointerCapture::default().is_captured());
assert!(
PointerCapture {
dragging: true,
..default()
}
.is_captured()
);
assert!(
PointerCapture {
over_ui: true,
..default()
}
.is_captured()
);
assert!(
PointerCapture {
wheel_captured: true,
..default()
}
.is_captured()
);
}
}