use bevy::camera::primitives::MeshAabb as _;
use bevy::prelude::*;
use super::components::*;
use super::cursor::*;
use super::pipeline::*;
use super::*;
use crate::common::{WebviewSize, WebviewSource};
use crate::drag::{DragState, DraggableRegions, DraggingState, InteractionEndPending};
use crate::system_param::pointer::WebviewPointer;
use crate::webview::WebviewSet;
pub struct ResizePlugin;
impl Plugin for ResizePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<ResizeState>()
.init_resource::<SystemCursorOverride>()
.add_systems(
Update,
(cursor_hover_system, resize_tracking_system).in_set(WebviewSet::ResizeInteraction),
)
.add_systems(
Update,
(
(init_resizable_system, pending_basis_init_system),
derive_pipeline_system,
(apply_display_to_mesh_system, apply_display_to_sprite_system),
)
.chain()
.in_set(WebviewSet::DerivePipeline),
)
.add_systems(Update, attach_resize_observers);
}
}
fn attach_resize_observers(
mut commands: Commands,
webviews: Query<
Entity,
(
Added<WebviewSource>,
With<Transform>,
With<WebviewResizable>,
Or<(With<Mesh3d>, With<Mesh2d>)>,
),
>,
) {
for entity in webviews.iter() {
commands.entity(entity).observe(on_resizable_press);
}
}
#[allow(clippy::too_many_arguments)]
fn on_resizable_press(
trigger: On<Pointer<Press>>,
mut resize_state: ResMut<ResizeState>,
mut drag_state: ResMut<DragState>,
mut commands: Commands,
pointer: WebviewPointer,
regions_q: Query<(&DraggableRegions, &WebviewResizable, &WebviewSize)>,
transforms_q: Query<(&GlobalTransform, &Transform, &DisplaySize), With<WebviewSource>>,
cameras_q: Query<(&Camera, &GlobalTransform)>,
keyboard: Res<ButtonInput<KeyCode>>,
#[cfg(not(target_os = "windows"))] browsers: NonSend<bevy_cef_core::prelude::Browsers>,
#[cfg(target_os = "windows")] browsers: Res<bevy_cef_core::prelude::BrowsersProxy>,
) {
if resize_state.is_resizing() || drag_state.is_dragging() {
return;
}
let Some((webview, pixel_pos, camera_entity)) = pointer.pos_from_trigger_raw(&trigger) else {
return;
};
let Ok((regions, resizable, webview_size)) = regions_q.get(webview) else {
return;
};
let frame = ResizeFrame {
width: webview_size.0.x as u32,
height: webview_size.0.y as u32,
edge_thickness: resizable.edge_thickness,
};
let hit = classify_hit(regions, Some(&frame), pixel_pos);
match hit {
HitResult::Resize(zone) => {
let Ok((webview_gtf, webview_tf, display_size)) = transforms_q.get(webview) else {
return;
};
let Ok((cam, cam_gtf)) = cameras_q.get(camera_entity) else {
return;
};
let viewport_pos = trigger.pointer_location.position;
let Ok(ray) = cam.viewport_to_world(cam_gtf, viewport_pos) else {
return;
};
let plane_origin = webview_gtf.translation();
let plane_normal = webview_gtf.forward();
let Some(t) = ray.intersect_plane(plane_origin, InfinitePlane3d::new(plane_normal))
else {
return;
};
let start_hit_world = ray.origin + ray.direction * t;
let u_axis = webview_gtf.right().as_vec3();
let v_axis = webview_gtf.up().as_vec3();
let aspect_lock_mode = resizable.aspect_lock;
*resize_state = ResizeState::Resizing {
webview,
zone,
start_display_size: display_size.0,
start_translation: webview_tf.translation,
start_hit_world,
plane_origin,
plane_normal,
camera: camera_entity,
u_axis,
v_axis,
aspect_lock_mode,
};
#[cfg(not(target_os = "windows"))]
browsers.send_mouse_move(
&webview,
std::iter::empty::<&MouseButton>(),
pixel_pos,
true,
);
#[cfg(target_os = "windows")]
browsers.send_mouse_move(&webview, &[], pixel_pos, true);
}
HitResult::Drag => {
let Ok((webview_gtf, webview_tf, _display_size)) = transforms_q.get(webview) else {
return;
};
let Ok((cam, cam_gtf)) = cameras_q.get(camera_entity) else {
return;
};
let viewport_pos = trigger.pointer_location.position;
let Ok(ray) = cam.viewport_to_world(cam_gtf, viewport_pos) else {
return;
};
let plane_origin = webview_gtf.translation();
let plane_normal = webview_gtf.forward();
let Some(t) = ray.intersect_plane(plane_origin, InfinitePlane3d::new(plane_normal))
else {
return;
};
let start_hit = ray.origin + ray.direction * t;
*drag_state = DragState::Dragging { webview };
commands.entity(webview).insert(DraggingState {
camera: camera_entity,
start_hit,
start_translation: webview_tf.translation,
plane_normal,
plane_origin,
});
#[cfg(not(target_os = "windows"))]
browsers.send_mouse_move(
&webview,
std::iter::empty::<&MouseButton>(),
pixel_pos,
true,
);
#[cfg(target_os = "windows")]
browsers.send_mouse_move(&webview, &[], pixel_pos, true);
}
HitResult::None => {
}
}
let _ = &keyboard;
}
fn resize_tracking_system(
mut resize_state: ResMut<ResizeState>,
mouse_button: Res<ButtonInput<MouseButton>>,
keyboard: Res<ButtonInput<KeyCode>>,
mut commands: Commands,
windows: Query<&Window>,
mut webviews: Query<(
&mut Transform,
&mut DisplaySize,
&WebviewResizable,
&BaseRenderScale,
&QualityMultiplier,
)>,
cameras_q: Query<(&Camera, &GlobalTransform)>,
) {
let ResizeState::Resizing {
webview,
zone,
start_display_size,
start_translation,
start_hit_world,
plane_origin,
plane_normal,
camera,
u_axis,
v_axis,
aspect_lock_mode,
} = &*resize_state
else {
return;
};
let webview = *webview;
let zone = *zone;
let start_display_size = *start_display_size;
let start_translation = *start_translation;
let start_hit_world = *start_hit_world;
let plane_origin = *plane_origin;
let plane_normal = *plane_normal;
let camera = *camera;
let u_axis = *u_axis;
let v_axis = *v_axis;
let aspect_lock_mode = *aspect_lock_mode;
if !mouse_button.pressed(MouseButton::Left) {
*resize_state = ResizeState::Idle;
commands.insert_resource(InteractionEndPending {
webview: Some(webview),
});
return;
}
let Some(cursor) = windows.iter().find_map(|w| w.cursor_position()) else {
return;
};
let Ok((cam, cam_gtf)) = cameras_q.get(camera) else {
return;
};
let Ok(ray) = cam.viewport_to_world(cam_gtf, cursor) else {
return;
};
let Some(t) = ray.intersect_plane(plane_origin, InfinitePlane3d::new(plane_normal)) else {
return;
};
let current_hit = ray.origin + ray.direction * t;
let delta = current_hit - start_hit_world;
let du = delta.dot(u_axis);
let dv = -delta.dot(v_axis);
let Ok((mut tf, mut display_size, resizable, base, quality)) = webviews.get_mut(webview) else {
return;
};
let scale_factor = Vec2::new(base.0.x * quality.0, base.0.y * quality.0);
let min_display = Vec2::new(
resizable.min_size.x as f32 / scale_factor.x,
resizable.min_size.y as f32 / scale_factor.y,
);
let max_display = resizable
.max_size
.map(|max| Vec2::new(max.x as f32 / scale_factor.x, max.y as f32 / scale_factor.y));
let lock_aspect = match aspect_lock_mode {
AspectLockMode::Always => true,
AspectLockMode::Never => false,
AspectLockMode::LockOnShift => {
keyboard.pressed(KeyCode::ShiftLeft) || keyboard.pressed(KeyCode::ShiftRight)
}
};
let (new_size, new_translation) = apply_resize(
zone,
start_display_size,
start_translation,
du,
dv,
u_axis,
v_axis,
lock_aspect,
min_display,
max_display,
);
display_size.0 = new_size;
tf.translation = new_translation;
}
#[allow(clippy::too_many_arguments)]
fn cursor_hover_system(
resize_state: Res<ResizeState>,
windows: Query<&Window>,
mut cursor_override: ResMut<SystemCursorOverride>,
pointer: WebviewPointer,
mesh_resizables: Query<
(Entity, &WebviewResizable, &WebviewSize),
(With<WebviewResizable>, With<Mesh3d>),
>,
sprite_resizables: Query<
(
Entity,
&WebviewResizable,
&WebviewSize,
&Sprite,
&GlobalTransform,
),
(With<WebviewResizable>, With<Sprite>),
>,
cameras: Query<(&Camera, &GlobalTransform)>,
) {
if resize_state.is_resizing() {
return;
}
let Some(cursor_pos) = windows.iter().find_map(|w| w.cursor_position()) else {
cursor_override.clear();
return;
};
for (entity, resizable, size) in mesh_resizables.iter() {
if let Some((pixel_pos, _cam)) = pointer.pointer_pos_raw(entity, cursor_pos) {
let frame = ResizeFrame {
width: size.0.x as u32,
height: size.0.y as u32,
edge_thickness: resizable.edge_thickness,
};
if let Some(zone) = frame.classify(pixel_pos) {
cursor_override.set(cursor_for_zone(zone));
return;
}
}
}
for (_entity, resizable, size, sprite, gtf) in sprite_resizables.iter() {
if let Some(pixel_pos) = crate::webview::webview_sprite::obtain_relative_pos(
sprite, size, gtf, &cameras, cursor_pos,
) {
let frame = ResizeFrame {
width: size.0.x as u32,
height: size.0.y as u32,
edge_thickness: resizable.edge_thickness,
};
if let Some(zone) = frame.classify(pixel_pos) {
cursor_override.set(cursor_for_zone(zone));
return;
}
}
}
cursor_override.clear();
}
#[allow(clippy::too_many_arguments)]
fn init_resizable_system(
mut commands: Commands,
new_resizables: Query<
(
Entity,
&WebviewSize,
&Transform,
&GlobalTransform,
Option<&Mesh3d>,
Option<&Sprite>,
),
Added<WebviewResizable>,
>,
mesh_assets: Res<Assets<Mesh>>,
) {
for (entity, webview_size, _tf, gtf, mesh3d, sprite) in new_resizables.iter() {
if let Some(mesh3d) = mesh3d {
if let Some(mesh) = mesh_assets.get(&mesh3d.0) {
if let Some(aabb) = mesh.compute_aabb() {
let local_size =
Vec2::new(aabb.half_extents.x * 2.0, aabb.half_extents.y * 2.0);
let scale = gtf.compute_transform().scale;
let world_size = Vec2::new(local_size.x * scale.x, local_size.y * scale.y);
let base = Vec2::new(
webview_size.0.x / world_size.x,
webview_size.0.y / world_size.y,
);
commands.entity(entity).insert((
DisplaySize(world_size),
BaseRenderScale(base),
QualityMultiplier::default(),
WebviewBasis2d { local_size },
));
} else {
commands
.entity(entity)
.insert((PendingBasisInit, QualityMultiplier::default()));
}
} else {
commands
.entity(entity)
.insert((PendingBasisInit, QualityMultiplier::default()));
}
} else if let Some(sprite) = sprite {
let display_size = sprite
.custom_size
.unwrap_or(Vec2::new(webview_size.0.x, webview_size.0.y));
let base = Vec2::new(
webview_size.0.x / display_size.x,
webview_size.0.y / display_size.y,
);
commands.entity(entity).insert((
DisplaySize(display_size),
BaseRenderScale(base),
QualityMultiplier::default(),
));
}
}
}
fn pending_basis_init_system(
mut commands: Commands,
pending: Query<(Entity, &WebviewSize, &GlobalTransform, &Mesh3d), With<PendingBasisInit>>,
mesh_assets: Res<Assets<Mesh>>,
) {
for (entity, webview_size, gtf, mesh3d) in pending.iter() {
let Some(mesh) = mesh_assets.get(&mesh3d.0) else {
continue;
};
let Some(aabb) = mesh.compute_aabb() else {
continue;
};
let local_size = Vec2::new(aabb.half_extents.x * 2.0, aabb.half_extents.y * 2.0);
let scale = gtf.compute_transform().scale;
let world_size = Vec2::new(local_size.x * scale.x, local_size.y * scale.y);
let base = Vec2::new(
webview_size.0.x / world_size.x,
webview_size.0.y / world_size.y,
);
commands.entity(entity).insert((
DisplaySize(world_size),
BaseRenderScale(base),
WebviewBasis2d { local_size },
));
commands.entity(entity).remove::<PendingBasisInit>();
}
}