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