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