cvars_console_macroquad/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4use macroquad::{
5    prelude::*,
6    ui::{
7        hash, root_ui,
8        widgets::{Group, Label},
9        Layout, Skin,
10    },
11};
12
13use cvars::SetGet;
14use cvars_console::Console;
15
16/// In-game console for the Macroquad game engine.
17///
18/// It handles all the input and drawing, you just need to call `update` every frame after rendering.
19#[derive(Debug, Clone, Default)]
20pub struct MacroquadConsole {
21    is_open: bool,
22    console: Console,
23    config: Config,
24    input: ConsoleInput,
25    input_prev: ConsoleInput,
26}
27
28impl MacroquadConsole {
29    /// Create a new console. Build its UI but keep it closed.
30    pub fn new() -> Self {
31        Self {
32            is_open: false,
33            console: Console::new(),
34            config: Config::default(),
35            input: ConsoleInput::new(),
36            input_prev: ConsoleInput::new(),
37        }
38    }
39
40    /// Process input, handle opening and closing, draw the console.
41    ///
42    /// Call this every frame after your game's rendering code so the console is drawn on top.
43    pub fn update(&mut self, cvars: &mut dyn SetGet) {
44        self.input_prev = self.input;
45        self.input = get_input();
46
47        self.open_close();
48
49        if self.is_open {
50            self.process_input();
51            self.draw_console();
52            if !self.input_prev.enter && self.input.enter && !self.console.prompt.is_empty() {
53                self.console.enter(cvars);
54            }
55        }
56    }
57
58    /// Open or close the console based on user's input.
59    fn open_close(&mut self) {
60        let pressed_console = !self.input_prev.console && self.input.console;
61        let pressed_escape = !self.input_prev.escape && self.input.escape;
62        if !self.is_open && pressed_console {
63            self.is_open = true;
64            show_mouse(true);
65        } else if self.is_open && (pressed_console || pressed_escape) {
66            self.is_open = false;
67            show_mouse(false);
68        }
69    }
70
71    /// Sanitize input text, handle cycling through history, etc.
72    fn process_input(&mut self) {
73        // The semicolon (default console bind) gets typed into the console
74        // when opening it (but interestingly not closing).
75        // Just disallow it completely we don't allow multiple commands on one line
76        // so there's currently no need for it.
77        // This has the side effect, that the text cursor moves one char to the right
78        // with each open/close cycle but that's OK.
79        // LATER A less hacky input system would be great.
80        self.console.prompt = self.console.prompt.replace(';', "");
81
82        // Detect key pressed based on previous and current state.
83        // MQ's UI doesn't seem to have a built-in way to detecting keyboard events.
84        let pressed_up = !self.input_prev.up && self.input.up;
85        let pressed_down = !self.input_prev.down && self.input.down;
86        let pressed_page_up = !self.input_prev.page_up && self.input.page_up;
87        let pressed_page_down = !self.input_prev.page_down && self.input.page_down;
88
89        if pressed_up {
90            self.console.history_back();
91        }
92
93        // Go forward in history
94        if pressed_down {
95            self.console.history_forward();
96        }
97
98        // Scroll history up
99        let count = 10; // LATER configurable
100        if pressed_page_up {
101            self.console.history_scroll_up(count);
102        }
103        if pressed_page_down {
104            self.console.history_scroll_down(count);
105        }
106    }
107
108    /// Draw the console and the UI elements it needs.
109    fn draw_console(&mut self) {
110        // Draw background
111        // Floor aligns to pixels, otherwise text renders poorly.
112        let console_height = (screen_height() * self.config.height_fraction).floor();
113        draw_rectangle(
114            0.0,
115            0.0,
116            screen_width(),
117            console_height,
118            Color::new(0.0, 0.0, 0.0, self.config.background_alpha),
119        );
120        draw_line(
121            0.0,
122            console_height,
123            screen_width(),
124            console_height,
125            1.0,
126            RED,
127        );
128
129        // Draw history
130        // This doesn't allow copying but in MQ's UI there's no way to print text
131        // which allows copying while preventing editing.
132        if self.console.history_view_end >= 1 {
133            let mut i = self.console.history_view_end - 1;
134            let mut y = console_height - self.config.history_y_offset;
135            loop {
136                let text = if self.console.history[i].is_input {
137                    format!("> {}", self.console.history[i].text)
138                } else {
139                    self.console.history[i].text.clone()
140                };
141                draw_text(
142                    &text,
143                    self.config.history_x,
144                    y,
145                    self.config.history_line_font_size,
146                    WHITE,
147                );
148                if i == 0 || y < 0.0 {
149                    break;
150                }
151                i -= 1;
152                y -= self.config.history_line_height;
153            }
154        }
155
156        // Prompt style
157        let bg_image = Image::gen_image_color(1, 1, BLANK);
158        let style = root_ui()
159            .style_builder()
160            .background(bg_image)
161            .color(BLANK) // This hides the faint rectangle around a Group
162            .text_color(WHITE)
163            .build();
164        let skin = Skin {
165            label_style: style.clone(),
166            editbox_style: style.clone(),
167            group_style: style,
168            ..root_ui().default_skin()
169        };
170        root_ui().push_skin(&skin);
171
172        // Draw prompt - this uses MQ's UI so i don't have to reimplement basic text editing ops.
173        let id_prompt = 0;
174        let label_y = console_height - self.config.prompt_label_y_offset;
175        Label::new(">")
176            .position(vec2(self.config.prompt_label_x, label_y))
177            .ui(&mut root_ui());
178        // Can't set position on an InputText so we wrap it in a Group.
179        let group_y =
180            screen_height() * self.config.height_fraction - self.config.prompt_group_y_offset;
181        Group::new(hash!(), vec2(screen_width() - 8.0, 20.0))
182            .position(vec2(self.config.prompt_group_x, group_y))
183            .layout(Layout::Horizontal)
184            .ui(&mut root_ui(), |ui| {
185                ui.input_text(id_prompt, "", &mut self.console.prompt);
186            });
187
188        // The prompt should have focus all the time.
189        root_ui().set_input_focus(id_prompt);
190    }
191
192    /// Whether the console is open right now.
193    ///
194    /// Useful for example to ignore game-related input
195    /// while the player is typing into console.
196    pub fn is_open(&self) -> bool {
197        self.is_open
198    }
199}
200
201/// Configuration for the console.
202#[derive(Debug, Clone)]
203struct Config {
204    background_alpha: f32,
205    prompt_group_x: f32,
206    prompt_group_y_offset: f32,
207    height_fraction: f32,
208    history_line_font_size: f32,
209    history_line_height: f32,
210    history_x: f32,
211    history_y_offset: f32,
212    prompt_label_x: f32,
213    prompt_label_y_offset: f32,
214}
215
216impl Default for Config {
217    fn default() -> Self {
218        Self {
219            background_alpha: 0.8,
220            prompt_group_x: 16.0,
221            prompt_group_y_offset: 26.0,
222            height_fraction: 0.45,
223            history_line_font_size: 16.0,
224            history_line_height: 14.0,
225            history_x: 8.0,
226            history_y_offset: 25.0,
227            prompt_label_x: 8.0,
228            prompt_label_y_offset: 22.0,
229        }
230    }
231}
232
233#[derive(Debug, Clone, Copy, Default)]
234struct ConsoleInput {
235    console: bool,
236    escape: bool,
237    enter: bool,
238    up: bool,
239    down: bool,
240    page_up: bool,
241    page_down: bool,
242}
243
244impl ConsoleInput {
245    fn new() -> Self {
246        Self::default()
247    }
248}
249
250fn get_input() -> ConsoleInput {
251    let mut input = ConsoleInput::new();
252    if are_keys_pressed(&[KeyCode::GraveAccent, KeyCode::Semicolon]) {
253        input.console = true;
254    }
255    if are_keys_pressed(&[KeyCode::Escape]) {
256        input.escape = true;
257    }
258    if are_keys_pressed(&[KeyCode::Enter, KeyCode::KpEnter]) {
259        input.enter = true;
260    }
261    if are_keys_pressed(&[KeyCode::Up]) {
262        input.up = true;
263    }
264    if are_keys_pressed(&[KeyCode::Down]) {
265        input.down = true;
266    }
267    if are_keys_pressed(&[KeyCode::PageUp]) {
268        input.page_up = true;
269    }
270    if are_keys_pressed(&[KeyCode::PageDown]) {
271        input.page_down = true;
272    }
273    input
274}
275
276fn are_keys_pressed(key_codes: &[KeyCode]) -> bool {
277    for &key_code in key_codes {
278        if is_key_pressed(key_code) {
279            return true;
280        }
281    }
282    false
283}