Skip to main content

bevy_trackball/controller/
viewport.rs

1use bevy::{
2	camera::RenderTarget,
3	input::{mouse::MouseWheel, touch::TouchPhase},
4	prelude::*,
5	window::{CursorOptions, PrimaryWindow, WindowRef},
6};
7
8use super::{TrackballCamera, TrackballController};
9
10/// Trackball viewport currently focused and hence capturing input events.
11///
12///  * Enables multiple viewports/windows with individual controllers/cameras.
13///  * Enables UI systems to steal the viewport in order to capture input events.
14#[derive(Resource, Clone, Debug, PartialEq, Eq, Default)]
15pub struct TrackballViewport {
16	entity: Option<Entity>,
17	stolen: usize,
18}
19
20impl TrackballViewport {
21	/// Condition whether the viewport has been stolen, evaluated by
22	/// [`IntoScheduleConfigs::run_if`].
23	///
24	/// Interferes with automatic viewport stealing if the `bevy_egui` feature is enabled. As
25	/// automatic viewport stealing gives the viewport back with `set_stolen(None)` instead of
26	/// `set_stolen(Some(0))`, you can override it in the same frame for your own input capturing.
27	#[allow(clippy::needless_pass_by_value, clippy::missing_const_for_fn)]
28	#[must_use]
29	pub fn stolen(viewport: Res<Self>) -> bool {
30		viewport.stolen != 0
31	}
32	/// Whether viewport has just been given back.
33	///
34	/// Interferes with automatic viewport stealing if the `bevy_egui` feature is enabled. As
35	/// automatic viewport stealing gives the viewport back with `set_stolen(None)` instead of
36	/// `set_stolen(Some(0))`, you can override it in the same frame for your own input capturing.
37	#[must_use]
38	pub const fn was_stolen(&self) -> bool {
39		self.entity.is_none()
40	}
41	/// Steals the viewport or gives it back.
42	///
43	/// Interferes with automatic viewport stealing if the `bevy_egui` feature is enabled. As
44	/// automatic viewport stealing gives the viewport back with `set_stolen(None)` instead of
45	/// `set_stolen(Some(0))`, you can override it in the same frame for your own input capturing.
46	///
47	/// # Examples
48	///
49	/// Steals the viewport for `Some(frames)` and lets it count `frames` down with `None`:
50	///
51	/// ```ignore
52	/// fn system(/* ... */) {
53	/// 	viewport.set_stolen(just_stolen.then_some(3));
54	/// }
55	///
56	/// // frame 0: just_stolen = true  -> set_stolen(Some(3)) -> frames = 3 -> stolen = true
57	/// // frame 1: just_stolen = false -> set_stolen(None)    -> frames = 2 -> stolen = true
58	/// // frame 2: just_stolen = false -> set_stolen(None)    -> frames = 1 -> stolen = true
59	/// // frame 3: just_stolen = false -> set_stolen(None)    -> frames = 0 -> stolen = false
60	/// ```
61	///
62	/// Steals the viewport with `Some(1)` and gives it back with `Some(0)`:
63	///
64	/// ```ignore
65	/// fn system(/* ... */) {
66	/// 	if just_stolen {
67	/// 		viewport.set_stolen(Some(1));
68	/// 	}
69	/// 	if just_give_back {
70	/// 		viewport.set_stolen(Some(0));
71	/// 	}
72	/// }
73	///
74	/// // frame  0: just_stolen = true    -> set_stolen(Some(1)) -> frames = 1 -> stolen = true
75	/// // frame  1: just_stolen = false   ->                     -> frames = 1 -> stolen = true
76	/// // frame 25: just_stolen = false   ->                     -> frames = 1 -> stolen = true
77	/// // frame 50: just_give_back = true -> set_stolen(Some(0)) -> frames = 0 -> stolen = false
78	/// ```
79	#[allow(clippy::needless_pass_by_value)]
80	pub const fn set_stolen(&mut self, stolen: Option<usize>) {
81		if let Some(frames) = stolen {
82			self.entity = None;
83			self.stolen = frames;
84		} else if self.stolen != 0 {
85			self.stolen -= 1;
86		}
87	}
88	#[allow(clippy::too_many_arguments)]
89	#[allow(clippy::type_complexity)]
90	pub(super) fn select<'a>(
91		viewport: &mut ResMut<Self>,
92		key_input: &Res<ButtonInput<KeyCode>>,
93		mouse_input: &Res<ButtonInput<MouseButton>>,
94		touch_events: &mut MessageReader<TouchInput>,
95		wheel_events: &MessageReader<MouseWheel>,
96		primary_windows: &'a mut Query<
97			(Entity, &mut Window, &mut CursorOptions),
98			With<PrimaryWindow>,
99		>,
100		secondary_windows: &'a mut Query<(&mut Window, &mut CursorOptions), Without<PrimaryWindow>>,
101		cameras: &'a mut Query<(
102			Entity,
103			&Camera,
104			&RenderTarget,
105			&TrackballCamera,
106			&mut TrackballController,
107		)>,
108	) -> Option<(
109		bool,
110		Entity,
111		Mut<'a, Window>,
112		Mut<'a, CursorOptions>,
113		Entity,
114		&'a Camera,
115		&'a TrackballCamera,
116		Mut<'a, TrackballController>,
117	)> {
118		let touch = touch_events
119			.read()
120			.filter_map(|touch| (touch.phase == TouchPhase::Started).then_some(touch.position))
121			.last();
122		let input = !wheel_events.is_empty()
123			|| key_input.get_just_pressed().len() != 0
124			|| mouse_input.get_just_pressed().len() != 0;
125		let mut new_viewport = viewport.clone();
126		let mut max_order = 0;
127		for (group, camera, target, _trackball, _controller) in cameras.iter() {
128			let RenderTarget::Window(window_ref) = target else {
129				continue;
130			};
131			let window = match window_ref {
132				WindowRef::Primary => primary_windows
133					.single()
134					.ok()
135					.map(|(_id, window, _cursor_options)| window),
136				WindowRef::Entity(id) => secondary_windows
137					.get(*id)
138					.ok()
139					.map(|(window, _cursor_options)| window),
140			};
141			let Some(window) = window else {
142				continue;
143			};
144			let Some(pos) = touch
145				.filter(|_pos| window.focused)
146				.or_else(|| window.cursor_position().filter(|_pos| input))
147			else {
148				continue;
149			};
150			let Some(Rect { min, max }) = camera.logical_viewport_rect() else {
151				continue;
152			};
153			let contained = (min.x..max.x).contains(&pos.x) && (min.y..max.y).contains(&pos.y);
154			if contained && camera.order >= max_order {
155				new_viewport.entity = Some(group);
156				max_order = camera.order;
157			}
158		}
159		let is_changed = viewport.entity != new_viewport.entity;
160		if is_changed {
161			viewport.entity = new_viewport.entity;
162		}
163		let camera = viewport
164			.entity
165			.and_then(|entity| cameras.get_mut(entity).ok());
166		let Some((group, camera, target, trackball, controller)) = camera else {
167			viewport.entity = None;
168			return None;
169		};
170		let RenderTarget::Window(window_ref) = target else {
171			return None;
172		};
173		let (window_id, window, cursor_options) = match window_ref {
174			WindowRef::Primary => primary_windows.single_mut().ok(),
175			WindowRef::Entity(id) => secondary_windows
176				.get_mut(*id)
177				.ok()
178				.map(|(window, cursor_options)| (*id, window, cursor_options)),
179		}?;
180		Some((
181			is_changed,
182			window_id,
183			window,
184			cursor_options,
185			group,
186			camera,
187			trackball,
188			controller,
189		))
190	}
191}