use std::path::PathBuf;
use crate::animations::{
AnimationCommand, AnimationSet, AnimationSettled, ReactUiAnimationsPlugin,
};
use bevy::asset::embedded_asset;
use bevy::prelude::*;
use bevy::window::CustomCursorImage;
use crate::filter::{FilterMaterial, FilterMaterialCache, init_filter_assets};
use crate::bridge::{JsBridge, OpReceiver, OutboundResource, OutboundSender};
use crate::event::ReactEventRegistry;
use crate::host::{self, HostConfig, HostSenders};
use crate::message::{ReactMessage, ReactRegistry};
use crate::protocol::{Op, Outbound};
use crate::reconcile::{
OpApplyStats, apply_interaction_styles, apply_js_ops, apply_pending_selections,
apply_surface_interaction_styles, collect_canvas_resize_events, collect_hover_events,
collect_pointer_events, collect_scroll_events, collect_surface_clicks,
collect_surface_hover_events, collect_surface_pointer_events, collect_ui_events,
on_focus_gained, on_focus_lost, on_text_edit_change, sync_editable_a11y,
};
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,
}
impl PointerCapture {
pub fn is_captured(&self) -> bool {
self.dragging || self.over_ui
}
}
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PointerCaptureSet;
pub struct ReactUiPlugin {
bundle: PathBuf,
hot_reload: bool,
animations: bool,
default_font: Option<PathBuf>,
named_fonts: Vec<(String, PathBuf)>,
custom_cursors: Vec<(String, PathBuf, (u16, u16))>,
}
impl ReactUiPlugin {
pub fn new(bundle: impl Into<PathBuf>) -> Self {
Self {
bundle: bundle.into(),
hot_reload: true,
animations: true,
default_font: None,
named_fonts: Vec::new(),
custom_cursors: Vec::new(),
}
}
pub fn hot_reload(mut self, yes: bool) -> Self {
self.hot_reload = yes;
self
}
pub fn with_animations(mut self, yes: bool) -> Self {
self.animations = 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
}
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
}
}
impl Plugin for ReactUiPlugin {
fn build(&self, app: &mut App) {
if app.is_plugin_added::<bevy::render::RenderPlugin>() {
embedded_asset!(app, "filter.wgsl");
app.add_plugins(UiMaterialPlugin::<FilterMaterial>::default())
.init_resource::<FilterMaterialCache>()
.add_systems(Startup, init_filter_assets);
}
let (ops_tx, ops_rx) = crossbeam_channel::unbounded::<Vec<Op>>();
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>();
let outbound_tx = host::spawn(
app,
HostConfig {
bundle: self.bundle.clone(),
hot_reload: self.hot_reload,
},
HostSenders {
ops: ops_tx,
emit: emit_tx,
request: request_tx,
anim: anim_tx,
},
);
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(),
custom_cursors: self.custom_cursors.clone(),
})
.init_resource::<ReactRegistry>()
.init_resource::<ReactRequestRegistry>()
.init_resource::<ReactEventRegistry>()
.init_resource::<PointerCapture>()
.init_resource::<OpApplyStats>()
.init_resource::<crate::ui_map::AtlasLayoutCache>()
.init_resource::<Fonts>()
.init_resource::<crate::cursor::CustomCursors>()
.init_resource::<crate::portal::RenderTargets>()
.add_systems(Startup, crate::portal::init_portal_placeholder)
.init_resource::<crate::surface::Surfaces>()
.add_systems(Startup, crate::surface::init_surface_pointer)
.add_systems(Startup, setup)
.add_systems(
PreUpdate,
(dispatch_react_messages, dispatch_react_requests),
)
.add_systems(
PreUpdate,
crate::surface::drive_surface_pointer
.before(bevy::picking::PickingSystems::ProcessInput),
)
.add_systems(
Update,
(
apply_js_ops,
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::scroll::collect_wheel_events
.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::anchor::position_anchored_nodes.after(apply_js_ops),
(
crate::canvas::update_canvas_surfaces.after(apply_js_ops),
collect_canvas_resize_events.after(apply_js_ops),
crate::cursor::drive_cursor_icon.after(apply_js_ops),
),
crate::portal::bind_portals.after(apply_js_ops),
crate::portal::drive_render_targets.after(crate::portal::bind_portals),
crate::surface::bind_surfaces.after(apply_js_ops),
crate::surface::drive_surfaces.after(crate::surface::bind_surfaces),
collect_surface_clicks,
collect_surface_pointer_events,
collect_surface_hover_events,
apply_surface_interaction_styles,
),
);
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),
);
if self.animations {
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));
}
}
}
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)]
struct UiRoot;
#[derive(Resource)]
struct ReactUiConfig {
default_font: Option<PathBuf>,
named_fonts: Vec<(String, PathBuf)>,
custom_cursors: Vec<(String, PathBuf, (u16, u16))>,
}
#[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>,
) {
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(),
});
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()
},
UiRoot,
))
.id();
commands.spawn((
Node {
position_type: PositionType::Absolute,
left: Val::Px(0.0),
top: Val::Px(0.0),
width: Val::Px(0.0),
height: Val::Px(0.0),
..default()
},
crate::anchor::AnchorLayer,
ChildOf(root),
));
let ops_rx = channels.ops_rx.take().expect("setup runs once");
commands.insert_resource(JsBridge::new(ops_rx, channels.outbound_tx.clone(), root));
}