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