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::ButtonMessage,
24    copypasta::ClipboardProvider,
25    core::{
26        log::{LogMessage, MessageKind},
27        pool::Handle,
28    },
29    dropdown_list::{DropdownListBuilder, DropdownListMessage},
30    grid::{Column, GridBuilder, Row},
31    menu::{ContextMenuBuilder, MenuItemBuilder, MenuItemContent, MenuItemMessage},
32    message::{MessageDirection, UiMessage},
33    popup::{Placement, PopupBuilder, PopupMessage},
34    scroll_viewer::{ScrollViewerBuilder, ScrollViewerMessage},
35    stack_panel::StackPanelBuilder,
36    style::{resource::StyleResourceExt, Style},
37    text::{Text, TextBuilder},
38    utils::{make_dropdown_list_option, make_image_button_with_tooltip},
39    widget::{WidgetBuilder, WidgetMessage},
40    window::{WindowBuilder, WindowMessage, WindowTitle},
41    BuildContext, HorizontalAlignment, Orientation, RcUiNodeHandle, Thickness, UiNode,
42    UserInterface, VerticalAlignment,
43};
44use fyrox_graph::{BaseSceneGraph, SceneGraphNode};
45use fyrox_texture::TextureResource;
46use std::sync::mpsc::Receiver;
47
48struct ContextMenu {
49    menu: RcUiNodeHandle,
50    copy: Handle<UiNode>,
51    placement_target: Handle<UiNode>,
52}
53
54impl ContextMenu {
55    pub fn new(ctx: &mut BuildContext) -> Self {
56        let copy;
57        let menu = ContextMenuBuilder::new(
58            PopupBuilder::new(WidgetBuilder::new())
59                .with_content(
60                    StackPanelBuilder::new(WidgetBuilder::new().with_child({
61                        copy = MenuItemBuilder::new(WidgetBuilder::new())
62                            .with_content(MenuItemContent::text("Copy"))
63                            .build(ctx);
64                        copy
65                    }))
66                    .build(ctx),
67                )
68                .with_restrict_picking(false),
69        )
70        .build(ctx);
71        let menu = RcUiNodeHandle::new(menu, ctx.sender());
72
73        Self {
74            menu,
75            copy,
76            placement_target: Default::default(),
77        }
78    }
79
80    pub fn handle_ui_message(&mut self, message: &UiMessage, ui: &mut UserInterface) {
81        if let Some(PopupMessage::Placement(Placement::Cursor(target))) = message.data() {
82            if message.destination() == self.menu.handle() {
83                self.placement_target = *target;
84            }
85        } else if let Some(MenuItemMessage::Click) = message.data() {
86            if message.destination() == self.copy {
87                if let Some(field) = ui
88                    .try_get_node(self.placement_target)
89                    .and_then(|n| n.query_component::<Text>())
90                {
91                    let text = field.text();
92                    if let Some(mut clipboard) = ui.clipboard_mut() {
93                        let _ = clipboard.set_contents(text);
94                    }
95                }
96            }
97        }
98    }
99}
100
101pub struct LogPanel {
102    pub window: Handle<UiNode>,
103    messages: Handle<UiNode>,
104    clear: Handle<UiNode>,
105    receiver: Receiver<LogMessage>,
106    severity: MessageKind,
107    severity_list: Handle<UiNode>,
108    context_menu: ContextMenu,
109    pub message_count: usize,
110}
111
112impl LogPanel {
113    pub fn new(
114        ctx: &mut BuildContext,
115        message_receiver: Receiver<LogMessage>,
116        clear_icon: Option<TextureResource>,
117        open: bool,
118    ) -> Self {
119        let messages;
120        let clear;
121        let severity_list;
122        let window = WindowBuilder::new(
123            WidgetBuilder::new()
124                .with_width(400.0)
125                .with_height(200.0)
126                .with_name("LogPanel"),
127        )
128        .can_minimize(false)
129        .open(open)
130        .with_title(WindowTitle::text("Message Log"))
131        .with_tab_label("Log")
132        .with_content(
133            GridBuilder::new(
134                WidgetBuilder::new()
135                    .with_child(
136                        StackPanelBuilder::new(
137                            WidgetBuilder::new()
138                                .with_horizontal_alignment(HorizontalAlignment::Left)
139                                .on_row(0)
140                                .on_column(0)
141                                .with_child({
142                                    clear = make_image_button_with_tooltip(
143                                        ctx,
144                                        24.0,
145                                        24.0,
146                                        clear_icon,
147                                        "Clear the log.",
148                                        Some(0),
149                                    );
150                                    clear
151                                })
152                                .with_child({
153                                    severity_list = DropdownListBuilder::new(
154                                        WidgetBuilder::new()
155                                            .with_tab_index(Some(1))
156                                            .with_width(120.0)
157                                            .with_margin(Thickness::uniform(1.0)),
158                                    )
159                                    .with_items(vec![
160                                        make_dropdown_list_option(ctx, "Info+"),
161                                        make_dropdown_list_option(ctx, "Warnings+"),
162                                        make_dropdown_list_option(ctx, "Errors"),
163                                    ])
164                                    // Warnings+
165                                    .with_selected(1)
166                                    .build(ctx);
167                                    severity_list
168                                }),
169                        )
170                        .with_orientation(Orientation::Horizontal)
171                        .build(ctx),
172                    )
173                    .with_child(
174                        ScrollViewerBuilder::new(
175                            WidgetBuilder::new()
176                                .on_row(1)
177                                .on_column(0)
178                                .with_margin(Thickness::uniform(3.0)),
179                        )
180                        .with_content({
181                            messages = StackPanelBuilder::new(
182                                WidgetBuilder::new().with_margin(Thickness::uniform(1.0)),
183                            )
184                            .build(ctx);
185                            messages
186                        })
187                        .with_horizontal_scroll_allowed(true)
188                        .with_vertical_scroll_allowed(true)
189                        .build(ctx),
190                    ),
191            )
192            .add_row(Row::strict(26.0))
193            .add_row(Row::stretch())
194            .add_column(Column::stretch())
195            .build(ctx),
196        )
197        .build(ctx);
198
199        let context_menu = ContextMenu::new(ctx);
200
201        Self {
202            window,
203            messages,
204            clear,
205            receiver: message_receiver,
206            severity: MessageKind::Warning,
207            severity_list,
208            context_menu,
209            message_count: 0,
210        }
211    }
212
213    pub fn destroy(self, ui: &UserInterface) {
214        ui.send_message(WidgetMessage::remove(
215            self.context_menu.menu.handle(),
216            MessageDirection::ToWidget,
217        ));
218        ui.send_message(WidgetMessage::remove(
219            self.window,
220            MessageDirection::ToWidget,
221        ));
222    }
223
224    pub fn open(&self, ui: &UserInterface) {
225        ui.send_message(WindowMessage::open(
226            self.window,
227            MessageDirection::ToWidget,
228            true,
229            true,
230        ));
231    }
232
233    pub fn close(&self, ui: &UserInterface) {
234        ui.send_message(WindowMessage::close(
235            self.window,
236            MessageDirection::ToWidget,
237        ));
238    }
239
240    pub fn handle_ui_message(&mut self, message: &UiMessage, ui: &mut UserInterface) {
241        if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
242            if message.destination() == self.clear {
243                ui.send_message(WidgetMessage::replace_children(
244                    self.messages,
245                    MessageDirection::ToWidget,
246                    vec![],
247                ));
248            }
249        } else if let Some(DropdownListMessage::SelectionChanged(Some(idx))) =
250            message.data::<DropdownListMessage>()
251        {
252            if message.destination() == self.severity_list
253                && message.direction() == MessageDirection::FromWidget
254            {
255                match idx {
256                    0 => self.severity = MessageKind::Information,
257                    1 => self.severity = MessageKind::Warning,
258                    2 => self.severity = MessageKind::Error,
259                    _ => (),
260                };
261            }
262        }
263
264        self.context_menu.handle_ui_message(message, ui);
265    }
266
267    pub fn update(&mut self, max_log_entries: usize, ui: &mut UserInterface) -> bool {
268        let existing_items = ui.node(self.messages).children();
269
270        let mut count = existing_items.len();
271
272        if count > max_log_entries {
273            let delta = count - max_log_entries;
274            // Remove every item in the head of the list of entries to keep the amount of entries
275            // in the limits.
276            //
277            // TODO: This is suboptimal, because it creates a message per each excessive entry, which
278            //  might be slow to process in case of large amount of messages.
279            for item in existing_items.iter().take(delta) {
280                ui.send_message(WidgetMessage::remove(*item, MessageDirection::ToWidget));
281            }
282
283            count -= delta;
284        }
285
286        let mut item_to_bring_into_view = Handle::NONE;
287
288        let mut received_anything = false;
289
290        while let Ok(msg) = self.receiver.try_recv() {
291            if msg.kind < self.severity {
292                continue;
293            }
294
295            self.message_count += 1;
296            received_anything = true;
297
298            let mut text = format!("[{:.2}s] {}", msg.time.as_secs_f32(), msg.content);
299            if let Some(ch) = text.chars().last() {
300                if ch == '\n' {
301                    text.pop();
302                }
303            }
304
305            let ctx = &mut ui.build_ctx();
306            let item = BorderBuilder::new(
307                WidgetBuilder::new()
308                    .with_background(if count % 2 == 0 {
309                        ctx.style.property(Style::BRUSH_LIGHT)
310                    } else {
311                        ctx.style.property(Style::BRUSH_DARK)
312                    })
313                    .with_child(
314                        TextBuilder::new(
315                            WidgetBuilder::new()
316                                .with_context_menu(self.context_menu.menu.clone())
317                                .with_margin(Thickness::uniform(2.0))
318                                .with_foreground(match msg.kind {
319                                    MessageKind::Information => {
320                                        ctx.style.property(Style::BRUSH_INFORMATION)
321                                    }
322                                    MessageKind::Warning => {
323                                        ctx.style.property(Style::BRUSH_WARNING)
324                                    }
325                                    MessageKind::Error => ctx.style.property(Style::BRUSH_ERROR),
326                                }),
327                        )
328                        .with_vertical_text_alignment(VerticalAlignment::Center)
329                        .with_text(text)
330                        .build(ctx),
331                    ),
332            )
333            .build(ctx);
334
335            ui.send_message(WidgetMessage::link(
336                item,
337                MessageDirection::ToWidget,
338                self.messages,
339            ));
340
341            item_to_bring_into_view = item;
342
343            count += 1;
344        }
345
346        if item_to_bring_into_view.is_some() {
347            ui.send_message(ScrollViewerMessage::bring_into_view(
348                self.messages,
349                MessageDirection::ToWidget,
350                item_to_bring_into_view,
351            ));
352        }
353
354        received_anything
355    }
356}