use crate::{picking_backend::compute_letterboxed_transform, prelude::*};
use bevy::{
prelude::*,
window::{PrimaryWindow, SystemCursorIcon},
winit::cursor::CursorIcon,
};
#[derive(Default, Debug, Component, Reflect, PartialEq, Clone)]
pub struct WindowState {
position: Vec2,
drag_offset: Vec2,
}
#[derive(Widget, Component, Reflect, PartialEq, Clone)]
#[auto_update(render)]
#[props(WoodpeckerWindow, PassedChildren)]
#[state(WindowState)]
#[context(WindowingContext)]
#[require(WoodpeckerStyle, PassedChildren, WidgetRender = WidgetRender::Quad, WidgetChildren, Pickable, Focusable)]
pub struct WoodpeckerWindow {
pub title: String,
pub initial_position: Vec2,
pub window_styles: WoodpeckerStyle,
pub title_styles: WoodpeckerStyle,
pub divider_styles: WoodpeckerStyle,
pub children_styles: WoodpeckerStyle,
}
impl Default for WoodpeckerWindow {
fn default() -> Self {
Self {
title: Default::default(),
initial_position: Default::default(),
window_styles: WoodpeckerStyle {
background_color: colors::BACKGROUND,
border_color: colors::DARK_BACKGROUND,
border: Edge::all(2.0),
border_radius: Corner::all(5.0),
flex_direction: WidgetFlexDirection::Column,
..Default::default()
},
title_styles: WoodpeckerStyle {
background_color: colors::DARK_BACKGROUND,
height: Units::Pixels(40.0),
width: Units::Percentage(100.0),
align_items: Some(WidgetAlignItems::Center),
padding: Edge::all(0.0).left(10.0).right(10.0),
margin: Edge::all(0.0).left(-2.0).right(-2.0).top(-2.0),
border_radius: Corner::all(0.0).top_left(5.0).top_right(5.0),
..Default::default()
},
divider_styles: WoodpeckerStyle {
..Default::default()
},
children_styles: WoodpeckerStyle {
..Default::default()
},
}
}
}
fn render(
mut commands: Commands,
current_widget: Res<CurrentWidget>,
mut hooks: ResMut<HookHelper>,
mut query: Query<(
&WoodpeckerWindow,
&mut WoodpeckerStyle,
&mut WidgetChildren,
&WidgetLayout,
&PassedChildren,
Option<&TitleChildren>,
)>,
state_query: Query<&mut WindowState>,
mut context_query: Query<&mut WindowingContext>,
) {
let Ok((window, mut styles, mut children, layout, passed_children, title_children)) =
query.get_mut(**current_widget)
else {
return;
};
let state_entity = hooks.use_state(
&mut commands,
*current_widget,
WindowState {
position: window.initial_position,
drag_offset: Vec2::new(0.0, 0.0),
},
);
let Ok(state) = state_query.get(state_entity) else {
return;
};
let context_entity =
hooks.use_context(&mut commands, *current_widget, WindowingContext::default());
let Ok(mut context) = context_query.get_mut(context_entity) else {
return;
};
let z_index = context.get_or_add(current_widget.entity());
*styles = WoodpeckerStyle {
position: WidgetPosition::Fixed,
left: state.position.x.into(),
top: state.position.y.into(),
z_index: Some(WidgetZ::Global(z_index)),
..window.window_styles
};
*children = WidgetChildren::default();
let current_widget = *current_widget;
children
.observe(
current_widget,
move |trigger: Trigger<WidgetFocus>,
mut context_query: Query<&mut WindowingContext>| {
let Ok(mut context) = context_query.get_mut(context_entity) else {
return;
};
context.shift_to_top(trigger.target);
},
)
.add::<Element>((
Element,
WoodpeckerStyle {
width: layout.width().into(),
..window.title_styles
},
if let Some(title_children) = title_children.as_ref() {
title_children.0.clone()
} else {
WidgetChildren::default().with_child::<Element>((
Element,
WoodpeckerStyle {
font_size: 14.0,
text_wrap: TextWrap::None,
..Default::default()
},
WidgetRender::Text {
content: window.title.clone(),
},
))
},
WidgetRender::Quad,
Pickable::default(),
))
.observe(
current_widget,
move |_trigger: Trigger<Pointer<Pressed>>,
mut context_query: Query<&mut WindowingContext>| {
let Ok(mut context) = context_query.get_mut(context_entity) else {
return;
};
context.shift_to_top(current_widget.entity());
},
)
.observe(
current_widget,
move |_trigger: Trigger<Pointer<Over>>,
mut commands: Commands,
entity: Single<Entity, With<PrimaryWindow>>| {
commands
.entity(*entity)
.insert(CursorIcon::from(SystemCursorIcon::Grab));
},
)
.observe(
current_widget,
move |_trigger: Trigger<Pointer<Out>>,
mut commands: Commands,
entity: Single<Entity, With<PrimaryWindow>>| {
commands
.entity(*entity)
.insert(CursorIcon::from(SystemCursorIcon::Default));
},
)
.observe(
current_widget,
move |_trigger: Trigger<Pointer<DragEnd>>,
mut commands: Commands,
entity: Single<Entity, With<PrimaryWindow>>| {
commands
.entity(*entity)
.insert(CursorIcon::from(SystemCursorIcon::Grab));
},
)
.observe(
current_widget,
move |trigger: Trigger<Pointer<DragStart>>,
mut commands: Commands,
mut state_query: Query<&mut WindowState>,
window: Single<(Entity, &Window), With<PrimaryWindow>>,
camera: Query<&Camera, With<WoodpeckerView>>,
mut context_query: Query<&mut WindowingContext>| {
let Ok(mut state) = state_query.get_mut(state_entity) else {
return;
};
let Some(camera) = camera.iter().next() else {
return;
};
let (offset, size, _scale) = compute_letterboxed_transform(
window.1.size(),
camera.logical_target_size().unwrap(),
);
let cursor_pos_world = ((trigger.pointer_location.position - offset) / size)
* camera.logical_target_size().unwrap();
state.drag_offset = state.position - cursor_pos_world;
let Ok(mut context) = context_query.get_mut(context_entity) else {
return;
};
commands
.entity(window.0)
.insert(CursorIcon::from(SystemCursorIcon::Grabbing));
context.shift_to_top(current_widget.entity());
},
)
.observe(
current_widget,
move |trigger: Trigger<Pointer<Drag>>,
mut state_query: Query<&mut WindowState>,
window: Single<(Entity, &Window), With<PrimaryWindow>>,
camera: Query<&Camera, With<WoodpeckerView>>,
mut context_query: Query<&mut WindowingContext>| {
let Ok(mut state) = state_query.get_mut(state_entity) else {
return;
};
let Some(camera) = camera.iter().next() else {
return;
};
let (offset, size, _scale) = compute_letterboxed_transform(
window.1.size(),
camera.logical_target_size().unwrap(),
);
let cursor_pos_world = ((trigger.pointer_location.position - offset) / size)
* camera.logical_target_size().unwrap();
state.position = cursor_pos_world + state.drag_offset;
let Ok(mut context) = context_query.get_mut(context_entity) else {
return;
};
context.shift_to_top(current_widget.entity());
},
)
.add::<Element>((Element, window.children_styles, passed_children.0.clone()));
children.apply(current_widget.as_parent());
}