1use 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 .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 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}