cvars_console_fyrox/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4use fyrox_ui::{
5    border::BorderBuilder,
6    brush::Brush,
7    core::{color::Color, pool::Handle},
8    formatted_text::WrapMode,
9    message::{KeyCode, MessageDirection, UiMessage},
10    stack_panel::StackPanelBuilder,
11    text::{TextBuilder, TextMessage},
12    text_box::{TextBoxBuilder, TextCommitMode},
13    widget::{WidgetBuilder, WidgetMessage},
14    Orientation, UiNode, UserInterface, VerticalAlignment,
15};
16
17use cvars::SetGet;
18use cvars_console::Console;
19
20/// In-game console for the Fyrox game engine.
21pub struct FyroxConsole {
22    is_open: bool,
23    first_open: bool,
24    was_mouse_grabbed: bool,
25    console: Console,
26    height: f32,
27    history: Handle<UiNode>,
28    prompt_text_box: Handle<UiNode>,
29    layout: Handle<UiNode>,
30}
31
32impl FyroxConsole {
33    /// Create a new console. Build its UI but keep it closed.
34    pub fn new(ui: &mut UserInterface) -> Self {
35        let history = TextBuilder::new(WidgetBuilder::new())
36            // Word wrap doesn't work if there's an extremely long word.
37            .with_wrap(WrapMode::Letter)
38            .build(&mut ui.build_ctx());
39
40        let prompt_arrow = TextBuilder::new(WidgetBuilder::new())
41            .with_text("> ")
42            .build(&mut ui.build_ctx());
43
44        let prompt_text_box = TextBoxBuilder::new(WidgetBuilder::new())
45            .with_text_commit_mode(TextCommitMode::Immediate)
46            .with_skip_chars(vec!['-', '_'])
47            .build(&mut ui.build_ctx());
48
49        let prompt_line = StackPanelBuilder::new(
50            WidgetBuilder::new().with_children([prompt_arrow, prompt_text_box]),
51        )
52        .with_orientation(Orientation::Horizontal)
53        .build(&mut ui.build_ctx());
54
55        // StackPanel doesn't support colored background so we wrap it in a Border.
56        let layout = BorderBuilder::new(
57            WidgetBuilder::new()
58                .with_visibility(false)
59                .with_background(Brush::Solid(Color::BLACK.with_new_alpha(220)))
60                .with_child(
61                    StackPanelBuilder::new(
62                        WidgetBuilder::new()
63                            .with_vertical_alignment(VerticalAlignment::Bottom)
64                            .with_children([history, prompt_line]),
65                    )
66                    .with_orientation(Orientation::Vertical)
67                    .build(&mut ui.build_ctx()),
68                ),
69        )
70        .build(&mut ui.build_ctx());
71
72        FyroxConsole {
73            is_open: false,
74            first_open: true,
75            was_mouse_grabbed: false,
76            console: Console::new(),
77            height: 0.0,
78            history,
79            prompt_text_box,
80            layout,
81        }
82    }
83
84    /// Call this when the window is resized.
85    pub fn resized(&mut self, ui: &mut UserInterface, width: f32, height: f32) {
86        ui.send_message(WidgetMessage::width(
87            self.layout,
88            MessageDirection::ToWidget,
89            width,
90        ));
91
92        self.height = height / 2.0;
93        ui.send_message(WidgetMessage::height(
94            self.layout,
95            MessageDirection::ToWidget,
96            self.height,
97        ));
98
99        // This actually goes beyond the screen but who cares.
100        // It, however, still won't let me put the cursor at the end by clicking after the text:
101        // https://github.com/FyroxEngine/Fyrox/issues/361
102        ui.send_message(WidgetMessage::width(
103            self.prompt_text_box,
104            MessageDirection::ToWidget,
105            width,
106        ));
107
108        // The number of lines that can fit might have changed - reprint history.
109        self.update_ui_history(ui);
110    }
111
112    /// Call this for every Fyrox UI message. The console will only react to them if it's open.
113    ///
114    /// # Example
115    /// ```rust,ignore
116    /// while let Some(msg) = engine.user_interface.poll_message() {
117    ///     console.ui_message(&msg);
118    ///     // ... Whatever else you do with UI messages ...
119    /// }
120    /// ```
121    pub fn ui_message(&mut self, ui: &mut UserInterface, cvars: &mut impl SetGet, msg: &UiMessage) {
122        if !self.is_open || msg.destination != self.prompt_text_box {
123            return;
124        }
125
126        // We could just listen for KeyboardInput and get the text from the prompt via
127        // ```
128        // let node = ui.node(self.prompt_text_box);
129        // let text = node.query_component::<TextBox>().unwrap().text();
130        // ```
131        // But this is the intended way to use the UI, even if it's more verbose.
132        // At least it should reduce issues with the prompt reacting to some keys
133        // but not others given KeyboardInput doesn't require focus.
134        //
135        // Note that it might still be better to read the text from the UI as the souce of truth
136        // because right now the console doesn't know about any text we set from code on init.
137        if let Some(TextMessage::Text(text)) = msg.data() {
138            self.console.prompt = text.to_owned();
139        }
140
141        match msg.data() {
142            Some(WidgetMessage::Unfocus) => {
143                // As long as the console is open, always keep the prompt focused
144                ui.send_message(WidgetMessage::focus(
145                    self.prompt_text_box,
146                    MessageDirection::ToWidget,
147                ));
148            }
149            Some(WidgetMessage::KeyDown(KeyCode::ArrowUp)) => {
150                self.console.history_back();
151                self.update_ui_prompt(ui);
152            }
153            Some(WidgetMessage::KeyDown(KeyCode::ArrowDown)) => {
154                self.console.history_forward();
155                self.update_ui_prompt(ui);
156            }
157            Some(WidgetMessage::KeyDown(KeyCode::PageUp)) => {
158                self.console.history_scroll_up(10);
159                self.update_ui_history(ui);
160            }
161            Some(WidgetMessage::KeyDown(KeyCode::PageDown)) => {
162                self.console.history_scroll_down(10);
163                self.update_ui_history(ui);
164            }
165            Some(WidgetMessage::KeyDown(KeyCode::Enter | KeyCode::NumpadEnter)) => {
166                self.console.enter(cvars);
167                self.update_ui_prompt(ui);
168                self.update_ui_history(ui);
169            }
170            _ => (),
171        }
172    }
173
174    fn update_ui_prompt(&mut self, ui: &mut UserInterface) {
175        ui.send_message(TextMessage::text(
176            self.prompt_text_box,
177            MessageDirection::ToWidget,
178            self.console.prompt.clone(),
179        ));
180    }
181
182    fn update_ui_history(&mut self, ui: &mut UserInterface) {
183        // LATER There should be a cleaner way to measure lines
184        let line_height = 14;
185        // Leave 1 line room for the prompt
186        // LATER This is not exact for tiny windows but good enough for now.
187        let max_lines = (self.height as usize / line_height).saturating_sub(1);
188
189        let hi = self.console.history_view_end;
190        let lo = hi.saturating_sub(max_lines);
191
192        let mut hist = String::new();
193        for line in &self.console.history[lo..hi] {
194            if line.is_input {
195                hist.push_str("> ");
196            }
197            hist.push_str(&line.text);
198            hist.push('\n');
199        }
200
201        ui.send_message(TextMessage::text(
202            self.history,
203            MessageDirection::ToWidget,
204            hist,
205        ));
206    }
207
208    /// Returns true if the console is currently open.
209    pub fn is_open(&self) -> bool {
210        self.is_open
211    }
212
213    /// Open the console.
214    ///
215    /// If your game grabs the mouse, you can save the previous state here
216    /// and get it back when closing.
217    pub fn open(&mut self, ui: &mut UserInterface, was_mouse_grabbed: bool) {
218        self.is_open = true;
219        self.was_mouse_grabbed = was_mouse_grabbed;
220
221        ui.send_message(WidgetMessage::visibility(
222            self.layout,
223            MessageDirection::ToWidget,
224            true,
225        ));
226
227        ui.send_message(WidgetMessage::focus(
228            self.prompt_text_box,
229            MessageDirection::ToWidget,
230        ));
231
232        if self.first_open {
233            // Currently it's not necessary to track the first opening,
234            // the history will be empty so we could just print it when creating the console.
235            // Eventually though, all stdout will be printed in the console
236            // so if the message was at the top, nobody would see it.
237            self.first_open = false;
238            self.console.print("Type 'help' or '?' for basic info");
239            self.update_ui_history(ui);
240        }
241    }
242
243    /// Close the console. Returns whether the mouse was grabbed before opening the console.
244    ///
245    /// It's `#[must_use]` so you don't accidentally forget to restore it.
246    /// You can safely ignore it if you don't grab the mouse.
247    #[must_use]
248    pub fn close(&mut self, ui: &mut UserInterface) -> bool {
249        ui.send_message(WidgetMessage::visibility(
250            self.layout,
251            MessageDirection::ToWidget,
252            false,
253        ));
254        ui.send_message(WidgetMessage::unfocus(
255            self.prompt_text_box,
256            MessageDirection::ToWidget,
257        ));
258
259        self.is_open = false;
260        self.was_mouse_grabbed
261    }
262}