use crate::icons::icons as embedded_icons;
use crate::styles::responsive::LayoutContext;
use crate::styles::{icons, typography};
use egui::{Pos2, Rect, Response, Sense, Ui, Vec2};
use crate::ext::UiExt;
use super::{
actions::TopToolbarAction,
buttons::{
IconTextButton, PillButtonFilled, PillButtonOutlined, TextIconButton, ToolbarIconButton,
},
components::{
chart_grid_selector::ChartGridSelector,
chart_type_selector::{ChartTypeAction, ChartTypeSelector},
layout_menu::{LayoutAction, LayoutMenu},
sync_button::SyncButton,
timeframe_selector::{Timeframe, TimeframeAction, TimeframeSelector, TimeframeUnit},
},
config::TopToolbarConfig,
state::TopToolbarState,
};
use crate::model::ChartType;
use crate::theming;
use crate::tokens::DESIGN_TOKENS;
pub struct TopToolbar {
pub config: TopToolbarConfig,
pub state: TopToolbarState,
pub timeframe_selector: TimeframeSelector,
pub chart_type_selector: ChartTypeSelector,
pub layout_menu: LayoutMenu,
pub chart_grid_selector: ChartGridSelector,
pub sync_button: SyncButton,
}
impl Default for TopToolbar {
fn default() -> Self {
Self {
config: TopToolbarConfig::default(),
state: TopToolbarState::default(),
timeframe_selector: TimeframeSelector::new(Timeframe::new(1, TimeframeUnit::Day)),
chart_type_selector: ChartTypeSelector::new(ChartType::Candles),
layout_menu: LayoutMenu::new(),
chart_grid_selector: ChartGridSelector::default(),
sync_button: SyncButton::new(),
}
}
}
impl TopToolbar {
pub fn new() -> Self {
Self::default()
}
pub fn with_config(mut self, config: TopToolbarConfig) -> Self {
self.config = config;
self
}
pub fn show_with_state(
&mut self,
ui: &mut Ui,
app_state: &dyn crate::ui::app_state::ChartAppState,
) -> TopToolbarAction {
let _layout_ctx = LayoutContext::from_egui(ui.ctx());
ui.horizontal(|ui| {
ui.set_height(self.config.height);
self.show_contents_desktop(ui, app_state.active_symbol())
})
.inner
}
pub fn show_inline(
&mut self,
ui: &mut Ui,
app_state: &dyn crate::ui::app_state::ChartAppState,
) -> TopToolbarAction {
ui.horizontal(|ui| {
ui.set_height(self.config.height);
self.show_contents_desktop(ui, app_state.active_symbol())
})
.inner
}
fn show_contents_desktop(&mut self, ui: &mut Ui, symbol: &str) -> TopToolbarAction {
let mut action = TopToolbarAction::None;
self.show_menu_button(ui, &mut action);
self.toolbar_separator(ui);
self.show_symbol_controls(ui, &mut action, symbol);
self.show_chart_controls(ui, &mut action);
self.show_action_controls(ui, &mut action);
ui.right_aligned(|ui| {
self.show_right_controls(ui, &mut action);
});
action
}
fn toolbar_separator(&self, ui: &mut Ui) {
ui.toolbar_separator();
}
fn show_symbol_controls(&mut self, ui: &mut Ui, action: &mut TopToolbarAction, symbol: &str) {
if self.symbol_search_pill(ui, symbol).clicked() {
*action = TopToolbarAction::OpenSymbolSearch;
}
ui.space_sm();
if ToolbarIconButton::new(&embedded_icons::PLUS, "Compare or Add Symbol", &self.config)
.show(ui)
.clicked()
{
*action = TopToolbarAction::OpenCompare;
}
self.toolbar_separator(ui);
let tf_action = self.timeframe_selector.show(ui);
if let TimeframeAction::Select(tf) = tf_action {
let display = tf.display();
*action = TopToolbarAction::IntervalSelected(display);
}
self.toolbar_separator(ui);
}
fn show_chart_controls(&mut self, ui: &mut Ui, action: &mut TopToolbarAction) {
let ct_action = self.chart_type_selector.show(ui);
if let ChartTypeAction::Select(ct) = ct_action {
let name = ct.name().to_string();
*action = TopToolbarAction::ChartStyleSelected(name);
}
self.toolbar_separator(ui);
if IconTextButton::new(&embedded_icons::INDICATORS, "Indicators", &self.config)
.show(ui)
.clicked()
{
*action = TopToolbarAction::OpenIndicators;
}
ui.space_xs();
if ToolbarIconButton::new(
&embedded_icons::LAYOUT_GRID,
"Indicator templates",
&self.config,
)
.show(ui)
.clicked()
{
*action = TopToolbarAction::OpenIndicatorTemplates;
}
self.toolbar_separator(ui);
}
fn show_action_controls(&mut self, ui: &mut Ui, action: &mut TopToolbarAction) {
if IconTextButton::new(&embedded_icons::BELL, "Alert", &self.config)
.show(ui)
.clicked()
{
*action = TopToolbarAction::CreateAlert;
}
ui.space_sm();
if IconTextButton::new(&embedded_icons::REPLAY, "Replay", &self.config)
.show(ui)
.clicked()
{
self.state.toggle_replay();
*action = TopToolbarAction::ToggleReplay;
}
self.toolbar_separator(ui);
if ToolbarIconButton::new(&embedded_icons::UNDO, "Undo", &self.config)
.show(ui)
.clicked()
{
*action = TopToolbarAction::Undo;
}
ui.space_xs();
if ToolbarIconButton::new(&embedded_icons::REDO, "Redo", &self.config)
.show(ui)
.clicked()
{
*action = TopToolbarAction::Redo;
}
}
fn show_right_controls(&mut self, ui: &mut Ui, action: &mut TopToolbarAction) {
ui.space_lg();
if PillButtonOutlined::new("Publish", "Publish Idea")
.show(ui)
.clicked()
{
*action = TopToolbarAction::Publish;
}
ui.space_lg();
if PillButtonFilled::new("Trade", "Open Trading Panel")
.show(ui)
.clicked()
{
*action = TopToolbarAction::OpenTradingPanel;
}
ui.space_sm();
if ToolbarIconButton::new(&embedded_icons::CAMERA, "Take Screenshot", &self.config)
.show(ui)
.clicked()
{
*action = TopToolbarAction::TakeScreenshot;
}
ui.space_xs();
if ToolbarIconButton::new(&embedded_icons::FULLSCREEN, "Fullscreen", &self.config)
.show(ui)
.clicked()
{
*action = TopToolbarAction::ToggleFullscreen;
}
ui.space_xs();
if ToolbarIconButton::new(&embedded_icons::SETTINGS_GEAR, "Settings", &self.config)
.show(ui)
.clicked()
{
*action = TopToolbarAction::OpenSettings;
}
ui.space_xs();
if ToolbarIconButton::new(&embedded_icons::QUICK_SEARCH, "Quick Search", &self.config)
.show(ui)
.clicked()
{
*action = TopToolbarAction::OpenSymbolSearch;
}
ui.toolbar_separator();
if TextIconButton::new(
"Save",
&embedded_icons::CHEVRON_DOWN,
"Save Chart",
&self.config,
)
.show(ui)
.clicked()
{
*action = TopToolbarAction::Save;
}
ui.space_sm();
if ToolbarIconButton::new(
&embedded_icons::LAYOUT_SETUP,
"Manage Layouts",
&self.config,
)
.show(ui)
.clicked()
{
*action = TopToolbarAction::Layout(LayoutAction::OpenMenu);
}
ui.space_sm();
}
fn show_menu_button(&self, ui: &mut Ui, action: &mut TopToolbarAction) {
let btn_size = DESIGN_TOKENS.sizing.button_dialog;
let icon_size = DESIGN_TOKENS.sizing.icon_lg;
let (rect, response) = ui.allocate_exact_size(Vec2::splat(btn_size), Sense::click());
if ui.is_rect_visible(rect) {
if response.hovered() {
ui.painter().rect_filled(
rect,
DESIGN_TOKENS.rounding.button,
theming::hover_color(ui),
);
}
let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
let icon_color = if response.hovered() {
theming::active_icon_color(ui)
} else {
theming::icon_color(ui)
};
embedded_icons::MENU
.as_image_tinted(Vec2::splat(icon_size), icon_color)
.paint_at(ui, icon_rect);
}
if response.clicked() {
*action = TopToolbarAction::OpenMenu;
}
response.on_hover_text("Menu");
}
fn symbol_search_pill(&self, ui: &mut Ui, symbol: &str) -> Response {
let icon_size = icons::SM_MD;
let small_icon_size = icons::XS;
let padding = DESIGN_TOKENS.spacing.lg;
let display_symbol = if symbol.is_empty() { "AAPL" } else { symbol };
let text_width = display_symbol.len() as f32 * 7.5; let separator_width = DESIGN_TOKENS.stroke.hairline;
let separator_padding = DESIGN_TOKENS.spacing.sm;
let total_width = icon_size
+ padding
+ text_width
+ separator_padding
+ separator_width
+ separator_padding
+ small_icon_size
+ DESIGN_TOKENS.spacing.xs
+ small_icon_size
+ padding;
let height = DESIGN_TOKENS.sizing.button_md;
let (rect, response) =
ui.allocate_exact_size(Vec2::new(total_width, height), Sense::click());
if ui.is_rect_visible(rect) {
let bg_color = if response.is_pointer_button_down_on() {
theming::btn_bg_pressed(ui)
} else if response.hovered() {
theming::btn_bg_hover(ui)
} else {
theming::pill_bg(ui)
};
ui.painter()
.rect_filled(rect, DESIGN_TOKENS.rounding.pill, bg_color);
let mut x = rect.min.x + padding;
let icon_pos = Pos2::new(x, rect.center().y - icon_size / 2.0);
let icon_rect = Rect::from_min_size(icon_pos, Vec2::splat(icon_size));
let icon_color = if response.hovered() {
theming::icon_hover_color(ui)
} else {
theming::icon_normal(ui)
};
embedded_icons::QUICK_SEARCH
.as_image_tinted(Vec2::splat(icon_size), icon_color)
.paint_at(ui, icon_rect);
x += icon_size + padding;
ui.painter().text(
Pos2::new(x, rect.center().y),
egui::Align2::LEFT_CENTER,
display_symbol,
egui::FontId::proportional(typography::MD),
theming::text_color(ui),
);
x += text_width + separator_padding;
let sep_height = height * 0.5;
let sep_y = rect.center().y - sep_height / 2.0;
ui.painter().rect_filled(
Rect::from_min_size(Pos2::new(x, sep_y), Vec2::new(separator_width, sep_height)),
DESIGN_TOKENS.rounding.none,
theming::separator_color(ui),
);
x += separator_width + separator_padding;
let diamond_pos = Pos2::new(x, rect.center().y - small_icon_size / 2.0);
let diamond_rect = Rect::from_min_size(diamond_pos, Vec2::splat(small_icon_size));
embedded_icons::CANDLESTICK
.as_image_tinted(Vec2::splat(small_icon_size), icon_color)
.paint_at(ui, diamond_rect);
x += small_icon_size + DESIGN_TOKENS.spacing.xs;
let chevron_pos = Pos2::new(x, rect.center().y - small_icon_size / 2.0);
let chevron_rect = Rect::from_min_size(chevron_pos, Vec2::splat(small_icon_size));
embedded_icons::CHEVRON_DOWN
.as_image_tinted(Vec2::splat(small_icon_size), icon_color)
.paint_at(ui, chevron_rect);
}
response
}
}
impl TopToolbar {
pub fn sync_from_app_state(&mut self, app_state: &dyn crate::ui::app_state::ChartAppState) {
self.state.sync_from_app_state(app_state);
}
}