use std::{fmt, path::PathBuf};
use crate::context::App;
use crate::prelude::{MonitorSelection, render::NannouCamera};
use crate::{geom::Point2, glam::Vec2, prelude::WindowResizeConstraints};
use bevy::{
camera::Hdr,
camera::RenderTarget,
camera::visibility::RenderLayers,
input::mouse::MouseWheel,
prelude::*,
render::extract_component::ExtractComponent,
window::{PresentMode, PrimaryWindow, WindowLevel, WindowRef},
};
pub struct Builder<'a, 'w, 's, M = ()> {
app: &'a App<'w, 's>,
window: bevy::window::Window,
camera: Option<Entity>,
light: Option<Entity>,
primary: bool,
user_functions: UserFunctions<M>,
clear_color: Option<Color>,
hdr: bool,
}
#[derive(Debug)]
pub(crate) struct UserFunctions<M> {
pub(crate) view: Option<View<M>>,
pub(crate) key_pressed: Option<KeyPressedFn<M>>,
pub(crate) key_released: Option<KeyReleasedFn<M>>,
pub(crate) received_character: Option<ReceivedCharacterFn<M>>,
pub(crate) mouse_moved: Option<MouseMovedFn<M>>,
pub(crate) mouse_pressed: Option<MousePressedFn<M>>,
pub(crate) mouse_released: Option<MouseReleasedFn<M>>,
pub(crate) mouse_entered: Option<MouseEnteredFn<M>>,
pub(crate) mouse_exited: Option<MouseExitedFn<M>>,
pub(crate) mouse_wheel: Option<MouseWheelFn<M>>,
pub(crate) moved: Option<MovedFn<M>>,
pub(crate) resized: Option<ResizedFn<M>>,
pub(crate) touch: Option<TouchFn<M>>,
pub(crate) hovered_file: Option<HoveredFileFn<M>>,
pub(crate) hovered_file_cancelled: Option<HoveredFileCancelledFn<M>>,
pub(crate) dropped_file: Option<DroppedFileFn<M>>,
pub(crate) focused: Option<FocusedFn<M>>,
pub(crate) unfocused: Option<UnfocusedFn<M>>,
pub(crate) closed: Option<ClosedFn<M>>,
}
impl<M> Default for UserFunctions<M> {
fn default() -> Self {
UserFunctions {
view: None,
key_pressed: None,
key_released: None,
received_character: None,
mouse_moved: None,
mouse_pressed: None,
mouse_released: None,
mouse_entered: None,
mouse_exited: None,
mouse_wheel: None,
moved: None,
resized: None,
touch: None,
hovered_file: None,
hovered_file_cancelled: None,
dropped_file: None,
focused: None,
unfocused: None,
closed: None,
}
}
}
impl<M> Clone for UserFunctions<M> {
fn clone(&self) -> Self {
UserFunctions {
view: self.view.clone(),
key_pressed: self.key_pressed,
key_released: self.key_released,
received_character: self.received_character,
mouse_moved: self.mouse_moved,
mouse_pressed: self.mouse_pressed,
mouse_released: self.mouse_released,
mouse_entered: self.mouse_entered,
mouse_exited: self.mouse_exited,
mouse_wheel: self.mouse_wheel,
moved: self.moved,
resized: self.resized,
touch: self.touch,
hovered_file: self.hovered_file,
hovered_file_cancelled: self.hovered_file_cancelled,
dropped_file: self.dropped_file,
focused: self.focused,
unfocused: self.unfocused,
closed: self.closed,
}
}
}
#[derive(Component, Deref, DerefMut, ExtractComponent)]
pub(crate) struct WindowUserFunctions<M: 'static>(pub(crate) UserFunctions<M>);
pub type ViewFn<Model> = fn(&App<'_, '_>, &Model);
pub type SketchFn = fn(&App<'_, '_>);
pub(crate) enum View<M> {
WithModel(ViewFn<M>),
Sketch(SketchFn),
}
impl<M> Clone for View<M> {
fn clone(&self) -> Self {
match self {
View::WithModel(f) => View::WithModel(*f),
View::Sketch(f) => View::Sketch(*f),
}
}
}
pub type KeyPressedFn<Model> = fn(&App<'_, '_>, &mut Model, KeyCode);
pub type KeyReleasedFn<Model> = fn(&App<'_, '_>, &mut Model, KeyCode);
pub type ReceivedCharacterFn<Model> = fn(&App<'_, '_>, &mut Model, char);
pub type MouseMovedFn<Model> = fn(&App<'_, '_>, &mut Model, Point2);
pub type MousePressedFn<Model> = fn(&App<'_, '_>, &mut Model, MouseButton);
pub type MouseReleasedFn<Model> = fn(&App<'_, '_>, &mut Model, MouseButton);
pub type MouseEnteredFn<Model> = fn(&App<'_, '_>, &mut Model);
pub type MouseExitedFn<Model> = fn(&App<'_, '_>, &mut Model);
pub type MouseWheelFn<Model> = fn(&App<'_, '_>, &mut Model, MouseWheel);
pub type MovedFn<Model> = fn(&App<'_, '_>, &mut Model, IVec2);
pub type ResizedFn<Model> = fn(&App<'_, '_>, &mut Model, Vec2);
pub type TouchFn<Model> = fn(&App<'_, '_>, &mut Model, TouchInput);
pub type HoveredFileFn<Model> = fn(&App<'_, '_>, &mut Model, PathBuf);
pub type HoveredFileCancelledFn<Model> = fn(&App<'_, '_>, &mut Model);
pub type DroppedFileFn<Model> = fn(&App<'_, '_>, &mut Model, PathBuf);
pub type FocusedFn<Model> = fn(&App<'_, '_>, &mut Model);
pub type UnfocusedFn<Model> = fn(&App<'_, '_>, &mut Model);
pub type ClosedFn<Model> = fn(&App<'_, '_>, &mut Model);
pub const DEFAULT_PRESENT_MODE: PresentMode = PresentMode::Mailbox;
impl<'a, 'w, 's, M> Builder<'a, 'w, 's, M>
where
M: 'static,
{
pub fn new(app: &'a App<'w, 's>) -> Self {
Builder {
app,
window: bevy::window::Window {
present_mode: DEFAULT_PRESENT_MODE,
..bevy::window::Window::default()
},
camera: None,
light: None,
primary: false,
user_functions: UserFunctions::<M>::default(),
clear_color: None,
hdr: false,
}
}
pub fn window(mut self, window: bevy::window::Window) -> Self {
self.window = window;
self
}
pub fn sketch(mut self, sketch_fn: SketchFn) -> Self {
self.user_functions.view = Some(View::Sketch(sketch_fn));
self
}
pub fn view(mut self, view_fn: ViewFn<M>) -> Self {
self.user_functions.view = Some(View::WithModel(view_fn));
self
}
pub fn clear_color<C>(mut self, color: C) -> Self
where
C: Into<Color>,
{
let color = color.into();
self.clear_color = Some(color);
self
}
pub fn key_pressed(mut self, f: KeyPressedFn<M>) -> Self {
self.user_functions.key_pressed = Some(f);
self
}
pub fn key_released(mut self, f: KeyReleasedFn<M>) -> Self {
self.user_functions.key_released = Some(f);
self
}
pub fn received_character(mut self, f: ReceivedCharacterFn<M>) -> Self {
self.user_functions.received_character = Some(f);
self
}
pub fn mouse_moved(mut self, f: MouseMovedFn<M>) -> Self {
self.user_functions.mouse_moved = Some(f);
self
}
pub fn mouse_pressed(mut self, f: MousePressedFn<M>) -> Self {
self.user_functions.mouse_pressed = Some(f);
self
}
pub fn mouse_released(mut self, f: MouseReleasedFn<M>) -> Self {
self.user_functions.mouse_released = Some(f);
self
}
pub fn mouse_wheel(mut self, f: MouseWheelFn<M>) -> Self {
self.user_functions.mouse_wheel = Some(f);
self
}
pub fn mouse_entered(mut self, f: MouseEnteredFn<M>) -> Self {
self.user_functions.mouse_entered = Some(f);
self
}
pub fn mouse_exited(mut self, f: MouseExitedFn<M>) -> Self {
self.user_functions.mouse_exited = Some(f);
self
}
pub fn touch(mut self, f: TouchFn<M>) -> Self {
self.user_functions.touch = Some(f);
self
}
pub fn moved(mut self, f: MovedFn<M>) -> Self {
self.user_functions.moved = Some(f);
self
}
pub fn resized(mut self, f: ResizedFn<M>) -> Self {
self.user_functions.resized = Some(f);
self
}
pub fn hovered_file(mut self, f: HoveredFileFn<M>) -> Self {
self.user_functions.hovered_file = Some(f);
self
}
pub fn hovered_file_cancelled(mut self, f: HoveredFileCancelledFn<M>) -> Self {
self.user_functions.hovered_file_cancelled = Some(f);
self
}
pub fn dropped_file(mut self, f: DroppedFileFn<M>) -> Self {
self.user_functions.dropped_file = Some(f);
self
}
pub fn focused(mut self, f: FocusedFn<M>) -> Self {
self.user_functions.focused = Some(f);
self
}
pub fn unfocused(mut self, f: UnfocusedFn<M>) -> Self {
self.user_functions.unfocused = Some(f);
self
}
pub fn closed(mut self, f: ClosedFn<M>) -> Self {
self.user_functions.closed = Some(f);
self
}
pub fn hdr(mut self, hdr: bool) -> Self {
self.hdr = hdr;
self
}
pub fn build(self) -> Entity {
let Builder {
app,
window,
camera,
light,
primary,
user_functions,
clear_color,
hdr,
} = self;
if cfg!(target_arch = "wasm32") && !primary {
panic!("Non-primary windows are not supported on wasm");
}
let layer = RenderLayers::layer(app.window_count());
let user_functions = WindowUserFunctions(user_functions);
let window_for_cache = window.clone();
let half_z_range = default_camera_half_z_range(&window_for_cache);
#[cfg(target_arch = "wasm32")]
let existing_primary = Some(app.main_window().id());
#[cfg(not(target_arch = "wasm32"))]
let existing_primary: Option<Entity> = None;
let window_entity = app.command_scope(move |mut commands| {
let window_entity = match existing_primary {
Some(entity) => {
commands
.entity(entity)
.insert((window, user_functions, layer.clone()));
entity
}
None => {
let mut window = commands.spawn((window, user_functions, layer.clone()));
if primary {
window.insert(PrimaryWindow);
}
window.id()
}
};
match camera {
Some(camera) => {
commands.entity(camera).insert((
RenderTarget::Window(WindowRef::Entity(window_entity)),
layer.clone(),
));
}
None => {
let mut camera = commands.spawn((
Camera {
clear_color: clear_color
.map(ClearColorConfig::Custom)
.unwrap_or(ClearColorConfig::None),
..default()
},
RenderTarget::Window(WindowRef::Entity(window_entity)),
Transform::from_xyz(0.0, 0.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
Projection::Orthographic(OrthographicProjection {
near: -half_z_range,
far: half_z_range,
..OrthographicProjection::default_3d()
}),
layer.clone(),
NannouCamera,
WindowDefaultCamera,
));
if hdr {
camera.insert(Hdr);
}
}
}
if let Some(light) = light {
commands.entity(light).insert(layer.clone());
}
window_entity
});
app.record_pending_window(window_entity, primary, window_for_cache);
window_entity
}
fn map_window<F>(self, map: F) -> Self
where
F: FnOnce(bevy::window::Window) -> bevy::window::Window,
{
Builder {
window: map(self.window),
..self
}
}
pub fn camera(mut self, camera: Entity) -> Self {
self.camera = Some(camera);
self
}
pub fn light(mut self, light: Entity) -> Self {
self.light = Some(light);
self
}
pub fn size(self, width: u32, height: u32) -> Self {
self.map_window(|mut w| {
w.resolution.set(width as f32, height as f32);
w
})
}
pub fn min_size(self, width: f32, height: f32) -> Self {
self.map_window(|mut w| {
w.resize_constraints = WindowResizeConstraints {
min_width: width,
min_height: height,
..w.resize_constraints
};
w
})
}
pub fn max_size(self, width: f32, height: f32) -> Self {
self.map_window(|mut w| {
w.resize_constraints = WindowResizeConstraints {
max_width: width,
max_height: height,
..w.resize_constraints
};
w
})
}
pub fn size_pixels(self, width: u32, height: u32) -> Self {
self.map_window(|mut w| {
w.resolution.set_physical_resolution(width, height);
w
})
}
pub fn present_mode(self, present_mode: PresentMode) -> Self {
self.map_window(|mut w| {
w.present_mode = present_mode;
w
})
}
pub fn resizable(self, resizable: bool) -> Self {
self.map_window(|mut w| {
w.resizable = resizable;
w
})
}
pub fn title<T>(self, title: T) -> Self
where
T: Into<String>,
{
self.map_window(|mut w| {
w.title = title.into();
w
})
}
pub fn primary(mut self) -> Self {
self.primary = true;
self
}
pub fn fullscreen(self) -> Self {
self.map_window(|mut w| {
w.position = WindowPosition::Centered(MonitorSelection::Primary);
w
})
}
pub fn monitor(self, monitor: MonitorSelection) -> Self {
self.map_window(|mut w| {
w.position = WindowPosition::Centered(monitor);
w
})
}
pub fn maximized(self, maximized: bool) -> Self {
self.map_window(|mut w| {
w.set_maximized(maximized);
w
})
}
pub fn visible(self, visible: bool) -> Self {
self.map_window(|mut w| {
w.visible = visible;
w
})
}
pub fn transparent(self, transparent: bool) -> Self {
self.map_window(|mut w| {
w.transparent = transparent;
w
})
}
pub fn decorations(self, decorations: bool) -> Self {
self.map_window(|mut w| {
w.decorations = decorations;
w
})
}
pub fn always_on_top(self, always_on_top: bool) -> Self {
self.map_window(|mut w| {
w.window_level = if always_on_top {
WindowLevel::AlwaysOnTop
} else {
WindowLevel::Normal
};
w
})
}
}
impl<M> fmt::Debug for View<M> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let variant = match *self {
View::WithModel(ref v) => format!("WithModel({:?})", v),
View::Sketch(_) => "Sketch".to_string(),
};
write!(f, "View::{}", variant)
}
}
#[derive(Component)]
pub(crate) struct WindowDefaultCamera;
fn default_camera_half_z_range(window: &bevy::window::Window) -> f32 {
window.width().max(window.height())
}
pub(crate) fn update_default_camera_z_range(
mut cameras: Query<(&mut Projection, &RenderTarget), With<WindowDefaultCamera>>,
windows: Query<&bevy::window::Window>,
primary_window: Query<Entity, With<PrimaryWindow>>,
) {
for (mut projection, target) in cameras.iter_mut() {
let window = match target {
RenderTarget::Window(WindowRef::Entity(entity)) => *entity,
RenderTarget::Window(WindowRef::Primary) => match primary_window.single() {
Ok(entity) => entity,
Err(_) => continue,
},
_ => continue,
};
let Ok(window) = windows.get(window) else {
continue;
};
let half_z_range = default_camera_half_z_range(window);
let Projection::Orthographic(ortho) = projection.as_ref() else {
continue;
};
if ortho.near != -half_z_range || ortho.far != half_z_range {
if let Projection::Orthographic(ortho) = &mut *projection {
ortho.near = -half_z_range;
ortho.far = half_z_range;
}
}
}
}