bevy_editor_pls/
controls.rs

1use bevy::{prelude::*, utils::HashMap};
2use bevy_editor_pls_core::{editor_window::EditorWindow, Editor, EditorEvent};
3
4#[derive(Debug)]
5pub enum Button {
6    Keyboard(KeyCode),
7    Mouse(MouseButton),
8}
9
10#[derive(Debug)]
11pub enum UserInput {
12    Single(Button),
13    Chord(Vec<Button>),
14}
15
16#[derive(Debug)]
17pub enum BindingCondition {
18    InViewport(bool),
19    EditorActive(bool),
20    ListeningForText(bool),
21}
22
23impl BindingCondition {
24    fn evaluate(&self, editor: &Editor) -> bool {
25        match *self {
26            BindingCondition::InViewport(in_viewport) => {
27                if in_viewport {
28                    !editor.pointer_used()
29                } else {
30                    editor.pointer_used()
31                }
32            }
33            BindingCondition::EditorActive(editor_active) => editor_active == editor.active(),
34            BindingCondition::ListeningForText(listening) => {
35                listening == editor.listening_for_text()
36            }
37        }
38    }
39}
40
41impl std::fmt::Display for BindingCondition {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        let str = match self {
44            BindingCondition::InViewport(true) => "mouse in viewport",
45            BindingCondition::InViewport(false) => "mouse not in viewport",
46            BindingCondition::EditorActive(true) => "editor is active",
47            BindingCondition::EditorActive(false) => "editor is not active",
48            BindingCondition::ListeningForText(true) => "a ui field is listening for text",
49            BindingCondition::ListeningForText(false) => "no ui fields are listening for text",
50        };
51        f.write_str(str)
52    }
53}
54
55#[derive(Debug)]
56pub struct Binding {
57    pub input: UserInput,
58    pub conditions: Vec<BindingCondition>,
59}
60
61impl From<UserInput> for Binding {
62    fn from(input: UserInput) -> Self {
63        Binding {
64            input,
65            conditions: Vec::new(),
66        }
67    }
68}
69
70impl Button {
71    fn just_pressed(
72        &self,
73        keyboard_input: &ButtonInput<KeyCode>,
74        mouse_input: &ButtonInput<MouseButton>,
75    ) -> bool {
76        match self {
77            Button::Keyboard(code) => keyboard_input.just_pressed(*code),
78            Button::Mouse(button) => mouse_input.just_pressed(*button),
79        }
80    }
81    fn pressed(
82        &self,
83        keyboard_input: &ButtonInput<KeyCode>,
84        mouse_input: &ButtonInput<MouseButton>,
85    ) -> bool {
86        match self {
87            Button::Keyboard(code) => keyboard_input.pressed(*code),
88            Button::Mouse(button) => mouse_input.pressed(*button),
89        }
90    }
91}
92
93impl UserInput {
94    fn just_pressed(
95        &self,
96        keyboard_input: &ButtonInput<KeyCode>,
97        mouse_input: &ButtonInput<MouseButton>,
98    ) -> bool {
99        match self {
100            UserInput::Single(single) => single.just_pressed(keyboard_input, mouse_input),
101            UserInput::Chord(chord) => match chord.as_slice() {
102                [modifiers @ .., final_key] => {
103                    let modifiers_pressed = modifiers
104                        .iter()
105                        .all(|key| key.pressed(keyboard_input, mouse_input));
106                    modifiers_pressed && final_key.just_pressed(keyboard_input, mouse_input)
107                }
108                [] => false,
109            },
110        }
111    }
112}
113
114impl Binding {
115    fn just_pressed(
116        &self,
117        keyboard_input: &ButtonInput<KeyCode>,
118        mouse_input: &ButtonInput<MouseButton>,
119        editor: &Editor,
120    ) -> bool {
121        let can_trigger = self
122            .conditions
123            .iter()
124            .all(|condition| condition.evaluate(editor));
125        if !can_trigger {
126            return false;
127        }
128
129        self.input.just_pressed(keyboard_input, mouse_input)
130    }
131}
132
133#[derive(PartialEq, Eq, Hash)]
134pub enum Action {
135    PlayPauseEditor,
136    PauseUnpauseTime,
137    FocusSelected,
138
139    // maybe investigate [GizmoOptions].hotkeys
140    // https://docs.rs/transform-gizmo-bevy/latest/transform_gizmo_bevy/struct.GizmoHotkeys.html
141    #[cfg(feature = "default_windows")]
142    SetGizmoModeTranslate,
143    #[cfg(feature = "default_windows")]
144    SetGizmoModeRotate,
145    #[cfg(feature = "default_windows")]
146    SetGizmoModeScale,
147}
148
149impl std::fmt::Display for Action {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        match self {
152            Action::PlayPauseEditor => write!(f, "Play/Pause editor"),
153            Action::PauseUnpauseTime => write!(f, "Pause/Unpause time"),
154            Action::FocusSelected => write!(f, "Focus Selected Entity"),
155            #[cfg(feature = "default_windows")]
156            Action::SetGizmoModeTranslate => write!(f, "Activate translation gizmo"),
157            #[cfg(feature = "default_windows")]
158            Action::SetGizmoModeRotate => write!(f, "Activate rotation gizmo"),
159            #[cfg(feature = "default_windows")]
160            Action::SetGizmoModeScale => write!(f, "Activate scale gizmo"),
161        }
162    }
163}
164
165/// Resource mapping input bindings to [`Action`]s
166#[derive(Resource, Default)]
167pub struct EditorControls {
168    pub actions: HashMap<Action, Vec<Binding>>,
169}
170
171impl EditorControls {
172    pub fn insert(&mut self, action: Action, binding: Binding) {
173        self.actions.entry(action).or_default().push(binding);
174    }
175    fn get(&self, action: &Action) -> &[Binding] {
176        self.actions.get(action).map_or(&[], Vec::as_slice)
177    }
178
179    fn just_pressed(
180        &self,
181        action: Action,
182        keyboard_input: &ButtonInput<KeyCode>,
183        mouse_input: &ButtonInput<MouseButton>,
184        editor: &Editor,
185    ) -> bool {
186        let bindings = &self.actions.get(&action).unwrap();
187        bindings
188            .iter()
189            .any(|binding| binding.just_pressed(keyboard_input, mouse_input, editor))
190    }
191}
192
193pub fn editor_controls_system(
194    controls: Res<EditorControls>,
195    keyboard_input: Res<ButtonInput<KeyCode>>,
196    mouse_input: Res<ButtonInput<MouseButton>>,
197    mut editor_events: EventWriter<EditorEvent>,
198    mut editor: ResMut<Editor>,
199) {
200    if controls.just_pressed(
201        Action::PlayPauseEditor,
202        &keyboard_input,
203        &mouse_input,
204        &editor,
205    ) && !editor.always_active()
206    {
207        let was_active = editor.active();
208        editor.set_active(!was_active);
209        editor_events.send(EditorEvent::Toggle {
210            now_active: !was_active,
211        });
212    }
213
214    if controls.just_pressed(
215        Action::PauseUnpauseTime,
216        &keyboard_input,
217        &mouse_input,
218        &editor,
219    ) {
220        if let Some(default_window) = editor.window_state_mut::<bevy_editor_pls_default_windows::debug_settings::DebugSettingsWindow>() {
221            default_window.pause_time = !default_window.pause_time;
222        }
223    }
224
225    if controls.just_pressed(
226        Action::FocusSelected,
227        &keyboard_input,
228        &mouse_input,
229        &editor,
230    ) {
231        editor_events.send(EditorEvent::FocusSelected);
232    }
233
234    #[cfg(feature = "default_windows")]
235    {
236        if controls.just_pressed(
237            Action::SetGizmoModeTranslate,
238            &keyboard_input,
239            &mouse_input,
240            &editor,
241        ) {
242            editor
243                .window_state_mut::<bevy_editor_pls_default_windows::gizmos::GizmoWindow>()
244                .unwrap()
245                .gizmo_modes = transform_gizmo_bevy::GizmoMode::all_translate();
246        }
247        if controls.just_pressed(
248            Action::SetGizmoModeRotate,
249            &keyboard_input,
250            &mouse_input,
251            &editor,
252        ) {
253            editor
254                .window_state_mut::<bevy_editor_pls_default_windows::gizmos::GizmoWindow>()
255                .unwrap()
256                .gizmo_modes = transform_gizmo_bevy::GizmoMode::all_rotate();
257        }
258        if controls.just_pressed(
259            Action::SetGizmoModeScale,
260            &keyboard_input,
261            &mouse_input,
262            &editor,
263        ) {
264            editor
265                .window_state_mut::<bevy_editor_pls_default_windows::gizmos::GizmoWindow>()
266                .unwrap()
267                .gizmo_modes = transform_gizmo_bevy::GizmoMode::all_scale();
268        }
269    }
270}
271
272impl EditorControls {
273    pub fn unbind(&mut self, action: Action) {
274        self.actions.remove(&action);
275    }
276
277    /// - `C-Enter`: pause time
278    /// - `E`: toggle editor
279    /// - `F`: focus on selected entity
280    /// `T/R/S`: show translate/rotate/scale gizmo
281    pub fn default_bindings() -> Self {
282        let mut controls = EditorControls::default();
283
284        controls.insert(
285            Action::PauseUnpauseTime,
286            Binding {
287                input: UserInput::Chord(vec![
288                    Button::Keyboard(KeyCode::ControlLeft),
289                    Button::Keyboard(KeyCode::Enter),
290                ]),
291                conditions: vec![BindingCondition::ListeningForText(false)],
292            },
293        );
294
295        controls.insert(
296            Action::PlayPauseEditor,
297            Binding {
298                input: UserInput::Single(Button::Keyboard(KeyCode::KeyE)),
299                conditions: vec![BindingCondition::ListeningForText(false)],
300            },
301        );
302
303        controls.insert(
304            Action::FocusSelected,
305            Binding {
306                input: UserInput::Single(Button::Keyboard(KeyCode::KeyF)),
307                conditions: vec![BindingCondition::EditorActive(true)],
308            },
309        );
310
311        #[cfg(feature = "default_windows")]
312        {
313            controls.insert(
314                Action::SetGizmoModeTranslate,
315                UserInput::Single(Button::Keyboard(KeyCode::KeyT)).into(),
316            );
317            controls.insert(
318                Action::SetGizmoModeRotate,
319                UserInput::Single(Button::Keyboard(KeyCode::KeyR)).into(),
320            );
321            controls.insert(
322                Action::SetGizmoModeScale,
323                UserInput::Single(Button::Keyboard(KeyCode::KeyS)).into(),
324            );
325        }
326
327        controls
328    }
329}
330
331impl std::fmt::Display for Button {
332    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333        match self {
334            Button::Keyboard(key) => write!(f, "{:?}", key),
335            Button::Mouse(mouse) => write!(f, "{:?}", mouse),
336        }
337    }
338}
339
340impl std::fmt::Display for UserInput {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        match self {
343            UserInput::Single(single) => {
344                write!(f, "{}", single)?;
345            }
346            UserInput::Chord(chord) => {
347                let mut iter = chord.iter();
348                let first = iter.next();
349                if let Some(first) = first {
350                    write!(f, "{}", first)?;
351                }
352
353                for remaining in iter {
354                    write!(f, " + {}", remaining)?;
355                }
356            }
357        }
358        Ok(())
359    }
360}
361
362impl std::fmt::Display for Binding {
363    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364        write!(f, "{}", self.input)?;
365
366        let mut conditions = self.conditions.iter();
367        let first_condition = conditions.next();
368        if let Some(first) = first_condition {
369            write!(f, "\n    when {}", first)?;
370        }
371        for remaining in conditions {
372            write!(f, " and {}", remaining)?;
373        }
374
375        Ok(())
376    }
377}
378
379pub struct ControlsWindow;
380
381impl EditorWindow for ControlsWindow {
382    type State = ();
383    const NAME: &'static str = "Controls";
384
385    fn ui(
386        world: &mut World,
387        _: bevy_editor_pls_core::editor_window::EditorWindowContext,
388        ui: &mut egui::Ui,
389    ) {
390        let controls = world.get_resource::<EditorControls>().unwrap();
391
392        for action in &[
393            Action::PlayPauseEditor,
394            Action::PauseUnpauseTime,
395            Action::FocusSelected,
396        ] {
397            ui.label(egui::RichText::new(action.to_string()).strong());
398            let bindings = controls.get(action);
399            for binding in bindings {
400                ui.add(egui::Label::new(format!("{}", binding)).extend());
401            }
402        }
403    }
404}