use crate::{
border::BorderBuilder,
button::{Button, ButtonMessage},
copypasta::ClipboardProvider,
core::{
color::Color,
log::{Log, LogMessage, MessageKind},
pool::Handle,
},
grid::{Column, GridBuilder, Row},
menu::{ContextMenuBuilder, MenuItem, MenuItemBuilder, MenuItemContent, MenuItemMessage},
message::UiMessage,
popup::{Placement, PopupBuilder, PopupMessage},
resources,
scroll_viewer::{ScrollViewer, ScrollViewerBuilder, ScrollViewerMessage},
stack_panel::{StackPanel, StackPanelBuilder},
style::{resource::StyleResourceExt, Style},
text::{Text, TextBuilder},
toggle::{ToggleButton, ToggleButtonMessage},
utils::ImageButtonBuilder,
widget::{WidgetBuilder, WidgetMessage},
window::{Window, WindowAlignment, WindowBuilder, WindowMessage, WindowTitle},
BuildContext, HorizontalAlignment, Orientation, RcUiNodeHandle, Thickness, UiNode,
UserInterface, VerticalAlignment,
};
use fyrox_graph::SceneGraph;
use fyrox_texture::TextureResource;
use std::sync::mpsc::Receiver;
struct ContextMenu {
menu: RcUiNodeHandle,
copy: Handle<MenuItem>,
placement_target: Handle<UiNode>,
}
impl ContextMenu {
pub fn new(ctx: &mut BuildContext) -> Self {
let copy;
let menu = ContextMenuBuilder::new(
PopupBuilder::new(WidgetBuilder::new())
.with_content(
StackPanelBuilder::new(WidgetBuilder::new().with_child({
copy = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(MenuItemContent::text("Copy"))
.build(ctx);
copy
}))
.build(ctx),
)
.with_restrict_picking(false),
)
.build(ctx);
let menu = RcUiNodeHandle::new(menu, ctx.sender());
Self {
menu,
copy,
placement_target: Default::default(),
}
}
fn on_copy_clicked(&self, ui: &mut UserInterface) -> Option<()> {
let text = ui.find_component::<Text>(self.placement_target)?.1.text();
ui.clipboard_mut()?.set_contents(text).ok()
}
pub fn handle_ui_message(&mut self, message: &UiMessage, ui: &mut UserInterface) {
if let Some(PopupMessage::Placement(Placement::Cursor(target))) =
message.data_from(self.menu.handle())
{
self.placement_target = *target;
} else if let Some(MenuItemMessage::Click) = message.data_from(self.copy) {
self.on_copy_clicked(ui);
}
}
}
pub struct LogPanel {
pub window: Handle<Window>,
messages: Handle<StackPanel>,
clear: Handle<Button>,
receiver: Receiver<LogMessage>,
log_info: Handle<ToggleButton>,
log_warning: Handle<ToggleButton>,
log_error: Handle<ToggleButton>,
context_menu: ContextMenu,
scroll_viewer: Handle<ScrollViewer>,
pub message_count: usize,
}
impl LogPanel {
pub fn new(
ctx: &mut BuildContext,
message_receiver: Receiver<LogMessage>,
clear_icon: Option<TextureResource>,
open: bool,
) -> Self {
let messages;
let clear;
let log_info;
let log_warning;
let log_error;
let scroll_viewer;
let buttons_left = StackPanelBuilder::new(
WidgetBuilder::new()
.with_horizontal_alignment(HorizontalAlignment::Left)
.on_row(0)
.on_column(0)
.with_child({
log_info = ImageButtonBuilder::default()
.with_tooltip("Enable or disable logging of information messages.")
.with_image_color(Color::WHITE)
.with_is_toggled(Log::is_logging_info())
.with_image(resources::INFO.clone())
.build_toggle(ctx);
log_info
})
.with_child({
log_warning = ImageButtonBuilder::default()
.with_tooltip("Enable or disable logging of warning messages.")
.with_image_color(Color::WHITE)
.with_is_toggled(Log::is_logging_warning())
.with_image(resources::WARNING.clone())
.build_toggle(ctx);
log_warning
})
.with_child({
log_error = ImageButtonBuilder::default()
.with_tooltip("Enable or disable logging of error messages.")
.with_image_color(Color::WHITE)
.with_is_toggled(Log::is_logging_error())
.with_image(resources::ERROR.clone())
.build_toggle(ctx);
log_error
}),
)
.with_orientation(Orientation::Horizontal)
.build(ctx);
let buttons_right = StackPanelBuilder::new(
WidgetBuilder::new()
.with_horizontal_alignment(HorizontalAlignment::Left)
.on_row(0)
.on_column(2)
.with_child({
clear = ImageButtonBuilder::default()
.with_image(clear_icon)
.with_image_color(Color::ORANGE)
.with_tooltip("Clear the log.")
.with_tab_index(Some(0))
.build_button(ctx);
clear
}),
)
.with_orientation(Orientation::Horizontal)
.build(ctx);
let buttons = GridBuilder::new(
WidgetBuilder::new()
.with_child(buttons_left)
.with_child(buttons_right),
)
.add_column(Column::auto())
.add_column(Column::stretch())
.add_column(Column::auto())
.add_row(Row::auto())
.build(ctx);
let window = WindowBuilder::new(
WidgetBuilder::new()
.with_width(400.0)
.with_height(200.0)
.with_name("LogPanel"),
)
.open(open)
.with_title(WindowTitle::text("Message Log"))
.with_tab_label("Log")
.with_content(
GridBuilder::new(WidgetBuilder::new().with_child(buttons).with_child({
scroll_viewer = ScrollViewerBuilder::new(
WidgetBuilder::new()
.on_row(1)
.on_column(0)
.with_margin(Thickness::uniform(3.0)),
)
.with_content({
messages = StackPanelBuilder::new(
WidgetBuilder::new().with_margin(Thickness::uniform(1.0)),
)
.build(ctx);
messages
})
.with_horizontal_scroll_allowed(true)
.with_vertical_scroll_allowed(true)
.build(ctx);
scroll_viewer
}))
.add_row(Row::auto())
.add_row(Row::stretch())
.add_column(Column::stretch())
.build(ctx),
)
.build(ctx);
let context_menu = ContextMenu::new(ctx);
Self {
window,
messages,
clear,
receiver: message_receiver,
log_info,
log_warning,
log_error,
context_menu,
message_count: 0,
scroll_viewer,
}
}
pub fn destroy(self, ui: &UserInterface) {
ui.send(self.context_menu.menu.handle(), WidgetMessage::Remove);
ui.send(self.window, WidgetMessage::Remove);
}
pub fn open(&self, ui: &UserInterface) {
ui.send(
self.window,
WindowMessage::Open {
alignment: WindowAlignment::Center,
modal: false,
focus_content: true,
},
);
}
pub fn close(&self, ui: &UserInterface) {
ui.send(self.window, WindowMessage::Close);
}
pub fn handle_ui_message(&mut self, message: &UiMessage, ui: &mut UserInterface) {
if let Some(ButtonMessage::Click) = message.data_from(self.clear) {
ui.send(self.messages, WidgetMessage::ReplaceChildren(vec![]));
} else if let Some(ToggleButtonMessage::Toggled(value)) = message.data_from(self.log_info) {
Log::set_log_info(*value);
} else if let Some(ToggleButtonMessage::Toggled(value)) =
message.data_from(self.log_warning)
{
Log::set_log_warning(*value);
} else if let Some(ToggleButtonMessage::Toggled(value)) = message.data_from(self.log_error)
{
Log::set_log_error(*value);
}
self.context_menu.handle_ui_message(message, ui);
}
pub fn update(&mut self, max_log_entries: usize, ui: &mut UserInterface) -> bool {
let existing_items = ui[self.messages].children();
let mut count = existing_items.len();
if count > max_log_entries {
let delta = count - max_log_entries;
for item in existing_items.iter().take(delta) {
ui.send(*item, WidgetMessage::Remove);
}
count -= delta;
}
let mut item_to_bring_into_view = Handle::NONE;
let mut received_anything = false;
while let Ok(msg) = self.receiver.try_recv() {
self.message_count += 1;
received_anything = true;
let mut text = format!("[{:.2}s] {}", msg.time.as_secs_f32(), msg.content);
if let Some(ch) = text.chars().last() {
if ch == '\n' {
text.pop();
}
}
let ctx = &mut ui.build_ctx();
let item = BorderBuilder::new(
WidgetBuilder::new()
.with_context_menu(self.context_menu.menu.clone())
.with_background(if count.is_multiple_of(2) {
ctx.style.property(Style::BRUSH_LIGHT)
} else {
ctx.style.property(Style::BRUSH_DARK)
})
.with_child(
TextBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(2.0))
.with_foreground(match msg.kind {
MessageKind::Information => {
ctx.style.property(Style::BRUSH_INFORMATION)
}
MessageKind::Warning => {
ctx.style.property(Style::BRUSH_WARNING)
}
MessageKind::Error => ctx.style.property(Style::BRUSH_ERROR),
}),
)
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_text(text)
.build(ctx),
),
)
.build(ctx);
ui.send(item, WidgetMessage::link_with(self.messages));
item_to_bring_into_view = item;
count += 1;
}
if item_to_bring_into_view.is_some() {
ui.send(
self.scroll_viewer,
ScrollViewerMessage::BringIntoView(item_to_bring_into_view.to_base()),
);
}
received_anything
}
}