1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#![doc = include_str!("../README.md")]
#![warn(missing_docs)]

use fyrox_ui::{
    border::BorderBuilder,
    brush::Brush,
    core::{color::Color, pool::Handle},
    formatted_text::WrapMode,
    message::{KeyCode, MessageDirection, UiMessage},
    stack_panel::StackPanelBuilder,
    text::{TextBuilder, TextMessage},
    text_box::{TextBoxBuilder, TextCommitMode},
    widget::{WidgetBuilder, WidgetMessage},
    Orientation, UiNode, UserInterface, VerticalAlignment,
};

use cvars::SetGet;
use cvars_console::Console;

/// In-game console for the Fyrox game engine.
pub struct FyroxConsole {
    is_open: bool,
    first_open: bool,
    was_mouse_grabbed: bool,
    console: Console,
    height: f32,
    history: Handle<UiNode>,
    prompt_text_box: Handle<UiNode>,
    layout: Handle<UiNode>,
}

impl FyroxConsole {
    /// Create a new console. Build its UI but keep it closed.
    pub fn new(ui: &mut UserInterface) -> Self {
        let history = TextBuilder::new(WidgetBuilder::new())
            // Word wrap doesn't work if there's an extremely long word.
            .with_wrap(WrapMode::Letter)
            .build(&mut ui.build_ctx());

        let prompt_arrow = TextBuilder::new(WidgetBuilder::new())
            .with_text("> ")
            .build(&mut ui.build_ctx());

        let prompt_text_box = TextBoxBuilder::new(WidgetBuilder::new())
            .with_text_commit_mode(TextCommitMode::Immediate)
            .with_skip_chars(vec!['-', '_'])
            .build(&mut ui.build_ctx());

        let prompt_line = StackPanelBuilder::new(
            WidgetBuilder::new().with_children([prompt_arrow, prompt_text_box]),
        )
        .with_orientation(Orientation::Horizontal)
        .build(&mut ui.build_ctx());

        // StackPanel doesn't support colored background so we wrap it in a Border.
        let layout = BorderBuilder::new(
            WidgetBuilder::new()
                .with_visibility(false)
                .with_background(Brush::Solid(Color::BLACK.with_new_alpha(220)))
                .with_child(
                    StackPanelBuilder::new(
                        WidgetBuilder::new()
                            .with_vertical_alignment(VerticalAlignment::Bottom)
                            .with_children([history, prompt_line]),
                    )
                    .with_orientation(Orientation::Vertical)
                    .build(&mut ui.build_ctx()),
                ),
        )
        .build(&mut ui.build_ctx());

        FyroxConsole {
            is_open: false,
            first_open: true,
            was_mouse_grabbed: false,
            console: Console::new(),
            height: 0.0,
            history,
            prompt_text_box,
            layout,
        }
    }

    /// Call this when the window is resized.
    pub fn resized(&mut self, ui: &mut UserInterface, width: f32, height: f32) {
        ui.send_message(WidgetMessage::width(
            self.layout,
            MessageDirection::ToWidget,
            width,
        ));

        self.height = height / 2.0;
        ui.send_message(WidgetMessage::height(
            self.layout,
            MessageDirection::ToWidget,
            self.height,
        ));

        // This actually goes beyond the screen but who cares.
        // It, however, still won't let me put the cursor at the end by clicking after the text:
        // https://github.com/FyroxEngine/Fyrox/issues/361
        ui.send_message(WidgetMessage::width(
            self.prompt_text_box,
            MessageDirection::ToWidget,
            width,
        ));

        // The number of lines that can fit might have changed - reprint history.
        self.update_ui_history(ui);
    }

    /// Call this for every Fyrox UI message. The console will only react to them if it's open.
    ///
    /// # Example
    /// ```rust,ignore
    /// while let Some(msg) = engine.user_interface.poll_message() {
    ///     console.ui_message(&msg);
    ///     // ... Whatever else you do with UI messages ...
    /// }
    /// ```
    pub fn ui_message(&mut self, ui: &mut UserInterface, cvars: &mut impl SetGet, msg: &UiMessage) {
        if !self.is_open || msg.destination != self.prompt_text_box {
            return;
        }

        // We could just listen for KeyboardInput and get the text from the prompt via
        // ```
        // let node = ui.node(self.prompt_text_box);
        // let text = node.query_component::<TextBox>().unwrap().text();
        // ```
        // But this is the intended way to use the UI, even if it's more verbose.
        // At least it should reduce issues with the prompt reacting to some keys
        // but not others given KeyboardInput doesn't require focus.
        //
        // Note that it might still be better to read the text from the UI as the souce of truth
        // because right now the console doesn't know about any text we set from code on init.
        if let Some(TextMessage::Text(text)) = msg.data() {
            self.console.prompt = text.to_owned();
        }

        match msg.data() {
            Some(WidgetMessage::Unfocus) => {
                // As long as the console is open, always keep the prompt focused
                ui.send_message(WidgetMessage::focus(
                    self.prompt_text_box,
                    MessageDirection::ToWidget,
                ));
            }
            Some(WidgetMessage::KeyDown(KeyCode::Up)) => {
                self.console.history_back();
                self.update_ui_prompt(ui);
            }
            Some(WidgetMessage::KeyDown(KeyCode::Down)) => {
                self.console.history_forward();
                self.update_ui_prompt(ui);
            }
            Some(WidgetMessage::KeyDown(KeyCode::PageUp)) => {
                self.console.history_scroll_up(10);
                self.update_ui_history(ui);
            }
            Some(WidgetMessage::KeyDown(KeyCode::PageDown)) => {
                self.console.history_scroll_down(10);
                self.update_ui_history(ui);
            }
            Some(WidgetMessage::KeyDown(KeyCode::Return | KeyCode::NumpadEnter)) => {
                self.console.enter(cvars);
                self.update_ui_prompt(ui);
                self.update_ui_history(ui);
            }
            _ => (),
        }
    }

    fn update_ui_prompt(&mut self, ui: &mut UserInterface) {
        ui.send_message(TextMessage::text(
            self.prompt_text_box,
            MessageDirection::ToWidget,
            self.console.prompt.clone(),
        ));
    }

    fn update_ui_history(&mut self, ui: &mut UserInterface) {
        // LATER There should be a cleaner way to measure lines
        let line_height = 14;
        // Leave 1 line room for the prompt
        // LATER This is not exact for tiny windows but good enough for now.
        let max_lines = (self.height as usize / line_height).saturating_sub(1);

        let hi = self.console.history_view_end;
        let lo = hi.saturating_sub(max_lines);

        let mut hist = String::new();
        for line in &self.console.history[lo..hi] {
            if line.is_input {
                hist.push_str("> ");
            }
            hist.push_str(&line.text);
            hist.push('\n');
        }

        ui.send_message(TextMessage::text(
            self.history,
            MessageDirection::ToWidget,
            hist,
        ));
    }

    /// Returns true if the console is currently open.
    pub fn is_open(&self) -> bool {
        self.is_open
    }

    /// Open the console.
    ///
    /// If your game grabs the mouse, you can save the previous state here
    /// and get it back when closing.
    pub fn open(&mut self, ui: &mut UserInterface, was_mouse_grabbed: bool) {
        self.is_open = true;
        self.was_mouse_grabbed = was_mouse_grabbed;

        ui.send_message(WidgetMessage::visibility(
            self.layout,
            MessageDirection::ToWidget,
            true,
        ));

        ui.send_message(WidgetMessage::focus(
            self.prompt_text_box,
            MessageDirection::ToWidget,
        ));

        if self.first_open {
            // Currently it's not necessary to track the first opening,
            // the history will be empty so we could just print it when creating the console.
            // Eventually though, all stdout will be printed in the console
            // so if the message was at the top, nobody would see it.
            self.first_open = false;
            self.console.print("Type 'help' or '?' for basic info");
            self.update_ui_history(ui);
        }
    }

    /// Close the console. Returns whether the mouse was grabbed before opening the console.
    ///
    /// It's `#[must_use]` so you don't accidentally forget to restore it.
    /// You can safely ignore it if you don't grab the mouse.
    #[must_use]
    pub fn close(&mut self, ui: &mut UserInterface) -> bool {
        ui.send_message(WidgetMessage::visibility(
            self.layout,
            MessageDirection::ToWidget,
            false,
        ));
        ui.send_message(WidgetMessage::unfocus(
            self.prompt_text_box,
            MessageDirection::ToWidget,
        ));

        self.is_open = false;
        self.was_mouse_grabbed
    }
}