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