Skip to main content

fyrox_ui/
log.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21use crate::{
22    border::BorderBuilder,
23    button::{Button, ButtonMessage},
24    copypasta::ClipboardProvider,
25    core::{
26        color::Color,
27        log::{Log, LogMessage, MessageKind},
28        pool::Handle,
29    },
30    grid::{Column, GridBuilder, Row},
31    menu::{ContextMenuBuilder, MenuItem, MenuItemBuilder, MenuItemContent, MenuItemMessage},
32    message::UiMessage,
33    popup::{Placement, PopupBuilder, PopupMessage},
34    resources,
35    scroll_viewer::{ScrollViewer, ScrollViewerBuilder, ScrollViewerMessage},
36    stack_panel::{StackPanel, StackPanelBuilder},
37    style::{resource::StyleResourceExt, Style},
38    text::{Text, TextBuilder},
39    toggle::{ToggleButton, ToggleButtonMessage},
40    utils::ImageButtonBuilder,
41    widget::{WidgetBuilder, WidgetMessage},
42    window::{Window, WindowAlignment, WindowBuilder, WindowMessage, WindowTitle},
43    BuildContext, HorizontalAlignment, Orientation, RcUiNodeHandle, Thickness, UiNode,
44    UserInterface, VerticalAlignment,
45};
46use fyrox_graph::SceneGraph;
47use fyrox_texture::TextureResource;
48use std::sync::mpsc::Receiver;
49
50struct ContextMenu {
51    menu: RcUiNodeHandle,
52    copy: Handle<MenuItem>,
53    placement_target: Handle<UiNode>,
54}
55
56impl ContextMenu {
57    pub fn new(ctx: &mut BuildContext) -> Self {
58        let copy;
59        let menu = ContextMenuBuilder::new(
60            PopupBuilder::new(WidgetBuilder::new())
61                .with_content(
62                    StackPanelBuilder::new(WidgetBuilder::new().with_child({
63                        copy = MenuItemBuilder::new(WidgetBuilder::new())
64                            .with_content(MenuItemContent::text("Copy"))
65                            .build(ctx);
66                        copy
67                    }))
68                    .build(ctx),
69                )
70                .with_restrict_picking(false),
71        )
72        .build(ctx);
73        let menu = RcUiNodeHandle::new(menu, ctx.sender());
74
75        Self {
76            menu,
77            copy,
78            placement_target: Default::default(),
79        }
80    }
81
82    fn on_copy_clicked(&self, ui: &mut UserInterface) -> Option<()> {
83        let text = ui.find_component::<Text>(self.placement_target)?.1.text();
84        ui.clipboard_mut()?.set_contents(text).ok()
85    }
86
87    pub fn handle_ui_message(&mut self, message: &UiMessage, ui: &mut UserInterface) {
88        if let Some(PopupMessage::Placement(Placement::Cursor(target))) =
89            message.data_from(self.menu.handle())
90        {
91            self.placement_target = *target;
92        } else if let Some(MenuItemMessage::Click) = message.data_from(self.copy) {
93            self.on_copy_clicked(ui);
94        }
95    }
96}
97
98pub struct LogPanel {
99    pub window: Handle<Window>,
100    messages: Handle<StackPanel>,
101    clear: Handle<Button>,
102    receiver: Receiver<LogMessage>,
103    log_info: Handle<ToggleButton>,
104    log_warning: Handle<ToggleButton>,
105    log_error: Handle<ToggleButton>,
106    context_menu: ContextMenu,
107    scroll_viewer: Handle<ScrollViewer>,
108    pub message_count: usize,
109}
110
111impl LogPanel {
112    pub fn new(
113        ctx: &mut BuildContext,
114        message_receiver: Receiver<LogMessage>,
115        clear_icon: Option<TextureResource>,
116        open: bool,
117    ) -> Self {
118        let messages;
119        let clear;
120        let log_info;
121        let log_warning;
122        let log_error;
123        let scroll_viewer;
124
125        let buttons_left = StackPanelBuilder::new(
126            WidgetBuilder::new()
127                .with_horizontal_alignment(HorizontalAlignment::Left)
128                .on_row(0)
129                .on_column(0)
130                .with_child({
131                    log_info = ImageButtonBuilder::default()
132                        .with_tooltip("Enable or disable logging of information messages.")
133                        .with_image_color(Color::WHITE)
134                        .with_is_toggled(Log::is_logging_info())
135                        .with_image(resources::INFO.clone())
136                        .build_toggle(ctx);
137                    log_info
138                })
139                .with_child({
140                    log_warning = ImageButtonBuilder::default()
141                        .with_tooltip("Enable or disable logging of warning messages.")
142                        .with_image_color(Color::WHITE)
143                        .with_is_toggled(Log::is_logging_warning())
144                        .with_image(resources::WARNING.clone())
145                        .build_toggle(ctx);
146                    log_warning
147                })
148                .with_child({
149                    log_error = ImageButtonBuilder::default()
150                        .with_tooltip("Enable or disable logging of error messages.")
151                        .with_image_color(Color::WHITE)
152                        .with_is_toggled(Log::is_logging_error())
153                        .with_image(resources::ERROR.clone())
154                        .build_toggle(ctx);
155                    log_error
156                }),
157        )
158        .with_orientation(Orientation::Horizontal)
159        .build(ctx);
160
161        let buttons_right = StackPanelBuilder::new(
162            WidgetBuilder::new()
163                .with_horizontal_alignment(HorizontalAlignment::Left)
164                .on_row(0)
165                .on_column(2)
166                .with_child({
167                    clear = ImageButtonBuilder::default()
168                        .with_image(clear_icon)
169                        .with_image_color(Color::ORANGE)
170                        .with_tooltip("Clear the log.")
171                        .with_tab_index(Some(0))
172                        .build_button(ctx);
173                    clear
174                }),
175        )
176        .with_orientation(Orientation::Horizontal)
177        .build(ctx);
178
179        let buttons = GridBuilder::new(
180            WidgetBuilder::new()
181                .with_child(buttons_left)
182                .with_child(buttons_right),
183        )
184        .add_column(Column::auto())
185        .add_column(Column::stretch())
186        .add_column(Column::auto())
187        .add_row(Row::auto())
188        .build(ctx);
189
190        let window = WindowBuilder::new(
191            WidgetBuilder::new()
192                .with_width(400.0)
193                .with_height(200.0)
194                .with_name("LogPanel"),
195        )
196        .open(open)
197        .with_title(WindowTitle::text("Message Log"))
198        .with_tab_label("Log")
199        .with_content(
200            GridBuilder::new(WidgetBuilder::new().with_child(buttons).with_child({
201                scroll_viewer = ScrollViewerBuilder::new(
202                    WidgetBuilder::new()
203                        .on_row(1)
204                        .on_column(0)
205                        .with_margin(Thickness::uniform(3.0)),
206                )
207                .with_content({
208                    messages = StackPanelBuilder::new(
209                        WidgetBuilder::new().with_margin(Thickness::uniform(1.0)),
210                    )
211                    .build(ctx);
212                    messages
213                })
214                .with_horizontal_scroll_allowed(true)
215                .with_vertical_scroll_allowed(true)
216                .build(ctx);
217                scroll_viewer
218            }))
219            .add_row(Row::auto())
220            .add_row(Row::stretch())
221            .add_column(Column::stretch())
222            .build(ctx),
223        )
224        .build(ctx);
225
226        let context_menu = ContextMenu::new(ctx);
227
228        Self {
229            window,
230            messages,
231            clear,
232            receiver: message_receiver,
233            log_info,
234            log_warning,
235            log_error,
236            context_menu,
237            message_count: 0,
238            scroll_viewer,
239        }
240    }
241
242    pub fn destroy(self, ui: &UserInterface) {
243        ui.send(self.context_menu.menu.handle(), WidgetMessage::Remove);
244        ui.send(self.window, WidgetMessage::Remove);
245    }
246
247    pub fn open(&self, ui: &UserInterface) {
248        ui.send(
249            self.window,
250            WindowMessage::Open {
251                alignment: WindowAlignment::Center,
252                modal: false,
253                focus_content: true,
254            },
255        );
256    }
257
258    pub fn close(&self, ui: &UserInterface) {
259        ui.send(self.window, WindowMessage::Close);
260    }
261
262    pub fn handle_ui_message(&mut self, message: &UiMessage, ui: &mut UserInterface) {
263        if let Some(ButtonMessage::Click) = message.data_from(self.clear) {
264            ui.send(self.messages, WidgetMessage::ReplaceChildren(vec![]));
265        } else if let Some(ToggleButtonMessage::Toggled(value)) = message.data_from(self.log_info) {
266            Log::set_log_info(*value);
267        } else if let Some(ToggleButtonMessage::Toggled(value)) =
268            message.data_from(self.log_warning)
269        {
270            Log::set_log_warning(*value);
271        } else if let Some(ToggleButtonMessage::Toggled(value)) = message.data_from(self.log_error)
272        {
273            Log::set_log_error(*value);
274        }
275
276        self.context_menu.handle_ui_message(message, ui);
277    }
278
279    pub fn update(&mut self, max_log_entries: usize, ui: &mut UserInterface) -> bool {
280        let existing_items = ui[self.messages].children();
281
282        let mut count = existing_items.len();
283
284        if count > max_log_entries {
285            let delta = count - max_log_entries;
286            // Remove every item in the head of the list of entries to keep the amount of entries
287            // in the limits.
288            //
289            // TODO: This is suboptimal, because it creates a message per each excessive entry, which
290            //  might be slow to process in case of large amount of messages.
291            for item in existing_items.iter().take(delta) {
292                ui.send(*item, WidgetMessage::Remove);
293            }
294
295            count -= delta;
296        }
297
298        let mut item_to_bring_into_view = Handle::NONE;
299
300        let mut received_anything = false;
301
302        while let Ok(msg) = self.receiver.try_recv() {
303            self.message_count += 1;
304            received_anything = true;
305
306            let mut text = format!("[{:.2}s] {}", msg.time.as_secs_f32(), msg.content);
307            if let Some(ch) = text.chars().last() {
308                if ch == '\n' {
309                    text.pop();
310                }
311            }
312
313            let ctx = &mut ui.build_ctx();
314            let item = BorderBuilder::new(
315                WidgetBuilder::new()
316                    .with_context_menu(self.context_menu.menu.clone())
317                    .with_background(if count.is_multiple_of(2) {
318                        ctx.style.property(Style::BRUSH_LIGHT)
319                    } else {
320                        ctx.style.property(Style::BRUSH_DARK)
321                    })
322                    .with_child(
323                        TextBuilder::new(
324                            WidgetBuilder::new()
325                                .with_margin(Thickness::uniform(2.0))
326                                .with_foreground(match msg.kind {
327                                    MessageKind::Information => {
328                                        ctx.style.property(Style::BRUSH_INFORMATION)
329                                    }
330                                    MessageKind::Warning => {
331                                        ctx.style.property(Style::BRUSH_WARNING)
332                                    }
333                                    MessageKind::Error => ctx.style.property(Style::BRUSH_ERROR),
334                                }),
335                        )
336                        .with_vertical_text_alignment(VerticalAlignment::Center)
337                        .with_text(text)
338                        .build(ctx),
339                    ),
340            )
341            .build(ctx);
342
343            ui.send(item, WidgetMessage::link_with(self.messages));
344
345            item_to_bring_into_view = item;
346
347            count += 1;
348        }
349
350        if item_to_bring_into_view.is_some() {
351            ui.send(
352                self.scroll_viewer,
353                ScrollViewerMessage::BringIntoView(item_to_bring_into_view.to_base()),
354            );
355        }
356
357        received_anything
358    }
359}