use bevy::input::ButtonState;
use bevy::input::gamepad::{Gamepad, GamepadButton};
use bevy::input::keyboard::{KeyCode, KeyboardInput};
use bevy::input_focus::{FocusedInput, InputFocus, InputFocusVisible};
use bevy::picking::events::{
PointerCancel, PointerClick, PointerDrag, PointerDragEnd, PointerDragStart, PointerPress,
PointerRelease,
};
use bevy::picking::pointer::PointerButton;
use bevy::prelude::*;
use bevy::ui::{InteractionDisabled, Pressed};
use bevy::ui_widgets::ValueChange;
use crate::model::{
PAD_REPEAT_DELAY, PAD_REPEAT_RATE, SPLIT_KEY_STEP_PX, repeat_steps, split_clamp, split_drag,
split_mins,
};
use crate::nav::show_focus;
use crate::skin::Role;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Reflect)]
#[reflect(Default, Debug, PartialEq, Hash, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SplitAxis {
#[default]
Horizontal,
Vertical,
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
#[require(SplitFraction, SplitMin)]
pub struct Split {
axis: SplitAxis,
}
impl Split {
pub(crate) fn new(axis: SplitAxis) -> Self {
Self { axis }
}
pub fn axis(&self) -> SplitAxis {
self.axis
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SplitFraction(pub f32);
impl Default for SplitFraction {
fn default() -> Self {
Self(0.5)
}
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SplitMin {
pub first: f32,
pub second: f32,
}
impl SplitMin {
pub fn new(first: f32, second: f32) -> Self {
Self { first, second }
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
pub struct SplitDivider {
pub(crate) axis: SplitAxis,
}
impl SplitDivider {
pub(crate) fn new(axis: SplitAxis) -> Self {
Self { axis }
}
pub fn axis(&self) -> SplitAxis {
self.axis
}
}
#[derive(Component, Clone, Copy, Debug)]
pub(crate) struct SplitGrab(f32);
#[derive(Component, Clone, Copy, Debug)]
pub(crate) struct SplitSaved(f32);
#[derive(EntityEvent, Clone, Copy, Debug)]
pub(crate) struct ToggleSplit {
#[event_target]
pub(crate) entity: Entity,
}
struct Reading {
root: Entity,
axis: SplitAxis,
fraction: f32,
min: SplitMin,
extent: f32,
disabled: bool,
}
impl Reading {
fn moved(&self, delta: f32) -> f32 {
split_drag(
self.fraction,
delta,
self.extent,
self.min.first,
self.min.second,
)
}
fn end(&self, large: bool) -> f32 {
let to = if large { 1.0 } else { 0.0 };
split_clamp(to, self.extent, self.min.first, self.min.second)
}
}
type Dividers = (
&'static ChildOf,
&'static SplitDivider,
&'static ComputedNode,
Has<InteractionDisabled>,
);
type Pointed = (Has<InteractionDisabled>, Has<Pressed>, Has<SplitGrab>);
type Roots = (
&'static SplitFraction,
&'static SplitMin,
&'static ComputedNode,
);
#[derive(bevy::ecs::system::SystemParam)]
pub struct SplitGeometry<'w, 's> {
dividers: Query<'w, 's, Dividers>,
roots: Query<'w, 's, Roots>,
}
impl SplitGeometry<'_, '_> {
fn enabled(&self, divider: Entity) -> Option<SplitAxis> {
let (_, divider, _, disabled) = self.dividers.get(divider).ok()?;
(!disabled).then_some(divider.axis)
}
fn read(&self, divider: Entity) -> Option<Reading> {
let Ok((parent, divider_axis, node, disabled)) = self.dividers.get(divider) else {
debug!(
"split divider {divider} is not read: it lacks a `ChildOf`, `SplitDivider` or \
`ComputedNode` (a node that has not been laid out has no `ComputedNode`)"
);
return None;
};
let Ok((fraction, min, root_node)) = self.roots.get(parent.parent()) else {
debug!(
"split divider {divider} is not read: its parent {} lacks a `SplitFraction`, \
`SplitMin` or `ComputedNode`",
parent.parent()
);
return None;
};
let along = |size: Vec2, axis: SplitAxis| match axis {
SplitAxis::Horizontal => size.x,
SplitAxis::Vertical => size.y,
};
let axis = divider_axis.axis;
let whole = along(root_node.size(), axis) * root_node.inverse_scale_factor();
let bar = along(node.size(), axis) * node.inverse_scale_factor();
Some(Reading {
root: parent.parent(),
axis,
fraction: fraction.0,
min: *min,
extent: (whole - bar).max(0.0),
disabled,
})
}
}
fn report(commands: &mut Commands, reading: &Reading, to: f32, is_final: bool) {
if to != reading.fraction {
commands.trigger(ValueChange {
source: reading.root,
value: to,
is_final,
});
}
}
pub(crate) fn split_self_update(
change: On<ValueChange<f32>>,
mut splits: Query<&mut SplitFraction, With<Split>>,
) {
if let Ok(mut fraction) = splits.get_mut(change.source) {
fraction.set_if_neq(SplitFraction(change.value));
}
}
pub(crate) fn divider_on_press(
mut press: On<PointerPress>,
dividers: Query<(Has<InteractionDisabled>, Has<Pressed>), With<SplitDivider>>,
mut commands: Commands,
) {
press.propagate(false);
if press.button == PointerButton::Primary
&& dividers
.get(press.entity)
.is_ok_and(|(disabled, pressed)| !disabled && !pressed)
{
commands.entity(press.entity).insert(Pressed);
}
}
pub(crate) fn divider_on_release(mut release: On<PointerRelease>, mut commands: Commands) {
release.propagate(false);
commands.entity(release.entity).remove::<Pressed>();
}
pub(crate) fn divider_on_drag_start(
mut start: On<PointerDragStart>,
geometry: SplitGeometry,
mut commands: Commands,
) {
start.propagate(false);
if start.button != PointerButton::Primary {
debug!(
"split divider {} is not dragged: the drag is by {:?}, not the primary button",
start.entity, start.button
);
return;
}
if let Some(reading) = geometry.read(start.entity) {
if reading.disabled {
debug!(
"split divider {} is not dragged: it is `InteractionDisabled`",
start.entity
);
return;
}
commands
.entity(start.entity)
.insert((Pressed, SplitGrab(reading.fraction)));
}
}
pub(crate) fn divider_on_drag(
mut drag: On<PointerDrag>,
ui_scale: Res<UiScale>,
grabs: Query<&SplitGrab>,
geometry: SplitGeometry,
mut commands: Commands,
) {
drag.propagate(false);
let Ok(grab) = grabs.get(drag.entity) else {
debug!(
"split divider {} ignores a drag: no drag began on it (no `SplitGrab`)",
drag.entity
);
return;
};
let Some(reading) = geometry.read(drag.entity) else {
return;
};
let along = match reading.axis {
SplitAxis::Horizontal => drag.distance.x,
SplitAxis::Vertical => drag.distance.y,
} / ui_scale.0;
let to = split_drag(
grab.0,
along,
reading.extent,
reading.min.first,
reading.min.second,
);
debug!(
"split divider {} dragged {along} layout px (pointer distance {}) of room {}: fraction {} to {to}",
drag.entity, drag.distance, reading.extent, reading.fraction
);
report(&mut commands, &reading, to, false);
}
pub(crate) fn divider_on_drag_end(
mut end: On<PointerDragEnd>,
grabs: Query<(), With<SplitGrab>>,
geometry: SplitGeometry,
mut commands: Commands,
) {
end.propagate(false);
commands.entity(end.entity).remove::<(Pressed, SplitGrab)>();
if grabs.contains(end.entity)
&& let Some(reading) = geometry.read(end.entity)
{
commands.trigger(ValueChange {
source: reading.root,
value: reading.fraction,
is_final: true,
});
}
}
pub(crate) fn divider_on_cancel(mut cancel: On<PointerCancel>, mut commands: Commands) {
cancel.propagate(false);
commands
.entity(cancel.entity)
.remove::<(Pressed, SplitGrab)>();
}
pub(crate) fn divider_on_click(
mut click: On<PointerClick>,
dividers: Query<Pointed, With<SplitDivider>>,
mut commands: Commands,
) {
click.propagate(false);
if click.button == PointerButton::Primary
&& click.count == 2
&& dividers
.get(click.entity)
.is_ok_and(|(disabled, pressed, dragged)| !disabled && pressed && !dragged)
{
commands.trigger(ToggleSplit {
entity: click.entity,
});
}
}
pub(crate) fn divider_on_key_input(
mut input: On<FocusedInput<KeyboardInput>>,
geometry: SplitGeometry,
mut commands: Commands,
) {
if input.input.state != ButtonState::Pressed {
return;
}
let Some(reading) = geometry.read(input.focused_entity) else {
return;
};
let step = |sign: f32| reading.moved(sign * SPLIT_KEY_STEP_PX);
let key = input.input.key_code;
let to = match (reading.axis, key) {
(SplitAxis::Horizontal, KeyCode::ArrowLeft) | (SplitAxis::Vertical, KeyCode::ArrowUp) => {
step(-1.0)
}
(SplitAxis::Horizontal, KeyCode::ArrowRight)
| (SplitAxis::Vertical, KeyCode::ArrowDown) => step(1.0),
(_, KeyCode::Home) => reading.end(false),
(_, KeyCode::End) => reading.end(true),
(_, KeyCode::Enter) => {
input.propagate(false);
if !reading.disabled && !input.input.repeat {
commands.trigger(ToggleSplit {
entity: input.focused_entity,
});
}
return;
}
_ => return,
};
input.propagate(false);
if !reading.disabled {
report(&mut commands, &reading, to, true);
}
}
pub(crate) fn toggle_split(
event: On<ToggleSplit>,
geometry: SplitGeometry,
saved: Query<&SplitSaved>,
mut commands: Commands,
) {
let Some(reading) = geometry.read(event.entity) else {
return;
};
if reading.disabled || reading.extent <= 0.0 {
return;
}
let smallest = reading.end(false);
let at_smallest = (reading.fraction - smallest) * reading.extent < 0.5;
let to = if at_smallest {
let back = saved.get(reading.root).map_or(0.5, |s| s.0);
commands.entity(reading.root).remove::<SplitSaved>();
split_clamp(back, reading.extent, reading.min.first, reading.min.second)
} else {
commands
.entity(reading.root)
.insert(SplitSaved(reading.fraction));
smallest
};
report(&mut commands, &reading, to, true);
}
pub fn split_by_pad(
time: Res<Time<Real>>,
focus: Res<InputFocus>,
mut shown: ResMut<InputFocusVisible>,
gamepads: Query<&Gamepad>,
geometry: SplitGeometry,
mut held: Local<(i8, f32)>,
mut commands: Commands,
) {
let down = |button| gamepads.iter().any(|g| g.pressed(button));
let Some((divider, axis)) = focus
.get()
.and_then(|f| geometry.enabled(f).map(|a| (f, a)))
else {
*held = (0, 0.0);
return;
};
if gamepads
.iter()
.any(|g| g.just_pressed(GamepadButton::South))
{
show_focus(&mut shown);
commands.trigger(ToggleSplit { entity: divider });
}
let (plus, minus, across) = match axis {
SplitAxis::Horizontal => (
GamepadButton::DPadRight,
GamepadButton::DPadLeft,
[GamepadButton::DPadUp, GamepadButton::DPadDown],
),
SplitAxis::Vertical => (
GamepadButton::DPadDown,
GamepadButton::DPadUp,
[GamepadButton::DPadLeft, GamepadButton::DPadRight],
),
};
let direction = down(plus) as i8 - down(minus) as i8;
if direction == 0 || across.into_iter().any(down) {
*held = (0, 0.0);
return;
}
let steps = if held.0 == direction {
let before = held.1;
held.1 += time.delta_secs();
repeat_steps(before, held.1, PAD_REPEAT_DELAY, PAD_REPEAT_RATE)
} else {
*held = (direction, 0.0);
1
};
if steps == 0 {
return;
}
show_focus(&mut shown);
if let Some(reading) = geometry.read(divider) {
let to = reading.moved(f32::from(direction) * steps as f32 * SPLIT_KEY_STEP_PX);
report(&mut commands, &reading, to, true);
}
}
pub fn update_splits(
splits: Query<(&Split, &SplitFraction, &SplitMin, &ComputedNode, &Children)>,
dividers: Query<&ComputedNode, With<SplitDivider>>,
mut parts: Query<(&Role, &mut Node)>,
) {
for (split, fraction, min, node, children) in &splits {
let along = |size: Vec2| match split.axis {
SplitAxis::Horizontal => size.x,
SplitAxis::Vertical => size.y,
};
let bar = children
.iter()
.find_map(|c| dividers.get(c).ok())
.map_or(0.0, |d| along(d.size()) * d.inverse_scale_factor());
let extent = (along(node.size()) * node.inverse_scale_factor() - bar).max(0.0);
let first = split_clamp(fraction.0, extent, min.first, min.second);
let (min_first, min_second) = split_mins(extent, min.first, min.second);
let panes: Vec<Entity> = children
.iter()
.filter(|&c| parts.get(c).is_ok_and(|(role, _)| *role == Role::SplitPane))
.collect();
for (pane, (grow, least)) in panes
.into_iter()
.zip([(first, min_first), (1.0 - first, min_second)])
{
let Ok((_, mut node)) = parts.get_mut(pane) else {
continue;
};
let least = Val::Px(least);
let (min_width, min_height) = match split.axis {
SplitAxis::Horizontal => (least, Val::Px(0.0)),
SplitAxis::Vertical => (Val::Px(0.0), least),
};
if node.flex_grow != grow
|| node.min_width != min_width
|| node.min_height != min_height
{
node.flex_grow = grow;
node.min_width = min_width;
node.min_height = min_height;
}
}
}
}