Эх сурвалжийг харах

Encapsulated `Message` routing (#16)

* initial commit

* chore: remove and organize `Message` events

* optimized `Message` routing

* chore for handling pane streams map and more on `Message`routing

* organized `Task` and `Message` routing

* chores: organizing and moving around state/layout parse methods

* hide `Dashboard` internals for layout modal, use `State`'s modal instead
Berke 1 жил өмнө
parent
commit
e3093b9247

+ 2 - 1
src/charts/timeandsales.rs

@@ -3,7 +3,8 @@ use iced::{
     alignment, Element, Length
 };
 use iced::widget::{Column, Row, Container, Text, container, Space};
-use crate::{Message, style, data_providers::Trade};
+use crate::screen::dashboard::pane::Message;
+use crate::{style, data_providers::Trade};
 
 struct ConvertedTrade {
     time: NaiveDateTime,

+ 1 - 1
src/data_providers/bybit/market_data.rs

@@ -587,7 +587,7 @@ pub async fn fetch_klines(ticker: Ticker, timeframe: Timeframe) -> Result<Vec<Kl
         Timeframe::M30 => "30",
     };
 
-    let url: String = format!("https://api.bybit.com/v5/market/kline?category=linear&symbol={symbol_str}&interval={timeframe_str}&limit=250");
+    let url: String = format!("https://api.bybit.com/v5/market/kline?category=linear&symbol={symbol_str}&interval={timeframe_str}&limit=720");
 
     let response: reqwest::Response = reqwest::get(&url).await
         .context("Failed to send request")?;

+ 147 - 1114
src/main.rs

@@ -6,34 +6,30 @@ mod style;
 mod screen;
 mod logger;
 
-use hyper::client::conn;
 use style::{ICON_FONT, ICON_BYTES, Icon};
 
-use screen::{Notification, Error};
+use screen::{dashboard, Error, Notification};
 use screen::dashboard::{
     Dashboard,
-    pane::{self, SerializablePane}, Uuid, LayoutId,
+    pane::{self, SerializablePane}, Uuid,
     PaneContent, PaneSettings, PaneState, 
-    SavedState, SerializableDashboard, SerializableState, 
-    read_layout_from_file, write_json_to_file, 
+    SerializableDashboard, 
 };
-use data_providers::{binance, bybit, Exchange, MarketEvents, TickMultiplier, Ticker, Timeframe, StreamType};
+use data_providers::{binance, bybit, Exchange, MarketEvents, Ticker, Timeframe, StreamType};
 
 use charts::footprint::FootprintChart;
 use charts::heatmap::HeatmapChart;
 use charts::candlestick::CandlestickChart;
 use charts::timeandsales::TimeAndSales;
 
-use futures::TryFutureExt;
-
-use std::{collections::{HashMap, HashSet, VecDeque}, vec};
+use std::{collections::{HashMap, VecDeque}, vec};
 
 use iced::{
     alignment, widget::{
         button, center, checkbox, mouse_area, opaque, pick_list, stack, tooltip, Column, Container, Row, Slider, Space, Text
-    }, window::{self, Position}, Alignment, Color, Element, Length, Point, Renderer, Size, Subscription, Task, Theme
+    }, window::{self, Position}, Alignment, Color, Element, Length, Point, Size, Subscription, Task, Theme
 };
-use iced::widget::pane_grid::{self, PaneGrid, Configuration};
+use iced::widget::pane_grid::{self, Configuration};
 use iced::widget::{container, row, scrollable, text};
 
 fn main() -> iced::Result {
@@ -180,65 +176,31 @@ fn main() -> iced::Result {
 
 #[derive(Debug, Clone)]
 pub enum Message {
-    FetchDistributeKlines(StreamType, Result<Vec<data_providers::Kline>, std::string::String>),
-    FetchDistributeTicks(StreamType, Result<f32, std::string::String>),
     Debug(String),
     Notification(Notification),
     ErrorOccurred(Error),
     ClearNotification,
 
-    ChartUserUpdate(charts::Message, Uuid),
     ShowLayoutModal,
     HideLayoutModal,
 
-    // Market&User data stream
-    UserKeySucceed(String),
-    UserKeyError,
-    TickerSelected(Ticker, Uuid),
-    ExchangeSelected(Exchange, Uuid),
     MarketWsEvent(MarketEvents),
-    FetchEvent(Result<Vec<data_providers::Kline>, std::string::String>, StreamType, Uuid),
-    
-    // Pane grid
-    Split(pane_grid::Axis, pane_grid::Pane),
-    Clicked(pane_grid::Pane),
-    Dragged(pane_grid::DragEvent),
-    Resized(pane_grid::ResizeEvent),
-    Maximize(pane_grid::Pane),
-    Restore,
-    Close(pane_grid::Pane),
-    ToggleLayoutLock,
-    PaneContentSelected(String, Uuid, Vec<StreamType>),
-    ReplacePane(pane_grid::Pane),
-
-    // Modal
-    OpenModal(pane_grid::Pane),
-    CloseModal(Uuid),
-
-    // Slider
-    SliderChanged(Uuid, f32),
-    SyncWithHeatmap(bool),
-
-    // Chart settings
-    TicksizeSelected(TickMultiplier, Uuid),
-    TimeframeSelected(Timeframe, Uuid),
-    SetMinTickSize(f32, Uuid),   
     
     Event(Event),
-
     SaveAndExit(window::Id, Option<Size>, Option<Point>),
 
+    ToggleLayoutLock,
     ResetCurrentLayout,
     LayoutSelected(LayoutId),
+    Dashboard(dashboard::Message),
 }
 
 struct State {
     layouts: HashMap<LayoutId, Dashboard>,
     last_active_layout: LayoutId,
+    show_layout_modal: bool,
     exchange_latency: Option<(u32, u32)>,
-    listen_key: Option<String>,
     feed_latency_cache: VecDeque<data_providers::FeedLatency>,
-    pane_streams: HashMap<Exchange, HashMap<Ticker, HashSet<StreamType>>>,
     notification: Option<Notification>,
 }
 
@@ -246,30 +208,21 @@ impl State {
     fn new(saved_state: SavedState) -> (Self, Task<Message>) {
         let mut tasks = vec![];
 
-        let mut pane_streams = HashMap::new();
-
         let last_active_layout = saved_state.last_active_layout;
-        let dashboard = saved_state.layouts.get(&last_active_layout);
-
-        if let Some(dashboard) = dashboard {
-            let sleep_and_fetch = Task::perform(
-                async { tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; },
-                move |_| Message::LayoutSelected(last_active_layout)
-            );
-
-            tasks.push(sleep_and_fetch);
 
-            pane_streams = dashboard.get_all_diff_streams();
-        }
+        let wait_and_fetch = Task::perform(
+            async { tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; },
+            move |_| Message::LayoutSelected(last_active_layout)
+        );
+        tasks.push(wait_and_fetch);
 
         (
             Self { 
                 layouts: saved_state.layouts,
                 last_active_layout,
-                listen_key: None,
+                show_layout_modal: false,
                 exchange_latency: None,
                 feed_latency_cache: VecDeque::new(),
-                pane_streams,
                 notification: None,
             },
             Task::batch(tasks)
@@ -278,168 +231,6 @@ impl State {
 
     fn update(&mut self, message: Message) -> Task<Message> {
         match message {
-            Message::ChartUserUpdate(message, pane_id) => {
-                let dashboard = self.get_mut_dashboard();
-            
-                match dashboard.update_chart_state(pane_id, message) {
-                    Ok(_) => Task::none(),
-                    Err(err) => {      
-                        Task::perform(
-                            async { err },
-                            move |err: Error| Message::ErrorOccurred(err)
-                        )
-                    }
-                }
-            },
-            Message::SetMinTickSize(min_tick_size, pane_id) => {
-                let dashboard = self.get_mut_dashboard();
-
-                match dashboard.get_pane_settings_mut(pane_id) {
-                    Ok(pane_settings) => {
-                        pane_settings.min_tick_size = Some(min_tick_size);
-                        
-                        Task::none()
-                    },
-                    Err(err) => {
-                        Task::perform(
-                            async { err },
-                            move |err: Error| Message::ErrorOccurred(err)
-                        )
-                    }
-                }
-            },
-            Message::TickerSelected(ticker, pane_id) => {
-                let dashboard = self.get_mut_dashboard();
-
-                match dashboard.get_pane_settings_mut(pane_id) {
-                    Ok(pane_settings) => {
-                        pane_settings.selected_ticker = Some(ticker);
-                        
-                        Task::none()
-                    },
-                    Err(err) => {
-                        Task::perform(
-                            async { err },
-                            move |err: Error| Message::ErrorOccurred(err)
-                        )
-                    }
-                }
-            },
-            Message::TimeframeSelected(timeframe, pane_id) => {    
-                let mut tasks = vec![];
-
-                let dashboard = self.get_mut_dashboard();
-            
-                match dashboard.set_pane_timeframe(pane_id, timeframe) {
-                    Ok(stream_type) => {
-                        if let StreamType::Kline { exchange, ticker, timeframe } = stream_type {
-                            let stream = *stream_type;
-            
-                            match exchange {
-                                Exchange::BinanceFutures => {
-                                    tasks.push(
-                                        Task::perform(
-                                            binance::market_data::fetch_klines(*ticker, *timeframe)
-                                                .map_err(|err| format!("{err}")),
-                                            move |klines| Message::FetchEvent(klines, stream, pane_id)
-                                        )
-                                    );
-                                },
-                                Exchange::BybitLinear => {                                    
-                                    tasks.push(
-                                        Task::perform(
-                                            bybit::market_data::fetch_klines(*ticker, *timeframe)
-                                                .map_err(|err| format!("{err}")),
-                                            move |klines| Message::FetchEvent(klines, stream, pane_id)
-                                        )
-                                    );
-                                },
-                            }
-
-                            tasks.push(
-                                Task::perform(
-                                    async {},
-                                    move |_| Message::Notification(Notification::Info("Fetching for klines...".to_string()))
-                                )
-                            );
-
-                            self.pane_streams = dashboard.get_all_diff_streams();
-                        }
-                    },
-                    Err(err) => {
-                        tasks.push(Task::perform(
-                            async { err },
-                            move |err: Error| Message::ErrorOccurred(err)
-                        ));
-                    }
-                }
-
-                Task::batch(tasks)
-            },
-            Message::ExchangeSelected(exchange, pane_id) => {
-                let dashboard = self.get_mut_dashboard();
-
-                match dashboard.get_pane_settings_mut(pane_id) {
-                    Ok(pane_settings) => {
-                        pane_settings.selected_exchange = Some(exchange);
-
-                        Task::none()
-                    },
-                    Err(err) => {
-                        Task::perform(
-                            async { err },
-                            move |err: Error| Message::ErrorOccurred(err)
-                        )
-                    }
-                }
-            },
-            Message::TicksizeSelected(tick_multiply, pane_id) => {
-                let dashboard = self.get_mut_dashboard();
-                
-                match dashboard.set_pane_ticksize(pane_id, tick_multiply) {
-                    Ok(_) => {
-                        Task::none()
-                    },
-                    Err(err) => {            
-                        Task::perform(
-                            async { err },
-                            move |err: Error| Message::ErrorOccurred(err)
-                        )
-                    }
-                }
-            },
-            Message::FetchEvent(klines, pane_stream, pane_id) => {
-                if let Some(notification) = &self.notification {
-                    match notification {
-                        Notification::Info(_) => {
-                            self.notification = None;
-                        },
-                        _ => {}
-                    }
-                }
-               
-                let dashboard = self.get_mut_dashboard();
-
-                match klines {
-                    Ok(klines) => {
-                        if let StreamType::Kline { .. } = pane_stream {
-                            dashboard.insert_klines_vec(&pane_stream, &klines, pane_id);
-
-                            Task::none()
-                        } else {
-                            log::error!("Invalid stream type for klines: {pane_stream:?}");
-
-                            Task::none()
-                        }
-                    },
-                    Err(err) => {
-                        Task::perform(
-                            async { err },
-                            move |err: String| Message::ErrorOccurred(Error::FetchError(err))
-                        )
-                    }
-                }
-            },
             Message::MarketWsEvent(event) => {
                 let dashboard = self.get_mut_dashboard();
 
@@ -459,13 +250,6 @@ impl State {
                             
                             if let Err(err) = dashboard.update_depth_and_trades(stream_type, depth_update_t, depth, trades_buffer) {
                                 log::error!("{err}, {stream_type:?}");
-
-                                self.pane_streams
-                                    .entry(Exchange::BinanceFutures)
-                                    .or_default()
-                                    .entry(ticker)
-                                    .or_default()
-                                    .remove(&stream_type);
                             }
                         }
                         binance::market_data::Event::KlineReceived(ticker, kline, timeframe) => {
@@ -477,13 +261,6 @@ impl State {
 
                             if let Err(err) = dashboard.update_latest_klines(&stream_type, &kline) {
                                 log::error!("{err}, {stream_type:?}");
-
-                                self.pane_streams
-                                    .entry(Exchange::BinanceFutures)
-                                    .or_default()
-                                    .entry(ticker)
-                                    .or_default()
-                                    .remove(&stream_type);
                             }
                         }
                     },
@@ -502,13 +279,6 @@ impl State {
                             
                             if let Err(err) = dashboard.update_depth_and_trades(stream_type, depth_update_t, depth, trades_buffer) {
                                 log::error!("{err}, {stream_type:?}");
-
-                                self.pane_streams
-                                    .entry(Exchange::BybitLinear)
-                                    .or_default()
-                                    .entry(ticker)
-                                    .or_default()
-                                    .remove(&stream_type);
                             }
                         }
                         bybit::market_data::Event::KlineReceived(ticker, kline, timeframe) => {
@@ -520,13 +290,6 @@ impl State {
 
                             if let Err(err) = dashboard.update_latest_klines(&stream_type, &kline) {
                                 log::error!("{err}, {stream_type:?}");
-
-                                self.pane_streams
-                                    .entry(Exchange::BybitLinear)
-                                    .or_default()
-                                    .entry(ticker)
-                                    .or_default()
-                                    .remove(&stream_type);
                             }
                         }
                     },
@@ -534,82 +297,10 @@ impl State {
 
                 Task::none()
             },
-            Message::UserKeySucceed(listen_key) => {
-                self.listen_key = Some(listen_key);
-                Task::none()
-            },
-            Message::UserKeyError => {
-                log::error!("Check API keys");
-                Task::none()
-            },
-            Message::Split(axis, pane) => {
-                let dashboard = self.get_mut_dashboard();
-
-                let focus_pane = if let Some((new_pane, _)) = 
-                    dashboard.panes.split(axis, pane, PaneState::new(Uuid::new_v4(), vec![], PaneSettings::default())) {
-                            Some(new_pane)
-                        } else {
-                            None
-                        };
-
-                if Some(focus_pane).is_some() {
-                    dashboard.focus = focus_pane;
-                }
-
-                Task::none()
-            },
-            Message::Clicked(pane) => {
-                let dashboard = self.get_mut_dashboard();
-
-                dashboard.focus = Some(pane);
-                Task::none()
-            },
-            Message::Resized(pane_grid::ResizeEvent { split, ratio }) => {
-                let dashboard = self.get_mut_dashboard();
-
-                dashboard.panes.resize(split, ratio);
-                Task::none()
-            },
-            Message::Dragged(pane_grid::DragEvent::Dropped {
-                pane,
-                target,
-            }) => {
-                let dashboard = self.get_mut_dashboard();
-
-                dashboard.panes.drop(pane, target);
-
-                dashboard.focus = None;
-
-                Task::none()
-            },
-            Message::Dragged(_) => {
-                Task::none()
-            },
-            Message::Maximize(pane) => {
-                let dashboard = self.get_mut_dashboard();
-
-                dashboard.panes.maximize(pane);
-                Task::none()
-            },
-            Message::Restore => {
-                let dashboard = self.get_mut_dashboard();
-
-                dashboard.panes.restore();
-                Task::none()
-            },
-            Message::Close(pane) => {    
-                let dashboard = self.get_mut_dashboard();
-
-                if let Some((_, sibling)) = dashboard.panes.close(pane) {
-                    dashboard.focus = Some(sibling);
-                }
-                
-                Task::none()
-            },
             Message::ToggleLayoutLock => {
                 let dashboard = self.get_mut_dashboard();
 
-                dashboard.pane_lock = !dashboard.pane_lock;
+                dashboard.layout_lock = !dashboard.layout_lock;
 
                 dashboard.focus = None;
 
@@ -676,59 +367,12 @@ impl State {
             
                 window::close(window)
             },
-            Message::OpenModal(pane) => {
-                let dashboard = self.get_mut_dashboard();
-
-                if let Some(pane) = dashboard.panes.get_mut(pane) {
-                    pane.show_modal = true;
-                };
-                Task::none()
-            },
-            Message::CloseModal(pane_id) => {
-                let dashboard = self.get_mut_dashboard();
-                
-                for (_, pane_state) in dashboard.panes.iter_mut() {
-                    if pane_state.id == pane_id {
-                        pane_state.show_modal = false;
-                    }
-                }
-                Task::none()
-            },
-            Message::SliderChanged(pane_id, value) => {
-                let dashboard = self.get_mut_dashboard();
-
-                match dashboard.set_pane_size_filter(pane_id, value) {
-                    Ok(_) => {
-                        log::info!("Size filter set to {value}");
-
-                        Task::none()
-                    }
-                    Err(err) => {
-                        Task::perform(
-                            async { err },
-                            move |err: Error| Message::ErrorOccurred(err)
-                        )
-                    }
-                }
-            },
-            Message::SyncWithHeatmap(sync) => {   
-                Task::perform(
-                    async {},
-                    move |_| Message::Notification(
-                        Notification::Warn("gonna have to reimplement that".to_string())
-                    )
-                )
-            },
             Message::ShowLayoutModal => {
-                let dashboard = self.get_mut_dashboard();
-
-                dashboard.show_layout_modal = true;
+                self.show_layout_modal = true;
                 iced::widget::focus_next()
             },
             Message::HideLayoutModal => {
-                let dashboard = self.get_mut_dashboard();
-
-                dashboard.show_layout_modal = false;
+                self.show_layout_modal = false;
                 Task::none()
             },
             Message::Notification(notification) => {
@@ -798,100 +442,6 @@ impl State {
 
                 Task::none()
             },
-            Message::PaneContentSelected(content, pane_id, pane_stream) => {
-                let dashboard = self.get_mut_dashboard();
-
-                let mut tasks = vec![];
-                    
-                let pane_content = match content.as_str() {
-                    "Heatmap chart" => PaneContent::Heatmap(
-                        HeatmapChart::new(1.0)
-                    ),
-                    "Footprint chart" => {
-                        PaneContent::Footprint(
-                            FootprintChart::new(1, 1.0, vec![], vec![])
-                        )
-                    },
-                    "Candlestick chart" => {
-                        PaneContent::Candlestick(
-                            CandlestickChart::new(vec![], 1)
-                        )
-                    },
-                    "Time&Sales" => PaneContent::TimeAndSales(
-                        TimeAndSales::new()
-                    ),
-                    _ => return Task::none(),
-                };
-
-                // set pane's stream and content identifiers
-                if let Err(err) = dashboard.set_pane_content(pane_id, pane_content) {
-                    log::error!("Failed to set pane content: {}", err);
-                } else {
-                    log::info!("Pane content set: {content}");
-                }
-                
-                if let Err(err) = dashboard.set_pane_stream(pane_id, pane_stream.to_vec()) {
-                    log::error!("Failed to set pane stream: {err}");
-                } else {
-                    log::info!("Pane stream set: {pane_stream:?}");
-                }
-            
-                // prepare unique streams for websocket
-                for stream in pane_stream.iter() {
-                    match stream {
-                        StreamType::Kline { exchange, ticker, .. } | StreamType::DepthAndTrades { exchange, ticker } => {
-                            self.pane_streams
-                                .entry(*exchange)
-                                .or_default()
-                                .entry(*ticker)
-                                .or_default()
-                                .insert(*stream);
-                        }
-                        _ => {}
-                    }
-                }
-            
-                log::info!("{:?}", &self.pane_streams);
-
-                // get fetch tasks for pane's content
-                if ["Footprint chart", "Candlestick chart", "Heatmap chart"].contains(&content.as_str()) {
-                    for stream in pane_stream.iter() {
-                        match stream {
-                            StreamType::Kline { exchange, ticker, .. } => {
-                                if ["Candlestick chart", "Footprint chart"].contains(&content.as_str()) {
-                                    tasks.push(create_fetch_klines_task(*stream, pane_id));
-                                    
-                                    if content == "Footprint chart" {
-                                        tasks.push(create_fetch_ticksize_task(exchange, ticker, pane_id));
-                                    }
-                                }
-                            },
-                            StreamType::DepthAndTrades { exchange, ticker } => {
-                                tasks.push(create_fetch_ticksize_task(exchange, ticker, pane_id));
-                            },
-                            _ => {}
-                        }
-                    }
-
-                    tasks.push(
-                        Task::perform(
-                            async {},
-                            move |_| Message::Notification(
-                                Notification::Info(format!("Fetching data for the {}...", content.to_lowercase()))
-                            )
-                        )
-                    );
-                }
-                
-                Task::batch(tasks)
-            },
-            Message::ReplacePane(pane) => {
-                let dashboard = self.get_mut_dashboard();
-
-                dashboard.replace_new_pane(pane);
-
-                Task::none()
-            },
             Message::ResetCurrentLayout => {
                 let new_dashboard = Dashboard::empty();
 
@@ -906,58 +456,25 @@ impl State {
             },
             Message::LayoutSelected(layout_id) => {
                 self.last_active_layout = layout_id;
-            
-                let mut tasks = vec![];
 
-                self.pane_streams = self.get_dashboard().get_all_diff_streams();
-
-                tasks.push(
-                    Task::perform(
-                        async {},
-                        move |_| Message::Notification(Notification::Info("Fetching data...".to_string()))
-                    )
-                );
-
-                tasks.extend(
-                    klines_fetch_all_task(&self.pane_streams)
-                );
-                tasks.extend(
-                    ticksize_fetch_all_task(&self.pane_streams)
-                );
-
-                Task::batch(tasks)
-            },
-            Message::FetchDistributeKlines(stream_type, klines) => {
                 let dashboard = self.get_mut_dashboard();
 
-                match klines {
-                    Ok(klines) => {
-                        if let Err(err) = dashboard.find_and_insert_klines(&stream_type, &klines) {
-                            log::error!("{err}");
-                        }
-                    },
-                    Err(err) => {
-                        log::error!("{err}");
-                    }
-                }
-
-                Task::none()
-            },  
-            Message::FetchDistributeTicks(stream_type, min_tick_size) => {
+                let layout_fetch_command = dashboard.layout_changed();
+            
+                Task::batch(vec![
+                    layout_fetch_command.map(Message::Dashboard),
+                ])
+            },
+            Message::Dashboard(message) => {
                 let dashboard = self.get_mut_dashboard();
+                
+                let command = dashboard.update(
+                    message,
+                );
 
-                match min_tick_size {
-                    Ok(ticksize) => {
-                        if let Err(err) = dashboard.find_and_insert_ticksizes(&stream_type, ticksize) {
-                            log::error!("{err}");
-                        }
-                    },
-                    Err(err) => {
-                        log::error!("{err}");
-                    }
-                }
-
-                Task::none()
+                Task::batch(vec![
+                    command.map(Message::Dashboard),
+                ])
             },
         }
     }
@@ -965,105 +482,9 @@ impl State {
     fn view(&self) -> Element<'_, Message> {
         let dashboard = self.get_dashboard();
 
-        let focus = dashboard.focus;
-
-        let pane_grid = PaneGrid::new(&dashboard.panes, |id, pane, is_maximized| {
-            let is_focused;
-            
-            if dashboard.pane_lock {
-                is_focused = false;
-            } else {
-                is_focused = focus == Some(id);
-            }
-        
-            let chart_type = &dashboard.panes.get(id).unwrap().content;
-
-            let stream_info = pane.stream.iter().find_map(|stream: &StreamType| {
-                match stream {
-                    StreamType::Kline { exchange, ticker, timeframe } => {
-                        Some(
-                            Some((exchange, format!("{} {}", ticker, timeframe)))
-                        )
-                    }
-                    _ => None,
-                }
-            }).or_else(|| {
-                pane.stream.iter().find_map(|stream: &StreamType| {
-                    match stream {
-                        StreamType::DepthAndTrades { exchange, ticker } => {
-                            Some(
-                                Some((exchange, ticker.to_string()))
-                            )
-                        }
-                        _ => None,
-                    }
-                })
-            }).unwrap_or(None);
-            
-            let mut stream_info_element: Row<Message> = Row::new();
-
-            if let Some((exchange, info)) = stream_info {
-                stream_info_element = Row::new()
-                    .spacing(3)
-                    .push(
-                        match exchange {
-                            Exchange::BinanceFutures => text(char::from(Icon::BinanceLogo).to_string()).font(ICON_FONT),
-                            Exchange::BybitLinear => text(char::from(Icon::BybitLogo).to_string()).font(ICON_FONT),
-                        }
-                    )
-                    .push(Text::new(info));
-            }
-    
-            let mut content: pane_grid::Content<'_, Message, _, Renderer> = 
-                pane_grid::Content::new({
-                    match chart_type {
-                        PaneContent::Heatmap(chart) => view_chart(pane, chart),
-                        
-                        PaneContent::Footprint(chart) => view_chart(pane, chart),
-                        
-                        PaneContent::Candlestick(chart) => view_chart(pane, chart),
-
-                        PaneContent::TimeAndSales(chart) => view_chart(pane, chart),
-
-                        PaneContent::Starter => view_starter(pane)
-                    }
-                })
-                .style(
-                    if is_focused {
-                        style::pane_focused
-                    } else {
-                        style::pane_active
-                    }
-                );
-    
-            let title_bar = pane_grid::TitleBar::new(stream_info_element)
-                .controls(view_controls(
-                    id,
-                    pane.id,
-                    chart_type,
-                    dashboard.panes.len(),
-                    is_maximized,
-                    &pane.settings,
-                ))
-                .padding(4)
-                .style(
-                    if is_focused {
-                        style::title_bar_focused
-                    } else {
-                        style::title_bar_active
-                    }
-                );
-            content = content.title_bar(title_bar);
-            
-            content
-        })
-        .width(Length::Fill)
-        .height(Length::Fill)
-        .spacing(6);
-
         let layout_lock_button = button(
             container(
-                if dashboard.pane_lock { 
+                if dashboard.layout_lock { 
                     text(char::from(Icon::Locked).to_string()).font(ICON_FONT) 
                 } else { 
                     text(char::from(Icon::Unlocked).to_string()).font(ICON_FONT) 
@@ -1073,7 +494,7 @@ impl State {
             )
             .on_press(Message::ToggleLayoutLock);
 
-        let add_pane_button = button(
+        let layout_modal_button = button(
             container(
                 text(char::from(Icon::Layout).to_string()).font(ICON_FONT))
                 .width(25)
@@ -1085,10 +506,16 @@ impl State {
             .spacing(10)
             .align_y(Alignment::Center)
             .push(
-                tooltip(add_pane_button, "Manage Layout", tooltip::Position::Bottom).style(style::tooltip)
+                tooltip(
+                    layout_modal_button, 
+                    "Manage Layouts", tooltip::Position::Bottom
+                ).style(style::tooltip)
             )
             .push(
-                tooltip(layout_lock_button, "Layout Lock", tooltip::Position::Bottom).style(style::tooltip)
+                tooltip(
+                    layout_lock_button, 
+                    "Layout Lock", tooltip::Position::Bottom
+                ).style(style::tooltip)
             );
 
         let mut ws_controls = Row::new()
@@ -1149,44 +576,39 @@ impl State {
                     .push(layout_controls)
             )
             .push(
-                if dashboard.pane_lock {
-                    pane_grid
-                } else {
-                    pane_grid
-                        .on_click(Message::Clicked)
-                        .on_drag(Message::Dragged)
-                        .on_resize(10, Message::Resized)
-                }
+                dashboard.view().map(Message::Dashboard)
             );
 
-        if dashboard.show_layout_modal {
-            let mut add_pane_button = button("Split selected pane").width(iced::Pixels(200.0));
+        if self.show_layout_modal {
+            let layout_picklist = pick_list(
+                &LayoutId::ALL[..],
+                Some(self.last_active_layout),
+                move |layout: LayoutId| Message::LayoutSelected(layout)
+            );
 
+            let mut add_pane_button = button("Split selected pane").width(iced::Pixels(200.0));
             let mut replace_pane_button = button("Replace selected pane").width(iced::Pixels(200.0));
 
             if dashboard.focus.is_some() {
                 replace_pane_button = replace_pane_button.on_press(
-                    Message::ReplacePane(
+                    Message::Dashboard(dashboard::Message::Pane(
+                        pane::Message::ReplacePane(
                         dashboard.focus
                             .unwrap_or_else(|| { *dashboard.panes.iter().next().unwrap().0 })
-                    )
+                        )
+                    ))
                 );
 
                 add_pane_button = add_pane_button.on_press(
-                    Message::Split(
-                        pane_grid::Axis::Horizontal, 
-                        dashboard.focus
-                            .unwrap_or_else(|| { *dashboard.panes.iter().next().unwrap().0 })
-                    )
+                    Message::Dashboard(dashboard::Message::Pane(
+                        pane::Message::SplitPane(
+                            pane_grid::Axis::Horizontal, 
+                            dashboard.focus.unwrap_or_else(|| { *dashboard.panes.iter().next().unwrap().0 })
+                        )
+                    ))
                 );
             }
 
-            let layout_picklist = pick_list(
-                &LayoutId::ALL[..],
-                Some(self.last_active_layout),
-                move |layout: LayoutId| Message::LayoutSelected(layout)
-            );
-
             let layout_modal = container(
                 Column::new()
                     .spacing(16)
@@ -1253,7 +675,7 @@ impl State {
     fn subscription(&self) -> Subscription<Message> {
         let mut all_subscriptions = Vec::new();
     
-        for (exchange, stream) in &self.pane_streams {
+        for (exchange, stream) in &self.get_dashboard().pane_streams {
             let mut depth_streams: Vec<Subscription<Message>> = Vec::new();
             let mut kline_streams: Vec<(Ticker, Timeframe)> = Vec::new();
     
@@ -1386,503 +808,114 @@ where
     .into()
 }
 
-trait ChartView {
-    fn view(&self, id: &PaneState) -> Element<Message>;
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Event {
+    CloseRequested(window::Id),
+    Copy,
+    Escape,
+    Home,
+    End,
 }
 
-impl ChartView for HeatmapChart {
-    fn view(&self, pane: &PaneState) -> Element<Message> {
-        
-
-        let pane_id = pane.id;
-
-        let underlay = self.view().map(move |message| Message::ChartUserUpdate(message, pane_id));
-
-        if pane.show_modal {
-            let size_filter = &self.get_size_filter();
-
-            let signup: Container<Message, Theme, _> = container(
-                Column::new()
-                    .spacing(10)
-                    .align_x(Alignment::Center)
-                    .push(
-                        Text::new("Heatmap > Settings")
-                            .size(16)
-                    )
-                    .push(
-                        Column::new()
-                            .align_x(Alignment::Center)
-                            .push(Text::new("Size Filtering"))
-                            .push(
-                                Slider::new(0.0..=50000.0, *size_filter, move |value| Message::SliderChanged(pane_id, value))
-                                    .step(500.0)
-                            )
-                            .push(
-                                Text::new(format!("${size_filter}")).size(16)
-                            )
-                    )
-                    .push( 
-                        Row::new()
-                            .spacing(10)
-                            .push(
-                                button("Close")
-                                .on_press(Message::CloseModal(pane_id))
-                            )
-                    )
-            )
-            .width(Length::Shrink)
-            .padding(20)
-            .max_width(500)
-            .style(style::chart_modal);
-
-            return modal(underlay, signup, Message::CloseModal(pane_id));
-        } else {
-            underlay
-        }
-    }
+pub fn events() -> Subscription<Event> {
+    iced::event::listen_with(filtered_events)
 }
-impl ChartView for FootprintChart {
-    fn view(&self, pane: &PaneState) -> Element<Message> {
-        let pane_id = pane.id;
 
-        self.view().map(move |message| Message::ChartUserUpdate(message, pane_id))
+fn filtered_events(
+    event: iced::Event,
+    _status: iced::event::Status,
+    window: window::Id,
+) -> Option<Event> {
+    match &event {
+        iced::Event::Window(window::Event::CloseRequested) => Some(Event::CloseRequested(window)),
+        _ => None,
     }
 }
-impl ChartView for TimeAndSales {
-    fn view(&self, pane: &PaneState) -> Element<Message> {
-        
-
-        let pane_id = pane.id;
-
-        let underlay = self.view();
-
-        if pane.show_modal {
-            let size_filter = &self.get_size_filter();
 
-            let filter_sync_heatmap = &self.get_filter_sync_heatmap();
-
-            let signup = container(
-                Column::new()
-                    .spacing(10)
-                    .align_x(Alignment::Center)
-                    .push(
-                        Text::new("Time&Sales > Settings")
-                            .size(16)
-                    )
-                    .push(
-                        Column::new()
-                            .align_x(Alignment::Center)
-                            .push(Text::new("Size Filtering"))
-                            .push(
-                                Slider::new(0.0..=50000.0, *size_filter, move |value| Message::SliderChanged(pane_id, value))
-                                    .step(500.0)
-                            )
-                            .push(
-                                Text::new(format!("${size_filter}")).size(16)
-                            )
-                            .push(
-                                checkbox("Sync Heatmap with", *filter_sync_heatmap)
-                                    .on_toggle(Message::SyncWithHeatmap)
-                            )
-                    )
-                    .push( 
-                        Row::new()
-                            .spacing(10)
-                            .push(
-                                button("Close")
-                                .on_press(Message::CloseModal(pane_id))
-                            )
-                    )
-            )
-            .width(Length::Shrink)
-            .padding(20)
-            .max_width(500)
-            .style(style::chart_modal);
-
-            return modal(underlay, signup, Message::CloseModal(pane_id));
-        } else {
-            underlay
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub enum LayoutId {
+    Layout1,
+    Layout2,
+    Layout3,
+    Layout4,
+}
+impl std::fmt::Display for LayoutId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            LayoutId::Layout1 => write!(f, "Layout 1"),
+            LayoutId::Layout2 => write!(f, "Layout 2"),
+            LayoutId::Layout3 => write!(f, "Layout 3"),
+            LayoutId::Layout4 => write!(f, "Layout 4"),
         }
     }
 }
-impl ChartView for CandlestickChart {
-    fn view(&self, pane: &PaneState) -> Element<Message> {
-        let pane_id = pane.id;
-
-        self.view().map(move |message| Message::ChartUserUpdate(message, pane_id))
-    }
+impl LayoutId {
+    const ALL: [LayoutId; 4] = [LayoutId::Layout1, LayoutId::Layout2, LayoutId::Layout3, LayoutId::Layout4];
 }
 
-fn view_chart<'a, C: ChartView>(
-    pane: &'a PaneState,
-    chart: &'a C,
-) -> Element<'a, Message> {
-    let chart_view: Element<Message> = chart.view(pane);
-
-    let container = Container::new(chart_view)
-        .width(Length::Fill)
-        .height(Length::Fill);
-
-    container.into()
+struct SavedState {
+    layouts: HashMap<LayoutId, Dashboard>,
+    last_active_layout: LayoutId,
+    window_size: Option<(f32, f32)>,
+    window_position: Option<(f32, f32)>,
 }
-
-fn view_starter(
-    pane: &PaneState,
-) -> Element<'_, Message> {
-    let content_names = ["Heatmap chart", "Footprint chart", "Candlestick chart", "Time&Sales"];
-    
-    let content_selector = content_names.iter().fold(
-        Column::new()
-            .spacing(6)
-            .align_x(Alignment::Center), |column, &label| {
-                let mut btn = button(label).width(Length::Fill);
-                if let (Some(exchange), Some(ticker)) = (pane.settings.selected_exchange, pane.settings.selected_ticker) {
-                    let timeframe = pane.settings.selected_timeframe.unwrap_or_else(
-                        || { log::error!("No timeframe found"); Timeframe::M1 }
-                    );
-
-                    let pane_stream: Vec<StreamType> = match label {
-                        "Heatmap chart" | "Time&Sales" => vec![
-                            StreamType::DepthAndTrades { exchange, ticker }
-                        ],
-                        "Footprint chart" => vec![
-                            StreamType::DepthAndTrades { exchange, ticker }, 
-                            StreamType::Kline { exchange, ticker, timeframe }
-                        ],
-                        "Candlestick chart" => vec![
-                            StreamType::Kline { exchange, ticker, timeframe }
-                        ],
-                        _ => vec![]
-                    };
-                
-                    btn = btn.on_press(
-                        Message::PaneContentSelected(label.to_string(), pane.id, pane_stream)
-                    );
-                }
-                column.push(btn)
-            }
-    );
-
-    let symbol_selector = pick_list(
-        &Ticker::ALL[..],
-        pane.settings.selected_ticker,
-        move |ticker| Message::TickerSelected(ticker, pane.id),
-    ).placeholder("ticker...").text_size(13).width(Length::Fill);
-
-    let exchange_selector = pick_list(
-        &Exchange::ALL[..],
-        pane.settings.selected_exchange,
-        move |exchange| Message::ExchangeSelected(exchange, pane.id),
-    ).placeholder("exchange...").text_size(13).width(Length::Fill);
-
-    let picklists = Row::new()
-        .spacing(6)
-        .align_y(Alignment::Center)
-        .push(exchange_selector.style(style::picklist_primary).menu_style(style::picklist_menu_primary))
-        .push(symbol_selector.style(style::picklist_primary).menu_style(style::picklist_menu_primary));
-
-    let column = Column::new()
-        .padding(10)
-        .spacing(10)
-        .align_x(Alignment::Center)
-        .push(picklists)
-        .push(content_selector);
+impl Default for SavedState {
+    fn default() -> Self {
+        let mut layouts = HashMap::new();
+        layouts.insert(LayoutId::Layout1, Dashboard::default());
+        layouts.insert(LayoutId::Layout2, Dashboard::default());
+        layouts.insert(LayoutId::Layout3, Dashboard::default());
+        layouts.insert(LayoutId::Layout4, Dashboard::default());
         
-    let container = Container::new(
-        Column::new()
-            .spacing(10)
-            .padding(20)
-            .align_x(Alignment::Center)
-            .max_width(300)
-            .push(
-                Text::new("Initialize the pane").size(16)
-            )
-            .push(scrollable(column))
-        ).align_x(alignment::Horizontal::Center);
-    
-    container.into()
-}
-
-fn view_controls<'a>(
-    pane: pane_grid::Pane,
-    pane_id: Uuid,
-    pane_type: &PaneContent,
-    total_panes: usize,
-    is_maximized: bool,
-    settings: &PaneSettings,
-) -> Element<'a, Message> {
-    let mut row = row![].spacing(5);
-
-    let (icon, message) = if is_maximized {
-        (Icon::ResizeSmall, Message::Restore)
-    } else {
-        (Icon::ResizeFull, Message::Maximize(pane))
-    };
-
-    match pane_type {
-        PaneContent::Heatmap(_) => {
-            let ticksize_picker = pick_list(
-                [TickMultiplier(1), TickMultiplier(2), TickMultiplier(5), TickMultiplier(10), TickMultiplier(25), TickMultiplier(50)],
-                settings.tick_multiply, 
-                move |tick_multiply| Message::TicksizeSelected(tick_multiply, pane_id)
-            ).placeholder("Ticksize multiplier...").text_size(11).width(iced::Pixels(80.0));
-
-            let ticksize_tooltip = tooltip(
-                ticksize_picker
-                    .style(style::picklist_primary)
-                    .menu_style(style::picklist_menu_primary),
-                    "Ticksize multiplier",
-                    tooltip::Position::FollowCursor
-                )
-                .style(style::tooltip);
-    
-            row = row.push(ticksize_tooltip);
-        },
-        PaneContent::TimeAndSales(_) => {
-        },
-        PaneContent::Footprint(_) => {
-            let timeframe_picker = pick_list(
-                &Timeframe::ALL[..],
-                settings.selected_timeframe,
-                move |timeframe| Message::TimeframeSelected(timeframe, pane_id),
-            ).placeholder("Choose a timeframe...").text_size(11).width(iced::Pixels(80.0));
-    
-            let tf_tooltip = tooltip(
-                timeframe_picker
-                    .style(style::picklist_primary)
-                    .menu_style(style::picklist_menu_primary),
-                    "Timeframe",
-                    tooltip::Position::FollowCursor
-                )
-                .style(style::tooltip);
-    
-            row = row.push(tf_tooltip);
-
-            let ticksize_picker = pick_list(
-                [TickMultiplier(1), TickMultiplier(2), TickMultiplier(5), TickMultiplier(10), TickMultiplier(25), TickMultiplier(50), TickMultiplier(100), TickMultiplier(200)],
-                settings.tick_multiply, 
-                move |tick_multiply| Message::TicksizeSelected(tick_multiply, pane_id)
-            ).placeholder("Ticksize multiplier...").text_size(11).width(iced::Pixels(80.0));
-            
-            let ticksize_tooltip = tooltip(
-                ticksize_picker
-                    .style(style::picklist_primary)
-                    .menu_style(style::picklist_menu_primary),
-                    "Ticksize multiplier",
-                    tooltip::Position::FollowCursor
-                )
-                .style(style::tooltip);
-    
-            row = row.push(ticksize_tooltip);
-        },
-        PaneContent::Candlestick(_) => {
-            let timeframe_picker = pick_list(
-                &Timeframe::ALL[..],
-                settings.selected_timeframe,
-                move |timeframe| Message::TimeframeSelected(timeframe, pane_id),
-            ).placeholder("Choose a timeframe...").text_size(11).width(iced::Pixels(80.0));
-    
-            let tooltip = tooltip(
-                timeframe_picker
-                    .style(style::picklist_primary)
-                    .menu_style(style::picklist_menu_primary),
-                    "Timeframe", 
-                    tooltip::Position::FollowCursor
-                )
-                .style(style::tooltip);
-    
-            row = row.push(tooltip);
-        },
-        PaneContent::Starter => {
-        },
-    }
-
-    let mut buttons = vec![
-        (container(text(char::from(Icon::Cog).to_string()).font(ICON_FONT).size(14)).width(25).center_x(iced::Pixels(25.0)), Message::OpenModal(pane)),
-        (container(text(char::from(icon).to_string()).font(ICON_FONT).size(14)).width(25).center_x(iced::Pixels(25.0)), message),
-    ];
-
-    if total_panes > 1 {
-        buttons.push((container(text(char::from(Icon::Close).to_string()).font(ICON_FONT).size(14)).width(25).center_x(iced::Pixels(25.0)), Message::Close(pane)));
-    }
-
-    for (content, message) in buttons {        
-        row = row.push(
-            button(content)
-                .style(style::button_primary)
-                .padding(3)
-                .on_press(message),
-        );
-    } 
-
-    row.into()
-}
-
-fn klines_fetch_all_task(stream_types: &HashMap<Exchange, HashMap<Ticker, HashSet<StreamType>>>) -> Vec<Task<Message>> {
-    let mut tasks: Vec<Task<Message>> = vec![];
-
-    for (exchange, stream) in stream_types {
-        let mut kline_fetches = Vec::new();
-
-        for stream_types in stream.values() {
-            for stream_type in stream_types {
-                match stream_type {
-                    StreamType::Kline { ticker, timeframe, .. } => {
-                        kline_fetches.push((*ticker, *timeframe));
-                    },
-                    _ => {}
-                }
-            }
-        }
-
-        for (ticker, timeframe) in kline_fetches {
-            let ticker = ticker;
-            let timeframe = timeframe;
-            let exchange = *exchange;
-
-            match exchange {
-                Exchange::BinanceFutures => {
-                    let fetch_klines = Task::perform(
-                        binance::market_data::fetch_klines(ticker, timeframe)
-                            .map_err(|err| format!("{err}")),
-                        move |klines| Message::FetchDistributeKlines(
-                            StreamType::Kline { exchange, ticker, timeframe }, klines
-                        )
-                    );
-                    tasks.push(fetch_klines);
-                },
-                Exchange::BybitLinear => {
-                    let fetch_klines = Task::perform(
-                        bybit::market_data::fetch_klines(ticker, timeframe)
-                            .map_err(|err| format!("{err}")),
-                        move |klines| Message::FetchDistributeKlines(
-                            StreamType::Kline { exchange, ticker, timeframe }, klines
-                        )
-                    );
-                    tasks.push(fetch_klines);
-                }
-            }
-        }
-    }
-
-    tasks
-}
-
-fn ticksize_fetch_all_task(stream_types: &HashMap<Exchange, HashMap<Ticker, HashSet<StreamType>>>) -> Vec<Task<Message>> {
-    let mut tasks: Vec<Task<Message>> = vec![];
-
-    for (exchange, stream) in stream_types {
-        let mut ticksize_fetches = Vec::new();
-
-        for stream_types in stream.values() {
-            for stream_type in stream_types {
-                match stream_type {
-                    StreamType::DepthAndTrades { ticker, .. } => {
-                        ticksize_fetches.push(*ticker);
-                    },
-                    _ => {}
-                }
-            }
-        }
-
-        for ticker in ticksize_fetches {
-            let ticker = ticker;
-            let exchange = *exchange;
-
-            match exchange {
-                Exchange::BinanceFutures => {
-                    let fetch_ticksize = Task::perform(
-                        binance::market_data::fetch_ticksize(ticker)
-                            .map_err(|err| format!("{err}")),
-                        move |ticksize| Message::FetchDistributeTicks(
-                            StreamType::DepthAndTrades { exchange, ticker }, ticksize
-                        )
-                    );
-                    tasks.push(fetch_ticksize);
-                },
-                Exchange::BybitLinear => {
-                    let fetch_ticksize = Task::perform(
-                        bybit::market_data::fetch_ticksize(ticker)
-                            .map_err(|err| format!("{err}")),
-                        move |ticksize| Message::FetchDistributeTicks(
-                            StreamType::DepthAndTrades { exchange, ticker }, ticksize
-                        )
-                    );
-                    tasks.push(fetch_ticksize);
-                }
-            }
+        SavedState {
+            layouts,
+            last_active_layout: LayoutId::Layout1,
+            window_size: None,
+            window_position: None,
         }
     }
-
-    tasks
 }
 
-fn create_fetch_klines_task(
-    stream: StreamType,
-    pane_id: Uuid,
-) -> Task<Message> {
-    match stream {
-        StreamType::Kline { exchange, ticker, timeframe } => {
-            match exchange {
-                Exchange::BinanceFutures => Task::perform(
-                    binance::market_data::fetch_klines(ticker, timeframe)
-                        .map_err(|err| format!("{err}")),
-                    move |klines| Message::FetchEvent(klines, stream, pane_id),
-                ),
-                Exchange::BybitLinear => Task::perform(
-                    bybit::market_data::fetch_klines(ticker, timeframe)
-                        .map_err(|err| format!("{err}")),
-                    move |klines| Message::FetchEvent(klines, stream, pane_id),
-                ),
-            }
-        },
-        _ => Task::none(),
-    }
-}
+use std::fs::File;
+use std::io::{Read, Write};
+use std::path::Path;
+use serde::{Deserialize, Serialize};
 
-fn create_fetch_ticksize_task(
-    exchange: &Exchange,
-    ticker: &Ticker,
-    pane_id: Uuid,
-) -> Task<Message> {
-    match exchange {
-        Exchange::BinanceFutures => Task::perform(
-            binance::market_data::fetch_ticksize(*ticker),
-            move |result| match result {
-                Ok(ticksize) => Message::SetMinTickSize(ticksize, pane_id),
-                Err(err) => Message::ErrorOccurred(Error::FetchError(err.to_string())),
-            },
-        ),
-        Exchange::BybitLinear => Task::perform(
-            bybit::market_data::fetch_ticksize(*ticker),
-            move |result| match result {
-                Ok(ticksize) => Message::SetMinTickSize(ticksize, pane_id),
-                Err(err) => Message::ErrorOccurred(Error::FetchError(err.to_string())),
-            },
-        ),
-    }
+fn write_json_to_file(json: &str, file_path: &str) -> std::io::Result<()> {
+    let path = Path::new(file_path);
+    let mut file = File::create(path)?;
+    file.write_all(json.as_bytes())?;
+    Ok(())
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Event {
-    CloseRequested(window::Id),
-    Copy,
-    Escape,
-    Home,
-    End,
+fn read_layout_from_file(file_path: &str) -> Result<SerializableState, Box<dyn std::error::Error>> {
+    let path = Path::new(file_path);
+    let mut file = File::open(path)?;
+    let mut contents = String::new();
+    file.read_to_string(&mut contents)?;
+   
+    Ok(serde_json::from_str(&contents)?)
 }
 
-pub fn events() -> Subscription<Event> {
-    iced::event::listen_with(filtered_events)
+#[derive(Debug, Clone, Deserialize, Serialize)]
+struct SerializableState {
+    pub layouts: HashMap<LayoutId, SerializableDashboard>,
+    pub last_active_layout: LayoutId,
+    pub window_size: Option<(f32, f32)>,
+    pub window_position: Option<(f32, f32)>,
 }
-
-fn filtered_events(
-    event: iced::Event,
-    _status: iced::event::Status,
-    window: window::Id,
-) -> Option<Event> {
-    match &event {
-        iced::Event::Window(window::Event::CloseRequested) => Some(Event::CloseRequested(window)),
-        _ => None,
+impl SerializableState {
+    fn from_parts(
+        layouts: HashMap<LayoutId, SerializableDashboard>,
+        last_active_layout: LayoutId,
+        size: Option<Size>,
+        position: Option<Point>,
+    ) -> Self {
+        SerializableState {
+            layouts,
+            last_active_layout,
+            window_size: size.map(|s| (s.width, s.height)),
+            window_position: position.map(|p| (p.x, p.y)),
+        }
     }
 }

+ 549 - 110
src/screen/dashboard.rs

@@ -1,25 +1,38 @@
 pub mod pane;
 
+use futures::TryFutureExt;
 use pane::SerializablePane;
 pub use pane::{Uuid, PaneState, PaneContent, PaneSettings};
 use serde::{Deserialize, Serialize};
 
 use crate::{
-    charts::{candlestick::CandlestickChart, footprint::FootprintChart, Message}, data_providers::{
-        Depth, Exchange, Kline, TickMultiplier, Ticker, Timeframe, Trade
-    }, StreamType
+    charts::{candlestick::CandlestickChart, footprint::FootprintChart, heatmap::HeatmapChart, timeandsales::TimeAndSales, Message as ChartMessage}, data_providers::{
+        binance, bybit, Depth, Exchange, Kline, TickMultiplier, Ticker, Timeframe, Trade
+    }, modal, style, StreamType
 };
 
 use super::{Error, Notification};
 
-use std::{collections::{HashMap, HashSet}, io::Read, rc::Rc};
-use iced::{widget::pane_grid::{self, Configuration}, Point, Size};
+use std::{collections::{HashMap, HashSet}, rc::Rc};
+use iced::{widget::{button, container, pane_grid::{self, Configuration}, Column, PaneGrid, Text}, window, Alignment, Element, Length, Point, Size, Task};
+
+#[derive(Debug, Clone)]
+pub enum Message {
+    Pane(pane::Message),
+    ErrorOccurred(Error),
+    Notification(Notification),
+    FetchEvent(Result<Vec<Kline>, String>, StreamType, Uuid),
+    FetchDistributeKlines(StreamType, Result<Vec<Kline>, String>),
+    FetchDistributeTicks(StreamType, Result<f32, String>),
+    FetchForLayout,
+}
 
 pub struct Dashboard {
     pub panes: pane_grid::State<PaneState>,
     pub focus: Option<pane_grid::Pane>,
-    pub pane_lock: bool,
-    pub show_layout_modal: bool,
+    pub layout_lock: bool,
+    pub pane_streams: HashMap<Exchange, HashMap<Ticker, HashSet<StreamType>>>,
+    pub notification: Option<Notification>,
 }
 impl Dashboard {
     pub fn empty() -> Self {
@@ -88,8 +101,9 @@ impl Dashboard {
         Self { 
             panes: pane_grid::State::with_configuration(pane_config),
             focus: None,
-            pane_lock: false,
-            show_layout_modal: false,
+            layout_lock: false,
+            pane_streams: HashMap::new(),
+            notification: None,
         }
     }
 
@@ -97,18 +111,380 @@ impl Dashboard {
         Self {
             panes: pane_grid::State::with_configuration(panes),
             focus: None,
-            pane_lock: false,
-            show_layout_modal: false,
+            layout_lock: false,
+            pane_streams: HashMap::new(),
+            notification: None,
         }
     }
 
-    pub fn replace_new_pane(&mut self, pane: pane_grid::Pane) {
+    pub fn update(&mut self, message: Message) -> Task<Message> {
+        match message {
+            Message::Pane(message) => {
+                match message {
+                    pane::Message::PaneClicked(pane_id) => {
+                        self.focus = Some(pane_id);
+                    },
+                    pane::Message::PaneResized(pane_grid::ResizeEvent { split, ratio })=> {
+                        self.panes.resize(split, ratio);
+                    },
+                    pane::Message::PaneDragged(event) => {
+                        match event {
+                            pane_grid::DragEvent::Dropped { pane, target } => {
+                                self.panes.drop(pane, target);
+
+                                self.focus = None;
+                            },
+                            _ => {}
+                        }
+                    },
+                    pane::Message::SplitPane(axis, pane) => {        
+                        let focus_pane = if let Some((new_pane, _)) = 
+                            self.panes.split(axis, pane, PaneState::new(Uuid::new_v4(), vec![], PaneSettings::default())) {
+                                    Some(new_pane)
+                                } else {
+                                    None
+                                };
+        
+                        if Some(focus_pane).is_some() {
+                            self.focus = focus_pane;
+                        }
+                    },
+                    pane::Message::ClosePane(pane) => {
+                        if let Some((_, sibling)) = self.panes.close(pane) {
+                            self.focus = Some(sibling);
+                        }
+                    },
+                    pane::Message::MaximizePane(pane) => {
+                        self.panes.maximize(pane);
+                    },
+                    pane::Message::Restore => {
+                        self.panes.restore();
+                    },
+                    pane::Message::TickerSelected(ticker, pane_id) => {
+                        if let Ok(settings) = self.get_pane_settings_mut(pane_id) {
+                            settings.selected_ticker = Some(ticker);
+                        }
+                    },
+                    pane::Message::ExchangeSelected(exchange, pane_id) => {
+                        if let Ok(settings) = self.get_pane_settings_mut(pane_id) {
+                            settings.selected_exchange = Some(exchange);
+                        }
+                    },
+                    pane::Message::ReplacePane(pane_id) => {
+                        self.replace_new_pane(pane_id);
+                    },
+                    pane::Message::ShowModal(pane_id) => {
+                        if let Some(pane) = self.panes.get_mut(pane_id) {
+                            pane.show_modal = true;
+                        };
+                    },
+                    pane::Message::HideModal(pane_id) => {
+                        for (_, pane_state) in self.panes.iter_mut() {
+                            if pane_state.id == pane_id {
+                                pane_state.show_modal = false;
+                            }
+                        }
+                    },
+                    pane::Message::ChartUserUpdate(message, pane_id) => {
+                        match self.update_chart_state(pane_id, message) {
+                            Ok(_) => return Task::none(),
+                            Err(err) => {      
+                                return Task::perform(
+                                    async { err },
+                                    move |err: Error| Message::ErrorOccurred(err)
+                                );
+                            }
+                        }
+                    },
+                    pane::Message::SliderChanged(pane_id, value) => {
+                        match self.set_pane_size_filter(pane_id, value) {
+                            Ok(_) => {
+                                log::info!("Size filter set to {value}");
+
+                                return Task::none()
+                            }
+                            Err(err) => {
+                                return Task::perform(
+                                    async { err },
+                                    move |err: Error| Message::ErrorOccurred(err)
+                                )
+                            }
+                        }
+                    },
+                    pane::Message::PaneContentSelected(content, pane_id, pane_stream) => {        
+                        let mut tasks = vec![];
+                            
+                        let pane_content = match content.as_str() {
+                            "Heatmap chart" => PaneContent::Heatmap(
+                                HeatmapChart::new(1.0)
+                            ),
+                            "Footprint chart" => {
+                                PaneContent::Footprint(
+                                    FootprintChart::new(1, 1.0, vec![], vec![])
+                                )
+                            },
+                            "Candlestick chart" => {
+                                PaneContent::Candlestick(
+                                    CandlestickChart::new(vec![], 1)
+                                )
+                            },
+                            "Time&Sales" => PaneContent::TimeAndSales(
+                                TimeAndSales::new()
+                            ),
+                            _ => return Task::none(),
+                        };
+        
+                        // set pane's stream and content identifiers
+                        if let Err(err) = self.set_pane_content(pane_id, pane_content) {
+                            log::error!("Failed to set pane content: {}", err);
+                        } else {
+                            log::info!("Pane content set: {content}");
+                        }
+                        
+                        if let Err(err) = self.set_pane_stream(pane_id, pane_stream.to_vec()) {
+                            log::error!("Failed to set pane stream: {err}");
+                        } else {
+                            log::info!("Pane stream set: {pane_stream:?}");
+                        }
+                    
+                        // prepare unique streams for websocket
+                        for stream in pane_stream.iter() {
+                            match stream {
+                                StreamType::Kline { exchange, ticker, .. } | StreamType::DepthAndTrades { exchange, ticker } => {
+                                    self.pane_streams
+                                        .entry(*exchange)
+                                        .or_default()
+                                        .entry(*ticker)
+                                        .or_default()
+                                        .insert(*stream);
+                                }
+                                _ => {}
+                            }
+                        }
+                    
+                        log::info!("{:?}", &self.pane_streams);
+        
+                        // get fetch tasks for pane's content
+                        if ["Footprint chart", "Candlestick chart", "Heatmap chart"].contains(&content.as_str()) {
+                            for stream in pane_stream.iter() {
+                                match stream {
+                                    StreamType::Kline { exchange, ticker, .. } => {
+                                        if ["Candlestick chart", "Footprint chart"].contains(&content.as_str()) {
+                                            tasks.push(create_fetch_klines_task(*stream, pane_id));
+                                            
+                                            if content == "Footprint chart" {
+                                                tasks.push(create_fetch_ticksize_task(exchange, ticker, pane_id));
+                                            }
+                                        }
+                                    },
+                                    StreamType::DepthAndTrades { exchange, ticker } => {
+                                        tasks.push(create_fetch_ticksize_task(exchange, ticker, pane_id));
+                                    },
+                                    _ => {}
+                                }
+                            }
+                        }
+                        
+                        return Task::batch(tasks)
+                    },
+                    pane::Message::TimeframeSelected(timeframe, pane_id) => {    
+                        let mut tasks = vec![];
+                
+                        match self.set_pane_timeframe(pane_id, timeframe) {
+                            Ok(stream_type) => {
+                                if let StreamType::Kline { exchange, ticker, timeframe } = stream_type {
+                                    let stream = *stream_type;
+                    
+                                    match exchange {
+                                        Exchange::BinanceFutures => {
+                                            tasks.push(
+                                                Task::perform(
+                                                    binance::market_data::fetch_klines(*ticker, *timeframe)
+                                                        .map_err(|err| format!("{err}")),
+                                                    move |klines| Message::FetchEvent(klines, stream, pane_id)
+                                                )
+                                            );
+                                        },
+                                        Exchange::BybitLinear => {                                    
+                                            tasks.push(
+                                                Task::perform(
+                                                    bybit::market_data::fetch_klines(*ticker, *timeframe)
+                                                        .map_err(|err| format!("{err}")),
+                                                    move |klines| Message::FetchEvent(klines, stream, pane_id)
+                                                )
+                                            );
+                                        },
+                                    }
+        
+                                    tasks.push(
+                                        Task::perform(
+                                            async {},
+                                            move |_| Message::Notification(Notification::Info("Fetching for klines...".to_string()))
+                                        )
+                                    );
+        
+                                    self.pane_streams = self.get_all_diff_streams();
+                                }
+                            },
+                            Err(err) => {
+                                tasks.push(Task::perform(
+                                    async { err },
+                                    move |err: Error| Message::ErrorOccurred(err)
+                                ));
+                            }
+                        }
+        
+                        return Task::batch(tasks)
+                    },
+                    pane::Message::TicksizeSelected(tick_multiply, pane_id) => {                        
+                        match self.set_pane_ticksize(pane_id, tick_multiply) {
+                            Ok(_) => {
+                            },
+                            Err(err) => {            
+                                return Task::perform(
+                                    async { err },
+                                    move |err: Error| Message::ErrorOccurred(err)
+                                )
+                            }
+                        }
+                    },
+                    pane::Message::SetMinTickSize(pane_id, ticksize) => {        
+                        match self.get_pane_settings_mut(pane_id) {
+                            Ok(pane_settings) => {
+                                pane_settings.min_tick_size = Some(ticksize);
+                            },
+                            Err(err) => {
+                                return Task::perform(
+                                    async { err },
+                                    move |err: Error| Message::ErrorOccurred(err)
+                                )
+                            }
+                        }
+                    },
+                }
+            },
+            Message::ErrorOccurred(err) => {
+                dbg!(err);
+            },
+            Message::Notification(notification) => {
+                dbg!(notification);
+            },
+            Message::FetchEvent(klines, pane_stream, pane_id) => {
+                if let Some(notification) = &self.notification {
+                    match notification {
+                        Notification::Info(_) => {
+                            self.notification = None;
+                        },
+                        _ => {}
+                    }
+                }
+               
+                match klines {
+                    Ok(klines) => {
+                        if let StreamType::Kline { .. } = pane_stream {
+                            self.insert_klines_vec(&pane_stream, &klines, pane_id);
+                        } else {
+                            log::error!("Invalid stream type for klines: {pane_stream:?}");
+                        }
+                    },
+                    Err(err) => {
+                        return Task::perform(
+                            async { err },
+                            move |err: String| Message::ErrorOccurred(Error::FetchError(err))
+                        )
+                    }
+                }
+            },
+            Message::FetchDistributeKlines(stream_type, klines) => {
+                match klines {
+                    Ok(klines) => {
+                        if let Err(err) = self.find_and_insert_klines(&stream_type, &klines) {
+                            log::error!("{err}");
+                        }
+                    },
+                    Err(err) => {
+                        log::error!("{err}");
+                    }
+                }
+            },  
+            Message::FetchDistributeTicks(stream_type, min_tick_size) => {
+                match min_tick_size {
+                    Ok(ticksize) => {
+                        if let Err(err) = self.find_and_insert_ticksizes(&stream_type, ticksize) {
+                            log::error!("{err}");
+                        }
+                    },
+                    Err(err) => {
+                        log::error!("{err}");
+                    }
+                }
+            },
+            Message::FetchForLayout => {
+                let mut tasks = vec![];
+
+                let pane_streams = self.get_all_diff_streams();
+
+                tasks.extend(
+                    klines_fetch_all_task(&pane_streams)
+                );
+                tasks.extend(
+                    ticksize_fetch_all_task(&pane_streams)
+                );
+ 
+                return Task::batch(tasks)
+            },
+        }
+
+        Task::none()
+    }
+
+    pub fn view<'a>(&'a self) -> Element<'a, Message> {
+        let focus = self.focus;
+        let pane_locked = self.layout_lock;
+        
+        let mut pane_grid = PaneGrid::new(&self.panes, |id, pane, maximized| {
+            let is_focused = !pane_locked && focus == Some(id);
+            pane.view(
+                id,
+                self.panes.len(),
+                is_focused,
+                maximized,
+            )
+        })
+        .spacing(4);
+    
+        if !pane_locked {
+            pane_grid = pane_grid
+                .on_click(pane::Message::PaneClicked)
+                .on_resize(6, pane::Message::PaneResized)
+                .on_drag(pane::Message::PaneDragged);
+        }
+    
+        let pane_grid: Element<_> = pane_grid.into();
+
+        let pane_grid = container(pane_grid.map(Message::Pane))
+            .width(Length::Fill)
+            .height(Length::Fill);
+
+        pane_grid.into()
+    }
+
+    pub fn layout_changed(&mut self) -> Task<Message> {
+        self.pane_streams = self.get_all_diff_streams();
+
+        Task::perform(
+            async {},
+            move |_| Message::FetchForLayout
+        )
+    }
+
+    fn replace_new_pane(&mut self, pane: pane_grid::Pane) {
         if let Some(pane) = self.panes.get_mut(pane) {
             *pane = PaneState::new(Uuid::new_v4(), vec![], PaneSettings::default());
         }
     }
 
-    pub fn get_pane_settings_mut(&mut self, pane_id: Uuid) -> Result<&mut PaneSettings, Error> {
+    fn get_pane_settings_mut(&mut self, pane_id: Uuid) -> Result<&mut PaneSettings, Error> {
         for (_, pane_state) in self.panes.iter_mut() {
             if pane_state.id == pane_id {
                 return Ok(&mut pane_state.settings);
@@ -117,7 +493,7 @@ impl Dashboard {
         Err(Error::UnknownError("No pane found".to_string()))
     }
 
-    pub fn set_pane_content(&mut self, pane_id: Uuid, content: PaneContent) -> Result<(), &str> {
+    fn set_pane_content(&mut self, pane_id: Uuid, content: PaneContent) -> Result<(), &str> {
         for (_, pane_state) in self.panes.iter_mut() {
             if pane_state.id == pane_id {
                 pane_state.content = content;
@@ -128,7 +504,7 @@ impl Dashboard {
         Err("No pane found")
     }
 
-    pub fn set_pane_stream(&mut self, pane_id: Uuid, stream: Vec<StreamType>) -> Result<(), &str> {
+    fn set_pane_stream(&mut self, pane_id: Uuid, stream: Vec<StreamType>) -> Result<(), &str> {
         for (_, pane_state) in self.panes.iter_mut() {
             if pane_state.id == pane_id {
                 pane_state.stream = stream;
@@ -139,7 +515,7 @@ impl Dashboard {
         Err("No pane found")
     }
 
-    pub fn set_pane_ticksize(&mut self, pane_id: Uuid, new_tick_multiply: TickMultiplier) -> Result<(), Error> {
+    fn set_pane_ticksize(&mut self, pane_id: Uuid, new_tick_multiply: TickMultiplier) -> Result<(), Error> {
         for (_, pane_state) in self.panes.iter_mut() {
             if pane_state.id == pane_id {
                 pane_state.settings.tick_multiply = Some(new_tick_multiply);
@@ -172,7 +548,7 @@ impl Dashboard {
         Err(Error::UnknownError("No pane found to change ticksize".to_string()))
     }
     
-    pub fn set_pane_timeframe(&mut self, pane_id: Uuid, new_timeframe: Timeframe) -> Result<&StreamType, Error> {
+    fn set_pane_timeframe(&mut self, pane_id: Uuid, new_timeframe: Timeframe) -> Result<&StreamType, Error> {
         for (_, pane_state) in self.panes.iter_mut() {
             if pane_state.id == pane_id {
                 pane_state.settings.selected_timeframe = Some(new_timeframe);
@@ -200,7 +576,7 @@ impl Dashboard {
         Err(Error::UnknownError("No pane found to change tiemframe".to_string()))
     }
 
-    pub fn set_pane_size_filter(&mut self, pane_id: Uuid, new_size_filter: f32) -> Result<(), Error> {
+    fn set_pane_size_filter(&mut self, pane_id: Uuid, new_size_filter: f32) -> Result<(), Error> {
         for (_, pane_state) in self.panes.iter_mut() {
             if pane_state.id == pane_id {
                 pane_state.settings.trade_size_filter = Some(new_size_filter);
@@ -336,6 +712,8 @@ impl Dashboard {
         if found_match {
             Ok(())
         } else {
+            self.pane_streams = self.get_all_diff_streams();
+
             Err("No matching pane found for the stream")
         }
     }
@@ -369,26 +747,28 @@ impl Dashboard {
         if found_match {
             Ok(())
         } else {
+            self.pane_streams = self.get_all_diff_streams();
+
             Err("No matching pane found for the stream")
         }
     }
 
-    pub fn update_chart_state(&mut self, pane_id: Uuid, message: Message) -> Result<(), Error> {
+    fn update_chart_state(&mut self, pane_id: Uuid, chart_message: ChartMessage) -> Result<(), Error> {
         for (_, pane_state) in self.panes.iter_mut() {
             if pane_state.id == pane_id {
                 match pane_state.content {
                     PaneContent::Heatmap(ref mut chart) => {
-                        chart.update(&message);
+                        chart.update(&chart_message);
 
                         return Ok(());
                     },
                     PaneContent::Footprint(ref mut chart) => {
-                        chart.update(&message);
+                        chart.update(&chart_message);
 
                         return Ok(());
                     },
                     PaneContent::Candlestick(ref mut chart) => {
-                        chart.update(&message);
+                        chart.update(&chart_message);
 
                         return Ok(());
                     },
@@ -401,7 +781,7 @@ impl Dashboard {
         Err(Error::UnknownError("No pane found to update its state".to_string()))
     }
 
-    pub fn get_all_diff_streams(&self) -> HashMap<Exchange, HashMap<Ticker, HashSet<StreamType>>> {
+    pub fn get_all_diff_streams(&mut self) -> HashMap<Exchange, HashMap<Ticker, HashSet<StreamType>>> {
         let mut pane_streams = HashMap::new();
 
         for (_, pane_state) in self.panes.iter() {
@@ -428,11 +808,157 @@ impl Dashboard {
                 }
             }
         }
+        self.pane_streams = pane_streams.clone();
 
         pane_streams
     }
 }
 
+fn create_fetch_klines_task(
+    stream: StreamType,
+    pane_id: Uuid,
+) -> Task<Message> {
+    match stream {
+        StreamType::Kline { exchange, ticker, timeframe } => {
+            match exchange {
+                Exchange::BinanceFutures => Task::perform(
+                    binance::market_data::fetch_klines(ticker, timeframe)
+                        .map_err(|err| format!("{err}")),
+                    move |klines| Message::FetchEvent(klines, stream, pane_id),
+                ),
+                Exchange::BybitLinear => Task::perform(
+                    bybit::market_data::fetch_klines(ticker, timeframe)
+                        .map_err(|err| format!("{err}")),
+                    move |klines| Message::FetchEvent(klines, stream, pane_id),
+                ),
+            }
+        },
+        _ => Task::none(),
+    }
+}
+
+fn create_fetch_ticksize_task(
+    exchange: &Exchange,
+    ticker: &Ticker,
+    pane_id: Uuid,
+) -> Task<Message> {
+    match exchange {
+        Exchange::BinanceFutures => Task::perform(
+            binance::market_data::fetch_ticksize(*ticker),
+            move |result| match result {
+                Ok(ticksize) => Message::Pane(pane::Message::SetMinTickSize(pane_id, ticksize)),
+                Err(err) => Message::ErrorOccurred(Error::FetchError(err.to_string())),
+            },
+        ),
+        Exchange::BybitLinear => Task::perform(
+            bybit::market_data::fetch_ticksize(*ticker),
+            move |result| match result {
+                Ok(ticksize) => Message::Pane(pane::Message::SetMinTickSize(pane_id, ticksize)),
+                Err(err) => Message::ErrorOccurred(Error::FetchError(err.to_string())),
+            },
+        ),
+    }
+}
+
+fn klines_fetch_all_task(stream_types: &HashMap<Exchange, HashMap<Ticker, HashSet<StreamType>>>) -> Vec<Task<Message>> {
+    let mut tasks: Vec<Task<Message>> = vec![];
+
+    for (exchange, stream) in stream_types {
+        let mut kline_fetches = Vec::new();
+
+        for stream_types in stream.values() {
+            for stream_type in stream_types {
+                match stream_type {
+                    StreamType::Kline { ticker, timeframe, .. } => {
+                        kline_fetches.push((*ticker, *timeframe));
+                    },
+                    _ => {}
+                }
+            }
+        }
+
+        for (ticker, timeframe) in kline_fetches {
+            let ticker = ticker;
+            let timeframe = timeframe;
+            let exchange = *exchange;
+
+            match exchange {
+                Exchange::BinanceFutures => {
+                    let fetch_klines = Task::perform(
+                        binance::market_data::fetch_klines(ticker, timeframe)
+                            .map_err(|err| format!("{err}")),
+                        move |klines| Message::FetchDistributeKlines(
+                            StreamType::Kline { exchange, ticker, timeframe }, klines
+                        )
+                    );
+                    tasks.push(fetch_klines);
+                },
+                Exchange::BybitLinear => {
+                    let fetch_klines = Task::perform(
+                        bybit::market_data::fetch_klines(ticker, timeframe)
+                            .map_err(|err| format!("{err}")),
+                        move |klines| Message::FetchDistributeKlines(
+                            StreamType::Kline { exchange, ticker, timeframe }, klines
+                        )
+                    );
+                    tasks.push(fetch_klines);
+                }
+            }
+        }
+    }
+
+    tasks
+}
+
+fn ticksize_fetch_all_task(stream_types: &HashMap<Exchange, HashMap<Ticker, HashSet<StreamType>>>) -> Vec<Task<Message>> {
+    let mut tasks: Vec<Task<Message>> = vec![];
+
+    for (exchange, stream) in stream_types {
+        let mut ticksize_fetches = Vec::new();
+
+        for stream_types in stream.values() {
+            for stream_type in stream_types {
+                match stream_type {
+                    StreamType::DepthAndTrades { ticker, .. } => {
+                        ticksize_fetches.push(*ticker);
+                    },
+                    _ => {}
+                }
+            }
+        }
+
+        for ticker in ticksize_fetches {
+            let ticker = ticker;
+            let exchange = *exchange;
+
+            match exchange {
+                Exchange::BinanceFutures => {
+                    let fetch_ticksize = Task::perform(
+                        binance::market_data::fetch_ticksize(ticker)
+                            .map_err(|err| format!("{err}")),
+                        move |ticksize| Message::FetchDistributeTicks(
+                            StreamType::DepthAndTrades { exchange, ticker }, ticksize
+                        )
+                    );
+                    tasks.push(fetch_ticksize);
+                },
+                Exchange::BybitLinear => {
+                    let fetch_ticksize = Task::perform(
+                        bybit::market_data::fetch_ticksize(ticker)
+                            .map_err(|err| format!("{err}")),
+                        move |ticksize| Message::FetchDistributeTicks(
+                            StreamType::DepthAndTrades { exchange, ticker }, ticksize
+                        )
+                    );
+                    tasks.push(fetch_ticksize);
+                }
+            }
+        }
+    }
+
+    tasks
+}
+
 impl Default for Dashboard {
     fn default() -> Self {
         Self::empty()
@@ -482,91 +1008,4 @@ impl Default for SerializableDashboard {
             pane: SerializablePane::Starter,
         }
     }
-}
-
-pub struct SavedState {
-    pub layouts: HashMap<LayoutId, Dashboard>,
-    pub last_active_layout: LayoutId,
-    pub window_size: Option<(f32, f32)>,
-    pub window_position: Option<(f32, f32)>,
-}
-impl Default for SavedState {
-    fn default() -> Self {
-        let mut layouts = HashMap::new();
-        layouts.insert(LayoutId::Layout1, Dashboard::default());
-        layouts.insert(LayoutId::Layout2, Dashboard::default());
-        layouts.insert(LayoutId::Layout3, Dashboard::default());
-        layouts.insert(LayoutId::Layout4, Dashboard::default());
-        
-        SavedState {
-            layouts,
-            last_active_layout: LayoutId::Layout1,
-            window_size: None,
-            window_position: None,
-        }
-    }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
-pub enum LayoutId {
-    Layout1,
-    Layout2,
-    Layout3,
-    Layout4,
-}
-impl std::fmt::Display for LayoutId {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            LayoutId::Layout1 => write!(f, "Layout 1"),
-            LayoutId::Layout2 => write!(f, "Layout 2"),
-            LayoutId::Layout3 => write!(f, "Layout 3"),
-            LayoutId::Layout4 => write!(f, "Layout 4"),
-        }
-    }
-}
-impl LayoutId {
-    pub const ALL: [LayoutId; 4] = [LayoutId::Layout1, LayoutId::Layout2, LayoutId::Layout3, LayoutId::Layout4];
-}
-
-#[derive(Debug, Clone, Deserialize, Serialize)]
-pub struct SerializableState {
-    pub layouts: HashMap<LayoutId, SerializableDashboard>,
-    pub last_active_layout: LayoutId,
-    pub window_size: Option<(f32, f32)>,
-    pub window_position: Option<(f32, f32)>,
-}
-impl SerializableState {
-    pub fn from_parts(
-        layouts: HashMap<LayoutId, SerializableDashboard>,
-        last_active_layout: LayoutId,
-        size: Option<Size>,
-        position: Option<Point>,
-    ) -> Self {
-        SerializableState {
-            layouts,
-            last_active_layout,
-            window_size: size.map(|s| (s.width, s.height)),
-            window_position: position.map(|p| (p.x, p.y)),
-        }
-    }
-}
-
-use std::fs::File;
-use std::io::Write;
-use std::path::Path;
-
-pub fn write_json_to_file(json: &str, file_path: &str) -> std::io::Result<()> {
-    let path = Path::new(file_path);
-    let mut file = File::create(path)?;
-    file.write_all(json.as_bytes())?;
-    Ok(())
-}
-
-pub fn read_layout_from_file(file_path: &str) -> Result<SerializableState, Box<dyn std::error::Error>> {
-    let path = Path::new(file_path);
-    let mut file = File::open(path)?;
-    let mut contents = String::new();
-    file.read_to_string(&mut contents)?;
-   
-    Ok(serde_json::from_str(&contents)?)
 }

+ 440 - 5
src/screen/dashboard/pane.rs

@@ -1,18 +1,39 @@
 use std::fmt;
 
+use iced::{alignment, widget::{button, container, pane_grid, pick_list, row, scrollable, text, tooltip, Column, Container, Row, Slider, Text}, Alignment, Element, Length, Renderer, Theme};
 use serde::{Deserialize, Serialize};
 pub use uuid::Uuid;
 
 use crate::{
     charts::{
-        candlestick::CandlestickChart, footprint::FootprintChart, heatmap::HeatmapChart, timeandsales::TimeAndSales
-    }, 
-    data_providers::{
+        self, candlestick::CandlestickChart, footprint::FootprintChart, heatmap::HeatmapChart, timeandsales::TimeAndSales
+    }, data_providers::{
         Exchange, TickMultiplier, Ticker, Timeframe
-    }, 
-    StreamType
+    }, modal, style::{self, Icon, ICON_FONT}, StreamType
 };
 
+#[derive(Debug, Clone)]
+pub enum Message {
+    PaneClicked(pane_grid::Pane),
+    PaneResized(pane_grid::ResizeEvent),
+    PaneDragged(pane_grid::DragEvent),
+    ClosePane(pane_grid::Pane),
+    SplitPane(pane_grid::Axis, pane_grid::Pane),
+    MaximizePane(pane_grid::Pane),
+    Restore,
+    TicksizeSelected(TickMultiplier, Uuid),
+    TimeframeSelected(Timeframe, Uuid),
+    TickerSelected(Ticker, Uuid),
+    ExchangeSelected(Exchange, Uuid),
+    ShowModal(pane_grid::Pane),
+    HideModal(Uuid),
+    PaneContentSelected(String, Uuid, Vec<StreamType>),
+    ReplacePane(pane_grid::Pane),
+    ChartUserUpdate(charts::Message, Uuid),
+    SliderChanged(Uuid, f32),
+    SetMinTickSize(Uuid, f32),
+}
+
 #[derive(Debug)]
 pub struct PaneState {
     pub id: Uuid,
@@ -43,11 +64,425 @@ impl PaneState {
         }
     }
 
+    pub fn view<'a>(
+        &'a self,
+        id: pane_grid::Pane,
+        panes: usize,
+        is_focused: bool,
+        maximized: bool,
+    ) -> iced::widget::pane_grid::Content<'a, Message, Theme, Renderer> {
+        let stream_info = self.stream.iter().find_map(|stream: &StreamType| {
+            match stream {
+                StreamType::Kline { exchange, ticker, timeframe } => {
+                    Some(
+                        Some((exchange, format!("{} {}", ticker, timeframe)))
+                    )
+                }
+                _ => None,
+            }
+        }).or_else(|| {
+            self.stream.iter().find_map(|stream: &StreamType| {
+                match stream {
+                    StreamType::DepthAndTrades { exchange, ticker } => {
+                        Some(
+                            Some((exchange, ticker.to_string()))
+                        )
+                    }
+                    _ => None,
+                }
+            })
+        }).unwrap_or(None);
+
+        let mut stream_info_element: Row<Message> = Row::new();
+
+        if let Some((exchange, info)) = stream_info {
+            stream_info_element = Row::new()
+                .spacing(3)
+                .push(
+                    match exchange {
+                        Exchange::BinanceFutures => text(char::from(Icon::BinanceLogo).to_string()).font(ICON_FONT),
+                        Exchange::BybitLinear => text(char::from(Icon::BybitLogo).to_string()).font(ICON_FONT),
+                    }
+                )
+                .push(Text::new(info));
+        }
+        
+        let mut content: pane_grid::Content<'_, Message, _, Renderer> = 
+            pane_grid::Content::new({
+                match self.content {
+                    PaneContent::Starter => view_starter(&self.id, &self.settings),
+
+                    PaneContent::Heatmap(ref chart) => view_chart(self, chart),
+
+                    PaneContent::Footprint(ref chart) => view_chart(self, chart),
+
+                    PaneContent::Candlestick(ref chart) => view_chart(self, chart),
+
+                    PaneContent::TimeAndSales(ref chart) => view_chart(self, chart),
+                }
+            })
+            .style(
+                if is_focused {
+                    style::pane_focused
+                } else {
+                    style::pane_active
+                }
+            );
+
+        let title_bar = pane_grid::TitleBar::new(stream_info_element)
+            .controls(view_controls(
+                id,
+                self.id,
+                &self.content,
+                panes,
+                maximized,
+                &self.settings,
+            ))
+            .padding(4)
+            .style(
+                if is_focused {
+                    style::title_bar_focused
+                } else {
+                    style::title_bar_active
+                }
+            );
+        content = content.title_bar(title_bar);
+
+        content
+    }
+
     pub fn matches_stream(&self, stream_type: &StreamType) -> bool {
         self.stream.iter().any(|stream| stream == stream_type)
     }
 }
 
+trait ChartView {
+    fn view(&self, id: &PaneState) -> Element<Message>;
+}
+
+impl ChartView for HeatmapChart {
+    fn view(&self, pane: &PaneState) -> Element<Message> {
+        let pane_id = pane.id;
+
+        let underlay = self.view().map(move |message| Message::ChartUserUpdate(message, pane_id));
+
+        if pane.show_modal {
+            let size_filter = &self.get_size_filter();
+
+            let signup: Container<Message, Theme, _> = container(
+                Column::new()
+                    .spacing(10)
+                    .align_x(Alignment::Center)
+                    .push(
+                        Text::new("Heatmap > Settings")
+                            .size(16)
+                    )
+                    .push(
+                        Column::new()
+                            .align_x(Alignment::Center)
+                            .push(Text::new("Size Filtering"))
+                            .push(
+                                Slider::new(0.0..=50000.0, *size_filter, move |value| Message::SliderChanged(pane_id, value))
+                                    .step(500.0)
+                            )
+                            .push(
+                                Text::new(format!("${size_filter}")).size(16)
+                            )
+                    )
+                    .push( 
+                        Row::new()
+                            .spacing(10)
+                            .push(
+                                button("Close")
+                                .on_press(Message::HideModal(pane_id))
+                            )
+                    )
+            )
+            .width(Length::Shrink)
+            .padding(20)
+            .max_width(500)
+            .style(style::chart_modal);
+
+            return modal(underlay, signup, Message::HideModal(pane_id));
+        } else {
+            underlay
+        }
+    }
+}
+impl ChartView for FootprintChart {
+    fn view(&self, pane: &PaneState) -> Element<Message> {
+        let pane_id = pane.id;
+
+        self.view().map(move |message| Message::ChartUserUpdate(message, pane_id))
+    }
+}
+impl ChartView for TimeAndSales {
+    fn view(&self, pane: &PaneState) -> Element<Message> {
+        let pane_id = pane.id;
+
+        let underlay = self.view();
+
+        if pane.show_modal {
+            let size_filter = &self.get_size_filter();
+
+            let signup = container(
+                Column::new()
+                    .spacing(10)
+                    .align_x(Alignment::Center)
+                    .push(
+                        Text::new("Time&Sales > Settings")
+                            .size(16)
+                    )
+                    .push(
+                        Column::new()
+                            .align_x(Alignment::Center)
+                            .push(Text::new("Size Filtering"))
+                            .push(
+                                Slider::new(0.0..=50000.0, *size_filter, move |value| Message::SliderChanged(pane_id, value))
+                                    .step(500.0)
+                            )
+                            .push(
+                                Text::new(format!("${size_filter}")).size(16)
+                            )
+                    )
+                    .push( 
+                        Row::new()
+                            .spacing(10)
+                            .push(
+                                button("Close")
+                                .on_press(Message::HideModal(pane_id))
+                            )
+                    )
+            )
+            .width(Length::Shrink)
+            .padding(20)
+            .max_width(500)
+            .style(style::chart_modal);
+
+            return modal(underlay, signup, Message::HideModal(pane_id));
+        } else {
+            underlay
+        }
+    }
+}
+impl ChartView for CandlestickChart {
+    fn view(&self, pane: &PaneState) -> Element<Message> {
+        let pane_id = pane.id;
+
+        self.view().map(move |message| Message::ChartUserUpdate(message, pane_id))
+    }
+}
+
+fn view_chart<'a, C: ChartView>(
+    pane: &'a PaneState,
+    chart: &'a C,
+) -> Element<'a, Message> {
+    let chart_view: Element<Message> = chart.view(pane);
+
+    let container = Container::new(chart_view)
+        .width(Length::Fill)
+        .height(Length::Fill);
+
+    container.into()
+}
+
+fn view_controls<'a>(
+    pane: pane_grid::Pane,
+    pane_id: Uuid,
+    pane_type: &PaneContent,
+    total_panes: usize,
+    is_maximized: bool,
+    settings: &PaneSettings,
+) -> Element<'a, Message> {
+    let mut row = row![].spacing(5);
+
+    let (icon, message) = if is_maximized {
+        (Icon::ResizeSmall, Message::Restore)
+    } else {
+        (Icon::ResizeFull, Message::MaximizePane(pane))
+    };
+
+    match pane_type {
+        PaneContent::Heatmap(_) => {
+            let ticksize_picker = pick_list(
+                [TickMultiplier(1), TickMultiplier(2), TickMultiplier(5), TickMultiplier(10), TickMultiplier(25), TickMultiplier(50)],
+                settings.tick_multiply, 
+                move |tick_multiply| Message::TicksizeSelected(tick_multiply, pane_id)
+            ).placeholder("Ticksize multiplier...").text_size(11).width(iced::Pixels(80.0));
+
+            let ticksize_tooltip = tooltip(
+                ticksize_picker
+                    .style(style::picklist_primary)
+                    .menu_style(style::picklist_menu_primary),
+                    "Ticksize multiplier",
+                    tooltip::Position::FollowCursor
+                )
+                .style(style::tooltip);
+    
+            row = row.push(ticksize_tooltip);
+        },
+        PaneContent::TimeAndSales(_) => {
+        },
+        PaneContent::Footprint(_) => {
+            let timeframe_picker = pick_list(
+                &Timeframe::ALL[..],
+                settings.selected_timeframe,
+                move |timeframe| Message::TimeframeSelected(timeframe, pane_id),
+            ).placeholder("Choose a timeframe...").text_size(11).width(iced::Pixels(80.0));
+    
+            let tf_tooltip = tooltip(
+                timeframe_picker
+                    .style(style::picklist_primary)
+                    .menu_style(style::picklist_menu_primary),
+                    "Timeframe",
+                    tooltip::Position::FollowCursor
+                )
+                .style(style::tooltip);
+    
+            row = row.push(tf_tooltip);
+
+            let ticksize_picker = pick_list(
+                [TickMultiplier(1), TickMultiplier(2), TickMultiplier(5), TickMultiplier(10), TickMultiplier(25), TickMultiplier(50), TickMultiplier(100), TickMultiplier(200)],
+                settings.tick_multiply, 
+                move |tick_multiply| Message::TicksizeSelected(tick_multiply, pane_id)
+            ).placeholder("Ticksize multiplier...").text_size(11).width(iced::Pixels(80.0));
+            
+            let ticksize_tooltip = tooltip(
+                ticksize_picker
+                    .style(style::picklist_primary)
+                    .menu_style(style::picklist_menu_primary),
+                    "Ticksize multiplier",
+                    tooltip::Position::FollowCursor
+                )
+                .style(style::tooltip);
+    
+            row = row.push(ticksize_tooltip);
+        },
+        PaneContent::Candlestick(_) => {
+            let timeframe_picker = pick_list(
+                &Timeframe::ALL[..],
+                settings.selected_timeframe,
+                move |timeframe| Message::TimeframeSelected(timeframe, pane_id),
+            ).placeholder("Choose a timeframe...").text_size(11).width(iced::Pixels(80.0));
+    
+            let tooltip = tooltip(
+                timeframe_picker
+                    .style(style::picklist_primary)
+                    .menu_style(style::picklist_menu_primary),
+                    "Timeframe", 
+                    tooltip::Position::FollowCursor
+                )
+                .style(style::tooltip);
+    
+            row = row.push(tooltip);
+        },
+        PaneContent::Starter => {
+        },
+    }
+
+    let mut buttons = vec![
+        (container(text(char::from(Icon::Cog).to_string()).font(ICON_FONT).size(14)).width(25).center_x(iced::Pixels(25.0)), Message::ShowModal(pane)),
+        (container(text(char::from(icon).to_string()).font(ICON_FONT).size(14)).width(25).center_x(iced::Pixels(25.0)), message),
+    ];
+
+    if total_panes > 1 {
+        buttons.push((container(text(char::from(Icon::Close).to_string()).font(ICON_FONT).size(14)).width(25).center_x(iced::Pixels(25.0)), Message::ClosePane(pane)));
+    }
+
+    for (content, message) in buttons {        
+        row = row.push(
+            button(content)
+                .style(style::button_primary)
+                .padding(3)
+                .on_press(message),
+        );
+    } 
+
+    row.into()
+}
+
+fn view_starter<'a>(
+    pane_id: &'a Uuid,
+    pane_settings: &'a PaneSettings,
+) -> Element<'a, Message> {
+    let content_names = ["Heatmap chart", "Footprint chart", "Candlestick chart", "Time&Sales"];
+    
+    let content_selector = content_names.iter().fold(
+        Column::new()
+            .spacing(6)
+            .align_x(Alignment::Center), |column, &label| {
+                let mut btn = button(label).width(Length::Fill);
+                if let (Some(exchange), Some(ticker)) = (pane_settings.selected_exchange, pane_settings.selected_ticker) {
+                    let timeframe = pane_settings.selected_timeframe.unwrap_or_else(
+                        || { log::error!("No timeframe found"); Timeframe::M1 }
+                    );
+
+                    let pane_stream: Vec<StreamType> = match label {
+                        "Heatmap chart" | "Time&Sales" => vec![
+                            StreamType::DepthAndTrades { exchange, ticker }
+                        ],
+                        "Footprint chart" => vec![
+                            StreamType::DepthAndTrades { exchange, ticker }, 
+                            StreamType::Kline { exchange, ticker, timeframe }
+                        ],
+                        "Candlestick chart" => vec![
+                            StreamType::Kline { exchange, ticker, timeframe }
+                        ],
+                        _ => vec![]
+                    };
+                
+                    btn = btn.on_press(
+                        Message::PaneContentSelected(
+                            label.to_string(),
+                            *pane_id,
+                            pane_stream
+                        )
+                    );
+                }
+                column.push(btn)
+            }
+    );
+
+    let symbol_selector = pick_list(
+        &Ticker::ALL[..],
+        pane_settings.selected_ticker,
+        move |ticker| Message::TickerSelected(ticker, *pane_id),
+    ).placeholder("ticker...").text_size(13).width(Length::Fill);
+
+    let exchange_selector = pick_list(
+        &Exchange::ALL[..],
+        pane_settings.selected_exchange,
+        move |exchange| Message::ExchangeSelected(exchange, *pane_id),
+    ).placeholder("exchange...").text_size(13).width(Length::Fill);
+
+    let picklists = Row::new()
+        .spacing(6)
+        .align_y(Alignment::Center)
+        .push(exchange_selector.style(style::picklist_primary).menu_style(style::picklist_menu_primary))
+        .push(symbol_selector.style(style::picklist_primary).menu_style(style::picklist_menu_primary));
+
+    let column = Column::new()
+        .padding(10)
+        .spacing(10)
+        .align_x(Alignment::Center)
+        .push(picklists)
+        .push(content_selector);
+        
+    let container = Container::new(
+        Column::new()
+            .spacing(10)
+            .padding(20)
+            .align_x(Alignment::Center)
+            .max_width(300)
+            .push(
+                Text::new("Initialize the pane").size(16)
+            )
+            .push(scrollable(column))
+        ).align_x(alignment::Horizontal::Center);
+    
+    container.into()
+}
+
 pub enum PaneContent {
     Heatmap(HeatmapChart),
     Footprint(FootprintChart),