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    dropdown_list::{DropdownListBuilder, DropdownListMessage},
26    formatted_text::WrapMode,
27    grid::{Column, GridBuilder, Row},
28    list_view::{ListView, ListViewBuilder, ListViewMessage},
29    menu::{ContextMenuBuilder, MenuItemBuilder, MenuItemContent, MenuItemMessage},
30    message::{MessageDirection, UiMessage},
31    popup::{Placement, PopupBuilder, PopupMessage},
32    scroll_viewer::ScrollViewerBuilder,
33    stack_panel::StackPanelBuilder,
34    style::{resource::StyleResourceExt, Style},
35    text::{Text, TextBuilder},
36    utils::{make_dropdown_list_option, make_image_button_with_tooltip},
37    widget::{WidgetBuilder, WidgetMessage},
38    window::{WindowBuilder, WindowMessage, WindowTitle},
39    BuildContext, HorizontalAlignment, Orientation, RcUiNodeHandle, Thickness, UiNode,
40    UserInterface,
41};
42use fyrox_core::{
43    log::{LogMessage, MessageKind},
44    pool::Handle,
45};
46use fyrox_graph::BaseSceneGraph;
47use fyrox_texture::TextureResource;
48use std::sync::mpsc::Receiver;
49
50struct ContextMenu {
51    menu: RcUiNodeHandle,
52    copy: Handle<UiNode>,
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()).with_content(
61                StackPanelBuilder::new(WidgetBuilder::new().with_child({
62                    copy = MenuItemBuilder::new(WidgetBuilder::new())
63                        .with_content(MenuItemContent::text("Copy"))
64                        .build(ctx);
65                    copy
66                }))
67                .build(ctx),
68            ),
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(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_content(
132            GridBuilder::new(
133                WidgetBuilder::new()
134                    .with_child(
135                        StackPanelBuilder::new(
136                            WidgetBuilder::new()
137                                .with_horizontal_alignment(HorizontalAlignment::Left)
138                                .on_row(0)
139                                .on_column(0)
140                                .with_child({
141                                    clear = make_image_button_with_tooltip(
142                                        ctx,
143                                        24.0,
144                                        24.0,
145                                        clear_icon,
146                                        "Clear the log.",
147                                        Some(0),
148                                    );
149                                    clear
150                                })
151                                .with_child({
152                                    severity_list = DropdownListBuilder::new(
153                                        WidgetBuilder::new()
154                                            .with_tab_index(Some(1))
155                                            .with_width(120.0)
156                                            .with_margin(Thickness::uniform(1.0)),
157                                    )
158                                    .with_items(vec![
159                                        make_dropdown_list_option(ctx, "Info+"),
160                                        make_dropdown_list_option(ctx, "Warnings+"),
161                                        make_dropdown_list_option(ctx, "Errors"),
162                                    ])
163                                    // Warnings+
164                                    .with_selected(1)
165                                    .build(ctx);
166                                    severity_list
167                                }),
168                        )
169                        .with_orientation(Orientation::Horizontal)
170                        .build(ctx),
171                    )
172                    .with_child({
173                        messages = ListViewBuilder::new(
174                            WidgetBuilder::new()
175                                .with_margin(Thickness::uniform(1.0))
176                                .on_row(1)
177                                .on_column(0),
178                        )
179                        .with_scroll_viewer(
180                            ScrollViewerBuilder::new(
181                                WidgetBuilder::new().with_margin(Thickness::uniform(3.0)),
182                            )
183                            .with_horizontal_scroll_allowed(true)
184                            .with_vertical_scroll_allowed(true)
185                            .build(ctx),
186                        )
187                        .build(ctx);
188                        messages
189                    }),
190            )
191            .add_row(Row::strict(26.0))
192            .add_row(Row::stretch())
193            .add_column(Column::stretch())
194            .build(ctx),
195        )
196        .build(ctx);
197
198        let context_menu = ContextMenu::new(ctx);
199
200        Self {
201            window,
202            messages,
203            clear,
204            receiver: message_receiver,
205            severity: MessageKind::Warning,
206            severity_list,
207            context_menu,
208            message_count: 0,
209        }
210    }
211
212    pub fn destroy(self, ui: &UserInterface) {
213        ui.send_message(WidgetMessage::remove(
214            self.context_menu.menu.handle(),
215            MessageDirection::ToWidget,
216        ));
217        ui.send_message(WidgetMessage::remove(
218            self.window,
219            MessageDirection::ToWidget,
220        ));
221    }
222
223    pub fn open(&self, ui: &UserInterface) {
224        ui.send_message(WindowMessage::open(
225            self.window,
226            MessageDirection::ToWidget,
227            true,
228            true,
229        ));
230    }
231
232    pub fn close(&self, ui: &UserInterface) {
233        ui.send_message(WindowMessage::close(
234            self.window,
235            MessageDirection::ToWidget,
236        ));
237    }
238
239    pub fn handle_ui_message(&mut self, message: &UiMessage, ui: &mut UserInterface) {
240        if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
241            if message.destination() == self.clear {
242                ui.send_message(ListViewMessage::items(
243                    self.messages,
244                    MessageDirection::ToWidget,
245                    vec![],
246                ));
247            }
248        } else if let Some(DropdownListMessage::SelectionChanged(Some(idx))) =
249            message.data::<DropdownListMessage>()
250        {
251            if message.destination() == self.severity_list
252                && message.direction() == MessageDirection::FromWidget
253            {
254                match idx {
255                    0 => self.severity = MessageKind::Information,
256                    1 => self.severity = MessageKind::Warning,
257                    2 => self.severity = MessageKind::Error,
258                    _ => (),
259                };
260            }
261        }
262
263        self.context_menu.handle_ui_message(message, ui);
264    }
265
266    pub fn update(&mut self, max_log_entries: usize, ui: &mut UserInterface) -> bool {
267        let existing_items = ui
268            .node(self.messages)
269            .cast::<ListView>()
270            .map(|v| v.items())
271            .unwrap();
272
273        let mut count = existing_items.len();
274
275        if count > max_log_entries {
276            let delta = count - max_log_entries;
277            // Remove every item in the head of the list of entries to keep the amount of entries
278            // in the limits.
279            //
280            // TODO: This is suboptimal, because it creates a message per each excessive entry, which
281            //  might be slow to process in case of large amount of messages.
282            for item in existing_items.iter().take(delta) {
283                ui.send_message(ListViewMessage::remove_item(
284                    self.messages,
285                    MessageDirection::ToWidget,
286                    *item,
287                ));
288            }
289
290            count -= delta;
291        }
292
293        let mut item_to_bring_into_view = Handle::NONE;
294
295        let mut received_anything = false;
296
297        while let Ok(msg) = self.receiver.try_recv() {
298            if msg.kind < self.severity {
299                continue;
300            }
301
302            self.message_count += 1;
303            received_anything = true;
304
305            let text = format!("[{:.2}s] {}", msg.time.as_secs_f32(), msg.content);
306
307            let ctx = &mut ui.build_ctx();
308            let item = BorderBuilder::new(
309                WidgetBuilder::new()
310                    .with_background(if count % 2 == 0 {
311                        ctx.style.property(Style::BRUSH_LIGHT)
312                    } else {
313                        ctx.style.property(Style::BRUSH_DARK)
314                    })
315                    .with_child(
316                        TextBuilder::new(
317                            WidgetBuilder::new()
318                                .with_context_menu(self.context_menu.menu.clone())
319                                .with_margin(Thickness::uniform(1.0))
320                                .with_foreground(match msg.kind {
321                                    MessageKind::Information => {
322                                        ctx.style.property(Style::BRUSH_INFORMATION)
323                                    }
324                                    MessageKind::Warning => {
325                                        ctx.style.property(Style::BRUSH_WARNING)
326                                    }
327                                    MessageKind::Error => ctx.style.property(Style::BRUSH_ERROR),
328                                }),
329                        )
330                        .with_text(text)
331                        .with_wrap(WrapMode::Word)
332                        .build(ctx),
333                    ),
334            )
335            .build(ctx);
336
337            ui.send_message(ListViewMessage::add_item(
338                self.messages,
339                MessageDirection::ToWidget,
340                item,
341            ));
342
343            item_to_bring_into_view = item;
344
345            count += 1;
346        }
347
348        if item_to_bring_into_view.is_some() {
349            ui.send_message(ListViewMessage::bring_item_into_view(
350                self.messages,
351                MessageDirection::ToWidget,
352                item_to_bring_into_view,
353            ));
354        }
355
356        received_anything
357    }
358}