瀏覽代碼

Persistent layouts (#13)

* preparing for task batching async fetch jobs

* Bumps for Iced's master branch updates

* feat: kline fetches with pane config load

* hide Subscription internals

* chore for deprecated styling methods of Iced

* feat: save of layout on close window request

* feat: persistent dashboard and window settings

* fix for kline fetch on layout config load, +add layout specific inputs

* feat: saveable layouts. lots of dirty code that i hope i won't regret but it works

* feat crosshair toggle on by default

* chores and code cleanup

* feat little tooltip to inform about layout saves

* fix heatmap chart causing panics on NaN position values

* fix charts' x-axis labels causing panic on NaN position values

* removed unused old crates, +version bump
Berke 1 年之前
父節點
當前提交
0d0f48c243

+ 2 - 1
.gitignore

@@ -1,3 +1,4 @@
 .DS_Store
 /target
-/.vscode
+/.vscode
+dashboard_state.json

文件差異過大導致無法顯示
+ 169 - 258
Cargo.lock


+ 2 - 5
Cargo.toml

@@ -1,17 +1,13 @@
 [package]
 name = "iced-trade"
-version = "0.1.0"
+version = "0.4.0"
 edition = "2021"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-plotters-iced = "0.10"
 iced = { git = "https://github.com/iced-rs/iced.git", features = ["canvas", "tokio", "lazy", "image", "advanced"] }
-plotters="0.3"
 chrono = "0.4.37"
-plotters-backend = "0.3.5"
-rand = "0.8.5"
 tokio = { version = "1.37.0", features = ["full", "macros"] }
 tokio-tungstenite = "0.21.0"
 url = "2.5.0"
@@ -39,6 +35,7 @@ hyper-util = { version = "0.1.0", features = ["tokio"] }
 tokio-rustls = "0.24.0"
 webpki-roots = "0.23.0"
 uuid = { version = "1.10.0", features = ["v4"] }
+rustc-hash = "2.0.0"
 [dependencies.async-tungstenite]
 version = "0.25"
 features = ["tokio-rustls-webpki-roots"]

+ 9 - 1
src/charts.rs

@@ -53,7 +53,7 @@ impl Default for CommonChartData {
 
             mesh_cache: Cache::default(),
 
-            crosshair: false,
+            crosshair: true,
             crosshair_cache: Cache::default(),
             crosshair_position: Point::new(0.0, 0.0),
 
@@ -287,6 +287,10 @@ impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
                 while time <= latest_in_millis {                    
                     let x_position = ((time - earliest_in_millis) as f64 / (latest_in_millis - earliest_in_millis) as f64) * bounds.width as f64;
 
+                    if x_position.is_nan() {
+                        break;
+                    }
+
                     if x_position >= 0.0 && x_position <= bounds.width as f64 {
                         let text_size = 12.0;
                         let time_as_datetime = NaiveDateTime::from_timestamp(time / 1000, 0);
@@ -345,6 +349,10 @@ impl canvas::Program<Message> for AxisLabelXCanvas<'_> {
                 };
         
                 let snap_x = snap_ratio * bounds.width as f64;
+
+                if snap_x.is_nan() {
+                    return;
+                }
         
                 let text_size = 12.0;
                 let growth_amount = 6.0;

+ 3 - 3
src/charts/candlestick.rs

@@ -190,7 +190,7 @@ impl CandlestickChart {
         let autoscale_button = button(
             Text::new("A")
                 .size(12)
-                .horizontal_alignment(alignment::Horizontal::Center)
+                .align_x(alignment::Horizontal::Center)
             )
             .width(Length::Fill)
             .height(Length::Fill)
@@ -199,7 +199,7 @@ impl CandlestickChart {
         let crosshair_button = button(
             Text::new("+")
                 .size(12)
-                .horizontal_alignment(alignment::Horizontal::Center)
+                .align_x(alignment::Horizontal::Center)
             ) 
             .width(Length::Fill)
             .height(Length::Fill)
@@ -210,7 +210,7 @@ impl CandlestickChart {
             Row::new()
                 .push(autoscale_button)
                 .push(crosshair_button).spacing(2)
-            ).padding([0, 2, 0, 2])
+            ).padding([0, 2])
             .width(Length::Fixed(60.0))
             .height(Length::Fixed(26.0));
 

+ 3 - 3
src/charts/footprint.rs

@@ -290,7 +290,7 @@ impl FootprintChart {
         let autoscale_button = button(
             Text::new("A")
                 .size(12)
-                .horizontal_alignment(alignment::Horizontal::Center)
+                .align_x(alignment::Horizontal::Center)
             )
             .width(Length::Fill)
             .height(Length::Fill)
@@ -299,7 +299,7 @@ impl FootprintChart {
         let crosshair_button = button(
             Text::new("+")
                 .size(12)
-                .horizontal_alignment(alignment::Horizontal::Center)
+                .align_x(alignment::Horizontal::Center)
             ) 
             .width(Length::Fill)
             .height(Length::Fill)
@@ -310,7 +310,7 @@ impl FootprintChart {
             Row::new()
                 .push(autoscale_button)
                 .push(crosshair_button).spacing(2)
-            ).padding([0, 2, 0, 2])
+            ).padding([0, 2])
             .width(Length::Fixed(60.0))
             .height(Length::Fixed(26.0));
 

+ 7 - 3
src/charts/heatmap.rs

@@ -222,7 +222,7 @@ impl HeatmapChart {
         let autoscale_button = button(
             Text::new("A")
                 .size(12)
-                .horizontal_alignment(alignment::Horizontal::Center)
+                .align_x(alignment::Horizontal::Center)
             )
             .width(Length::Fill)
             .height(Length::Fill)
@@ -231,7 +231,7 @@ impl HeatmapChart {
         let crosshair_button = button(
             Text::new("+")
                 .size(12)
-                .horizontal_alignment(alignment::Horizontal::Center)
+                .align_x(alignment::Horizontal::Center)
             ) 
             .width(Length::Fill)
             .height(Length::Fill)
@@ -242,7 +242,7 @@ impl HeatmapChart {
             Row::new()
                 .push(autoscale_button)
                 .push(crosshair_button).spacing(2)
-            ).padding([0, 2, 0, 2])
+            ).padding([0, 2])
             .width(Length::Fixed(60.0))
             .height(Length::Fixed(26.0));
 
@@ -459,6 +459,10 @@ impl canvas::Program<Message> for HeatmapChart {
                 for (time, (depth, trades)) in self.data_points.range(earliest..=latest) {
                     let x_position = ((time - earliest) as f64 / (latest - earliest) as f64) * bounds.width as f64;
 
+                    if x_position.is_nan() {
+                        continue;
+                    }
+
                     let mut buy_volume: f32 = 0.0;
                     let mut sell_volume: f32 = 0.0;
 

+ 7 - 5
src/data_providers.rs

@@ -1,7 +1,9 @@
+use serde::{Deserialize, Serialize};
+
 pub mod binance;
 pub mod bybit;
 
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
 pub enum StreamType {
     Kline {
         exchange: Exchange,
@@ -155,7 +157,7 @@ pub trait DataProvider {
     fn get_trades(&self, symbol: &str) -> Result<Vec<Trade>, Box<dyn std::error::Error>>;
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
 pub struct TickMultiplier(pub u16);
 
 impl std::fmt::Display for TickMultiplier {
@@ -171,7 +173,7 @@ impl TickMultiplier {
 }
 
 // connection types
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
 pub enum Exchange {
     BinanceFutures,
     BybitLinear,
@@ -207,7 +209,7 @@ impl std::fmt::Display for Ticker {
         )
     }
 }
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
 pub enum Ticker {
     BTCUSDT,
     ETHUSDT,
@@ -233,7 +235,7 @@ impl std::fmt::Display for Timeframe {
         )
     }
 }
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
 pub enum Timeframe {
     M1,
     M3,

+ 7 - 9
src/data_providers/binance/market_data.rs

@@ -1,6 +1,6 @@
 use hyper::client::conn;
-use iced::futures;  
-use iced::subscription::{self, Subscription};
+use iced::{futures, stream};  
+use futures::stream::{Stream, StreamExt};
 use serde::{de, Deserializer};
 use futures::sink::SinkExt;
 
@@ -300,15 +300,14 @@ where
   }
 }
 
-pub fn connect_market_stream(stream: Ticker) -> Subscription<Event> {
-    subscription::channel(
-        stream,
+pub fn connect_market_stream(ticker: Ticker) -> impl Stream<Item = Event> {    
+    stream::channel (
         100,
         move |mut output| async move {
             let mut state = State::Disconnected;     
             let mut trades_buffer: Vec<Trade> = Vec::new(); 
 
-            let selected_ticker = stream;
+            let selected_ticker = ticker;
 
             let symbol_str = match selected_ticker {
                 Ticker::BTCUSDT => "btcusdt",
@@ -514,9 +513,8 @@ pub fn connect_market_stream(stream: Ticker) -> Subscription<Event> {
     )
 }
 
-pub fn connect_kline_stream(streams: Vec<(Ticker, Timeframe)>) -> Subscription<Event> {
-    subscription::channel(
-        streams.clone(),
+pub fn connect_kline_stream(streams: Vec<(Ticker, Timeframe)>) -> impl Stream<Item = Event> {    
+    stream::channel (
         100,
         move |mut output| async move {
             let mut state = State::Disconnected;    

+ 6 - 13
src/data_providers/binance/user_data.rs

@@ -1,12 +1,11 @@
-use iced::futures;  
-use iced::subscription::{self, Subscription};
+use iced::{futures, stream};
+use futures::stream::{Stream, StreamExt};
 use reqwest::header::{HeaderMap, HeaderValue};
 use hmac::{Hmac, Mac};
 use sha2::Sha256;
 use hex;
 use futures::channel::mpsc;
 use futures::sink::SinkExt;
-use futures::stream::StreamExt;
 use chrono::Utc;
 use serde::Deserialize;
 use serde_json::json;
@@ -51,11 +50,8 @@ pub enum Event {
 #[derive(Debug, Clone)]
 pub struct Connection(mpsc::Sender<String>);
 
-pub fn connect_user_stream(listen_key: String) -> Subscription<Event> {
-    struct Connect;
-
-    subscription::channel(
-        std::any::TypeId::of::<Connect>(),
+pub fn connect_user_stream(listen_key: String) -> impl Stream<Item = Event> {
+    stream::channel(
         100,
         |mut output| async move {
             let mut state = State::Disconnected;     
@@ -145,14 +141,11 @@ pub fn connect_user_stream(listen_key: String) -> Subscription<Event> {
     )
 }
 
-pub fn fetch_user_stream(api_key: &str, secret_key: &str) -> Subscription<Event> {
-    struct Connect;
-
+pub fn fetch_user_stream(api_key: &str, secret_key: &str) -> impl Stream<Item = Event> {
     let api_key = api_key.to_owned();
     let secret_key = secret_key.to_owned();
 
-    subscription::channel(
-        std::any::TypeId::of::<Connect>(),
+    stream::channel(
         100,
         move |mut output| {
             tokio::spawn(async move {

+ 7 - 9
src/data_providers/bybit/market_data.rs

@@ -1,7 +1,7 @@
 use hyper::client::conn;
-use iced::futures;  
-use iced::subscription::{self, Subscription};
+use iced::{stream, futures};
 use futures::sink::SinkExt;
+use futures::stream::{Stream, StreamExt};
 
 use serde_json::Value;
 use bytes::Bytes;
@@ -306,16 +306,15 @@ fn string_to_timeframe(interval: &str) -> Option<Timeframe> {
     Timeframe::ALL.iter().find(|&tf| tf.to_string() == format!("{}m", interval)).copied()
 }
 
-pub fn connect_market_stream(stream: Ticker) -> Subscription<Event> {
-    subscription::channel(
-        stream,
+pub fn connect_market_stream(ticker: Ticker) -> impl Stream<Item = Event> {
+    stream::channel (
         100,
         move |mut output| async move {
             let mut state: State = State::Disconnected;  
 
             let mut trades_buffer: Vec<Trade> = Vec::new();    
 
-            let selected_ticker = stream;
+            let selected_ticker = ticker;
 
             let symbol_str = match selected_ticker {
                 Ticker::BTCUSDT => "BTCUSDT",
@@ -455,9 +454,8 @@ pub fn connect_market_stream(stream: Ticker) -> Subscription<Event> {
     )
 }
  
-pub fn connect_kline_stream(streams: Vec<(Ticker, Timeframe)>) -> Subscription<Event> {
-    subscription::channel(
-        streams.clone(),
+pub fn connect_kline_stream(streams: Vec<(Ticker, Timeframe)>) -> impl Stream<Item = Event> {
+    stream::channel (
         100,
         move |mut output| async move {
             let mut state = State::Disconnected;    

文件差異過大導致無法顯示
+ 465 - 159
src/main.rs


+ 222 - 9
src/screen/dashboard.rs

@@ -1,17 +1,17 @@
 pub mod pane;
 
+use pane::SerializablePane;
 pub use pane::{Uuid, PaneState, PaneContent, PaneSettings};
+use serde::{Deserialize, Serialize};
 
 use crate::{
-    charts::{candlestick::CandlestickChart, footprint::FootprintChart, Message}, 
-    data_providers::{
+    charts::{candlestick::CandlestickChart, footprint::FootprintChart, Message}, data_providers::{
         Depth, Exchange, Kline, TickMultiplier, Ticker, Timeframe, Trade
-    }, 
-    StreamType
+    }, StreamType
 };
 
-use std::{collections::{HashMap, HashSet}, rc::Rc};
-use iced::widget::pane_grid::{self, Configuration};
+use std::{collections::{HashMap, HashSet}, io::Read, rc::Rc};
+use iced::{widget::pane_grid::{self, Configuration}, Point, Size};
 
 pub struct Dashboard {
     pub panes: pane_grid::State<PaneState>,
@@ -20,17 +20,92 @@ pub struct Dashboard {
     pub show_layout_modal: bool,
 }
 impl Dashboard {
-    pub fn empty(pane_config: Configuration<PaneState>) -> Self {
-        let panes: pane_grid::State<PaneState> = pane_grid::State::with_configuration(pane_config);
+    pub fn empty() -> Self {
+        let pane_config: Configuration<PaneState> = Configuration::Split {
+            axis: pane_grid::Axis::Vertical,
+            ratio: 0.8,
+            a: Box::new(Configuration::Split {
+                axis: pane_grid::Axis::Horizontal,
+                ratio: 0.4,
+                a: Box::new(Configuration::Split {
+                    axis: pane_grid::Axis::Vertical,
+                    ratio: 0.5,
+                    a: Box::new(Configuration::Pane(
+                        PaneState { 
+                            id: Uuid::new_v4(), 
+                            show_modal: false, 
+                            stream: vec![],
+                            content: PaneContent::Starter,
+                            settings: PaneSettings::default(),
+                        })
+                    ),
+                    b: Box::new(Configuration::Pane(
+                        PaneState { 
+                            id: Uuid::new_v4(), 
+                            show_modal: false, 
+                            stream: vec![],
+                            content: PaneContent::Starter,
+                            settings: PaneSettings::default(),
+                        })
+                    ),
+                }),
+                b: Box::new(Configuration::Split {
+                    axis: pane_grid::Axis::Vertical,
+                    ratio: 0.5,
+                    a: Box::new(Configuration::Pane(
+                        PaneState { 
+                            id: Uuid::new_v4(), 
+                            show_modal: false, 
+                            stream: vec![],
+                            content: PaneContent::Starter,
+                            settings: PaneSettings::default(),
+                        })                      
+                    ),
+                    b: Box::new(Configuration::Pane(
+                        PaneState { 
+                            id: Uuid::new_v4(), 
+                            show_modal: false, 
+                            stream: vec![],
+                            content: PaneContent::Starter,
+                            settings: PaneSettings::default(),
+                        })
+                    ),
+                }),
+            }),
+            b: Box::new(Configuration::Pane(
+                PaneState { 
+                    id: Uuid::new_v4(), 
+                    show_modal: false, 
+                    stream: vec![],
+                    content: PaneContent::Starter,
+                    settings: PaneSettings::default(),
+                })
+            ),
+        };
         
         Self { 
-            panes,
+            panes: pane_grid::State::with_configuration(pane_config),
             focus: None,
             pane_lock: false,
             show_layout_modal: false,
         }
     }
 
+    pub fn from_config(panes: Configuration<PaneState>) -> Self {
+        Self {
+            panes: pane_grid::State::with_configuration(panes),
+            focus: None,
+            pane_lock: false,
+            show_layout_modal: false,
+        }
+    }
+
+    pub 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 update_chart_state(&mut self, pane_id: Uuid, message: Message) -> Result<(), &str> {
         for (_, pane_state) in self.panes.iter_mut() {
             if pane_state.id == pane_id {
@@ -276,4 +351,142 @@ impl Dashboard {
 
         pane_streams
     }
+}
+
+impl Default for Dashboard {
+    fn default() -> Self {
+        Self::empty()
+    }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct SerializableDashboard {
+    pub pane: SerializablePane,
+}
+
+impl<'a> From<&'a Dashboard> for SerializableDashboard {
+    fn from(dashboard: &'a Dashboard) -> Self {
+        use pane_grid::Node;
+
+        fn from_layout(panes: &pane_grid::State<PaneState>, node: pane_grid::Node) -> SerializablePane {
+            match node {
+                Node::Split {
+                    axis, ratio, a, b, ..
+                } => SerializablePane::Split {
+                    axis: match axis {
+                        pane_grid::Axis::Horizontal => pane::Axis::Horizontal,
+                        pane_grid::Axis::Vertical => pane::Axis::Vertical,
+                    },
+                    ratio,
+                    a: Box::new(from_layout(panes, *a)),
+                    b: Box::new(from_layout(panes, *b)),
+                },
+                Node::Pane(pane) => panes
+                    .get(pane)
+                    .map(SerializablePane::from)
+                    .unwrap_or(SerializablePane::Starter),
+            }
+        }
+
+        let layout = dashboard.panes.layout().clone();
+
+        SerializableDashboard {
+            pane: from_layout(&dashboard.panes, layout),
+        }
+    }
+}
+
+impl Default for SerializableDashboard {
+    fn default() -> Self {
+        Self {
+            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)?)
 }

+ 86 - 1
src/screen/dashboard/pane.rs

@@ -1,3 +1,6 @@
+use std::fmt;
+
+use serde::{Deserialize, Serialize};
 pub use uuid::Uuid;
 
 use crate::{
@@ -10,6 +13,7 @@ use crate::{
     StreamType
 };
 
+#[derive(Debug)]
 pub struct PaneState {
     pub id: Uuid,
     pub show_modal: bool,
@@ -29,6 +33,16 @@ impl PaneState {
         }
     }
 
+    pub fn from_config(content: PaneContent, stream: Vec<StreamType>, settings: PaneSettings) -> Self {
+        Self {
+            id: Uuid::new_v4(),
+            show_modal: false,
+            stream,
+            content,
+            settings,
+        }
+    }
+
     pub fn matches_stream(&self, stream_type: &StreamType) -> bool {
         self.stream.iter().any(|stream| stream == stream_type)
     }
@@ -42,7 +56,19 @@ pub enum PaneContent {
     Starter,
 }
 
-#[derive(Debug, Clone, Copy)]
+impl fmt::Debug for PaneContent {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            PaneContent::Heatmap(_) => write!(f, "Heatmap"),
+            PaneContent::Footprint(_) => write!(f, "Footprint"),
+            PaneContent::Candlestick(_) => write!(f, "Candlestick"),
+            PaneContent::TimeAndSales(_) => write!(f, "TimeAndSales"),
+            PaneContent::Starter => write!(f, "Starter"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
 pub struct PaneSettings {
     pub min_tick_size: Option<f32>,
     pub trade_size_filter: Option<f32>,
@@ -62,4 +88,63 @@ impl Default for PaneSettings {
             selected_timeframe: Some(Timeframe::M1),
         }
     }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub enum SerializablePane {
+    Split {
+        axis: Axis,
+        ratio: f32,
+        a: Box<SerializablePane>,
+        b: Box<SerializablePane>,
+    },
+    Starter,
+    HeatmapChart {
+        stream_type: Vec<StreamType>,
+        settings: PaneSettings,
+    },
+    FootprintChart {
+        stream_type: Vec<StreamType>,
+        settings: PaneSettings,
+    },
+    CandlestickChart {
+        stream_type: Vec<StreamType>,
+        settings: PaneSettings,
+    },
+    TimeAndSales {
+        stream_type: Vec<StreamType>,
+        settings: PaneSettings,
+    },
+}
+
+#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
+pub enum Axis {
+    Horizontal,
+    Vertical,
+}
+
+impl From<&PaneState> for SerializablePane {
+    fn from(pane: &PaneState) -> Self {
+        let pane_stream = pane.stream.clone();
+
+        match pane.content {
+            PaneContent::Starter => SerializablePane::Starter,
+            PaneContent::Heatmap(_) => SerializablePane::HeatmapChart {
+                stream_type: pane_stream,
+                settings: pane.settings,
+            },
+            PaneContent::Footprint(_) => SerializablePane::FootprintChart {
+                stream_type: pane_stream,
+                settings: pane.settings,
+            },
+            PaneContent::Candlestick(_) => SerializablePane::CandlestickChart {
+                stream_type: pane_stream,
+                settings: pane.settings,
+            },
+            PaneContent::TimeAndSales(_) => SerializablePane::TimeAndSales {
+                stream_type: pane_stream,
+                settings: pane.settings,
+            }
+        }
+    }
 }

+ 47 - 0
src/style.rs

@@ -107,6 +107,53 @@ pub fn chart_modal(theme: &Theme) -> Style {
     }
 }
 
+pub fn button_for_info(theme: &Theme, status: Status) -> iced::widget::button::Style {
+    let palette = theme.extended_palette();
+
+    match status {
+        Status::Active => iced::widget::button::Style {
+            background: Some(Color::BLACK.into()),
+            text_color: palette.background.base.text,
+            border: Border {
+                radius: 3.0.into(),
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Status::Pressed => iced::widget::button::Style {
+            background: Some(Color::BLACK.into()),
+            text_color: palette.background.base.text,
+            border: Border {
+                color: palette.primary.weak.color,
+                width: 2.0,
+                radius: 6.0.into(),
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Status::Hovered => iced::widget::button::Style {
+            background: Some(Color::BLACK.into()),
+            text_color: palette.background.weak.text,
+            border: Border {
+                color: palette.primary.strong.color,
+                width: 1.0,
+                radius: 3.0.into(),
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Status::Disabled => iced::widget::button::Style {
+            background: Some(Color::BLACK.into()),
+            text_color: palette.background.base.text,
+            border: Border {
+                radius: 3.0.into(),
+                ..Default::default()
+            },
+            ..Default::default()
+        }
+    }
+}
+
 pub fn button_primary(theme: &Theme, status: Status) -> iced::widget::button::Style {
     let palette = theme.extended_palette();
 

部分文件因文件數量過多而無法顯示