浏览代码

Panes test (#1)

* add pane grid layout

* add proper styling to pane grids

* fix more unnecessary styling leftovers

* add layout lock/unlock impl. for pane grid
Berke 1 年之前
父节点
当前提交
f19a10ab2a
共有 3 个文件被更改,包括 384 次插入38 次删除
  1. 72 0
      Cargo.lock
  2. 1 1
      Cargo.toml
  3. 311 37
      src/main.rs

+ 72 - 0
Cargo.lock

@@ -46,6 +46,12 @@ dependencies = [
  "zerocopy",
 ]
 
+[[package]]
+name = "aliasable"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
+
 [[package]]
 name = "allocator-api2"
 version = "0.2.16"
@@ -762,6 +768,12 @@ dependencies = [
  "wio",
 ]
 
+[[package]]
+name = "either"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.33"
@@ -1290,6 +1302,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
 [[package]]
 name = "hermit-abi"
 version = "0.3.9"
@@ -1596,6 +1614,7 @@ dependencies = [
  "iced_runtime",
  "iced_style",
  "num-traits",
+ "ouroboros",
  "thiserror",
  "unicode-segmentation",
 ]
@@ -1678,6 +1697,15 @@ version = "2.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
 
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.11"
@@ -2219,6 +2247,31 @@ dependencies = [
  "libredox 0.0.2",
 ]
 
+[[package]]
+name = "ouroboros"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b7be5a8a3462b752f4be3ff2b2bf2f7f1d00834902e46be2a4d68b87b0573c"
+dependencies = [
+ "aliasable",
+ "ouroboros_macro",
+ "static_assertions",
+]
+
+[[package]]
+name = "ouroboros_macro"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b645dcde5f119c2c454a92d0dfa271a2a3b205da92e4292a68ead4bdbfde1f33"
+dependencies = [
+ "heck",
+ "itertools",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn 2.0.57",
+]
+
 [[package]]
 name = "owned_ttf_parser"
 version = "0.20.0"
@@ -2527,6 +2580,19 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.57",
+ "version_check",
+ "yansi",
+]
+
 [[package]]
 name = "profiling"
 version = "1.0.15"
@@ -4333,6 +4399,12 @@ version = "0.8.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03"
 
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
 [[package]]
 name = "yazi"
 version = "0.1.6"

+ 1 - 1
Cargo.toml

@@ -7,7 +7,7 @@ edition = "2021"
 
 [dependencies]
 plotters-iced = "0.10"
-iced = { version = "0.12.1", features = ["canvas", "tokio"] }
+iced = { version = "0.12.1", features = ["canvas", "tokio", "lazy"] }
 plotters="0.3"
 chrono = "0.4.37"
 plotters-backend = "0.3.5"

+ 311 - 37
src/main.rs

@@ -3,8 +3,12 @@ use std::collections::BTreeMap;
 use chrono::{DateTime, Utc, Duration, TimeZone, LocalResult};
 use iced::{
     executor, widget::{
-        button, canvas::{path::lyon_path::geom::euclid::num::Round, Cache, Frame, Geometry}, pick_list, Column, Container, Row, Text
-    }, Alignment, Application, Command, Element, Font, Length, Settings, Size, Subscription, Theme
+        button, canvas::{path::lyon_path::geom::euclid::num::Round, Cache, Frame, Geometry}, pick_list, Column, Container, Row, Space, Text
+    }, Alignment, Application, Color, Command, Element, Font, Length, Settings, Size, Subscription, Theme
+};
+use iced::widget::pane_grid::{self, PaneGrid};
+use iced::widget::{
+    column, container, row, scrollable, text, responsive
 };
 use futures::TryFutureExt;
 use plotters::prelude::ChartBuilder;
@@ -61,6 +65,20 @@ impl Default for WsState {
     }
 }
 
+#[derive(Clone, Copy)]
+struct Pane {
+    id: usize,
+    pub is_pinned: bool,
+}
+impl Pane {
+    fn new(id: usize) -> Self {
+        Self {
+            id,
+            is_pinned: false,
+        }
+    }
+}
+
 fn main() {
     State::run(Settings {
         antialiasing: true,
@@ -77,6 +95,16 @@ enum Message {
     WsEvent(ws_binance::Event),
     WsToggle(),
     FetchEvent(Result<Vec<ws_binance::Kline>, std::string::String>),
+    Split(pane_grid::Axis, pane_grid::Pane),
+    Clicked(pane_grid::Pane),
+    Dragged(pane_grid::DragEvent),
+    Resized(pane_grid::ResizeEvent),
+    TogglePin(pane_grid::Pane),
+    Maximize(pane_grid::Pane),
+    Restore,
+    Close(pane_grid::Pane),
+    CloseFocused,
+    ToggleLayoutLock,
 }
 
 struct State {
@@ -86,6 +114,11 @@ struct State {
     selected_timeframe: Option<&'static str>,
     ws_state: WsState,
     ws_running: bool,
+    panes: pane_grid::State<Pane>,
+    panes_created: usize,
+    focus: Option<pane_grid::Pane>,
+    first_pane: pane_grid::Pane,
+    pane_lock: bool,
 }
 
 impl Application for State {
@@ -95,6 +128,7 @@ impl Application for State {
     type Theme = Theme;
 
     fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
+        let (panes, first_pane) = pane_grid::State::new(Pane::new(0));
         (
             Self { 
                 trades_chart: None,
@@ -103,13 +137,13 @@ impl Application for State {
                 selected_timeframe: Some("1m"),
                 ws_state: WsState::Disconnected,
                 ws_running: false,
+                panes,
+                panes_created: 1,
+                focus: None,
+                first_pane,
+                pane_lock: false,
             },
-           
-            Command::batch([
-                //Command::perform(tokio::task::spawn_blocking(generate_data), |data| {
-                //    Message::DataLoaded(data.unwrap())
-                //}),
-            ]),
+            Command::none(),
         )
     }
 
@@ -132,19 +166,34 @@ impl Application for State {
                 dbg!(&self.ws_running);
                 if self.ws_running {
                     self.trades_chart = Some(LineChart::new());
-                    Command::perform(
+                    let fetch_klines = Command::perform(
                         ws_binance::fetch_klines(self.selected_ticker.unwrap().to_string(), self.selected_timeframe.unwrap().to_string())
                             .map_err(|err| format!("{}", err)), 
                         |klines| {
                             Message::FetchEvent(klines)
                         }
-                    )
+                    );
+                    if self.panes.len() == 1 {
+                        let first_pane = self.first_pane;
+                        let split_pane = Command::perform(
+                            async move {
+                                tokio::time::sleep(std::time::Duration::from_secs(1)).await;
+                                (pane_grid::Axis::Horizontal, first_pane) 
+                            },
+                            |(axis, pane)| {
+                                Message::Split(axis, pane)
+                            }
+                        );
+                        Command::batch(vec![fetch_klines, split_pane])
+                    } else {
+                        fetch_klines
+                    }
                 } else {
                     self.trades_chart = None;
                     self.candlestick_chart = None;
                     Command::none()
                 }
-            },
+            },       
             Message::FetchEvent(klines) => {
                 match klines {
                     Ok(klines) => {
@@ -187,14 +236,130 @@ impl Application for State {
                     Command::none()
                 }
             }, 
+            Message::Split(axis, pane) => {
+                let result =
+                    self.panes.split(axis, pane, Pane::new(self.panes_created));
+
+                if let Some((pane, _)) = result {
+                    self.focus = Some(pane);
+                }
+
+                self.panes_created += 1;
+                Command::none()
+            }
+            Message::Clicked(pane) => {
+                self.focus = Some(pane);
+                Command::none()
+            }
+            Message::Resized(pane_grid::ResizeEvent { split, ratio }) => {
+                self.panes.resize(split, ratio);
+                Command::none()
+            }
+            Message::Dragged(pane_grid::DragEvent::Dropped {
+                pane,
+                target,
+            }) => {
+                self.panes.drop(pane, target);
+                Command::none()
+            }
+            Message::Dragged(_) => {
+                Command::none()
+            }
+            Message::TogglePin(pane) => {
+                if let Some(Pane { is_pinned, .. }) = self.panes.get_mut(pane) {
+                    *is_pinned = !*is_pinned;
+                }
+                Command::none()
+            }
+            Message::Maximize(pane) => {
+                self.panes.maximize(pane);
+                Command::none()
+            },
+            Message::Restore => {
+                self.panes.restore();
+                Command::none()
+            }
+            Message::Close(pane) => {
+                if let Some((_, sibling)) = self.panes.close(pane) {
+                    self.focus = Some(sibling);
+                }
+                Command::none()
+            }
+            Message::CloseFocused => {
+                if let Some(pane) = self.focus {
+                    if let Some(Pane { is_pinned, .. }) = self.panes.get(pane) {
+                        if !is_pinned {
+                            if let Some((_, sibling)) = self.panes.close(pane) {
+                                self.focus = Some(sibling);
+                            }
+                        }
+                    }
+                }
+                Command::none()
+            }
+            Message::ToggleLayoutLock => {
+                self.focus = None;
+                self.pane_lock = !self.pane_lock;
+                Command::none()
+            }
         }
     }
 
     fn view(&self) -> Element<'_, Self::Message> {
-        let button_text = if self.ws_running { "Disconnect" } else { "Connect" };
-        let ws_button = button(button_text).on_press(Message::WsToggle());
-
-        let mut controls = Row::new()
+        let focus = self.focus;
+        let total_panes = self.panes.len();
+    
+        let pane_grid = PaneGrid::new(&self.panes, |id, pane, is_maximized| {
+            let is_focused = focus == Some(id);
+    
+            let content = pane_grid::Content::new(responsive(move |size| {
+                view_content(id, total_panes, pane.is_pinned, size, pane.id.to_string(), &self.trades_chart, &self.candlestick_chart)
+            }));
+    
+            if self.pane_lock {
+                return content.style(style::pane_active);
+            }
+    
+            let mut content = content.style(if is_focused {
+                style::pane_focused
+            } else {
+                style::pane_active
+            });
+    
+            let title = if pane.id == 0 {
+                "Heatmap"
+            } else {
+                "Candlesticks"
+            };
+    
+            if is_focused {
+                let title_bar = pane_grid::TitleBar::new(title)
+                    .controls(view_controls(
+                        id,
+                        total_panes,
+                        pane.is_pinned,
+                        is_maximized,
+                    ))
+                    .padding(4)
+                    .style(style::title_bar_focused);
+    
+                content = content.title_bar(title_bar);
+            }
+            content
+        })
+        .width(Length::Fill)
+        .height(Length::Fill)
+        .spacing(10)
+        .on_click(Message::Clicked)
+        .on_drag(Message::Dragged)
+        .on_resize(10, Message::Resized);
+
+        let ws_button = button(if self.ws_running { "Disconnect" } else { "Connect" })
+            .on_press(Message::WsToggle());
+        let layout_lock = button(if self.pane_lock { "Unlock Layout" } else { "Lock Layout" })
+            .on_press(Message::ToggleLayoutLock);
+
+        let mut ws_controls = Row::new()
             .spacing(20)
             .align_items(Alignment::Center)
             .push(ws_button);
@@ -213,34 +378,29 @@ impl Application for State {
                     Message::TimeframeSelected,
                 );
             
-                controls = controls.push(timeframe_pick_list)
+                ws_controls = ws_controls.push(timeframe_pick_list)
                     .push(symbol_pick_list);
             } else {
-                controls = controls.push(Text::new(self.selected_ticker.unwrap().to_string()).size(20));
+                ws_controls = ws_controls.push(Text::new(self.selected_ticker.unwrap().to_string()).size(20));
             }
 
-        let trades_chart = match self.trades_chart {
-            Some(ref trades_chart) => trades_chart.view(),
-            None => Text::new("").into(),
-        };
-        let candlestick_chart = match self.candlestick_chart {
-            Some(ref candlestick_chart) => candlestick_chart.view(),
-            None => Text::new("").into(),
-        };
-
         let content = Column::new()
-            .spacing(20)
+            .spacing(10)
             .align_items(Alignment::Start)
             .width(Length::Fill)
             .height(Length::Fill)
-            .push(controls)
-            .push(trades_chart)
-            .push(candlestick_chart);
+            .push(
+                Row::new()
+                    .push(ws_controls)
+                    .push(Space::with_width(Length::Fill))
+                    .push(layout_lock)
+            )
+            .push(pane_grid);
 
         Container::new(content)
             .width(Length::Fill)
             .height(Length::Fill)
-            .padding(20)
+            .padding(10)
             .center_x()
             .center_y()
             .into()
@@ -258,6 +418,121 @@ impl Application for State {
     }
 }
 
+fn view_content<'a>(
+    _pane: pane_grid::Pane,
+    _total_panes: usize,
+    _is_pinned: bool,
+    _size: Size,
+    pane_id: String,
+    trades_chart: &'a Option<LineChart>,
+    candlestick_chart: &'a Option<CandlestickChart>,
+) -> Element<'a, Message> {
+
+    let chart = match pane_id.as_str() {
+        "0" => trades_chart.as_ref().map(LineChart::view).unwrap_or_else(|| Text::new("No data").into()),
+        "1" => candlestick_chart.as_ref().map(CandlestickChart::view).unwrap_or_else(|| Text::new("No data").into()),
+        _ => Text::new("No data").into(),
+    };
+
+    container(chart)
+        .width(Length::Fill)
+        .height(Length::Fill)
+        .into()
+}
+
+fn view_controls<'a>(
+    pane: pane_grid::Pane,
+    total_panes: usize,
+    _is_pinned: bool,
+    is_maximized: bool,
+) -> Element<'a, Message> {
+    let mut row = row![].spacing(5);
+
+    if total_panes > 1 {
+        let buttons = if is_maximized {
+            vec![
+                ("Restore", Message::Restore),
+                //("Split Horizontally", Message::Split(pane_grid::Axis::Horizontal, pane)),
+                //("Split Vertically", Message::Split(pane_grid::Axis::Vertical, pane))
+            ]
+        } else {
+            vec![
+                ("Maximize", Message::Maximize(pane)),
+                //("Split Horizontally", Message::Split(pane_grid::Axis::Horizontal, pane)),
+                //("Split Vertically", Message::Split(pane_grid::Axis::Vertical, pane))
+            ]
+        };
+
+        for (content, message) in buttons {
+            row = row.push(
+                button(text(content).size(14))
+                    .padding(3)
+                    .on_press(message),
+            );
+        }
+    }
+
+    //let close = button(text("Close").size(14))
+    //    .padding(3)
+    //    .on_press_maybe(if total_panes > 1 && !is_pinned {
+    //        Some(Message::Close(pane))
+    //    } else {
+    //        None
+    //    });
+    //row.push(close).into()
+    row.into()
+}
+
+mod style {
+    use iced::widget::container;
+    use iced::{Border, Theme};
+
+    pub fn title_bar_active(theme: &Theme) -> container::Appearance {
+        let palette = theme.extended_palette();
+
+        container::Appearance {
+            text_color: Some(palette.background.strong.text),
+            background: Some(palette.background.strong.color.into()),
+            ..Default::default()
+        }
+    }
+    pub fn title_bar_focused(theme: &Theme) -> container::Appearance {
+        let palette = theme.extended_palette();
+
+        container::Appearance {
+            text_color: Some(palette.primary.strong.text),
+            background: Some(palette.primary.strong.color.into()),
+            ..Default::default()
+        }
+    }
+    pub fn pane_active(theme: &Theme) -> container::Appearance {
+        let palette = theme.extended_palette();
+
+        container::Appearance {
+            //background: Some(palette.background.weak.color.into()),
+            border: Border {
+                width: 2.0,
+                color: palette.background.strong.color,
+                ..Border::default()
+            },
+            ..Default::default()
+        }
+    }
+    pub fn pane_focused(theme: &Theme) -> container::Appearance {
+        let palette = theme.extended_palette();
+
+        container::Appearance {
+            //background: Some(palette.background.weak.color.into()),
+            border: Border {
+                width: 2.0,
+                color: palette.primary.strong.color,
+                ..Border::default()
+            },
+            ..Default::default()
+        }
+    }
+}
+
 struct CandlestickChart {
     cache: Cache,
     data_points: BTreeMap<DateTime<Utc>, (f32, f32, f32, f32)>,
@@ -346,8 +621,8 @@ impl Chart<Message> for CandlestickChart {
         }
 
         let mut chart = chart
-            .x_label_area_size(28)
-            .y_label_area_size(28)
+            .x_label_area_size(20)
+            .y_label_area_size(32)
             .margin(20)
             .build_cartesian_2d(oldest_time..newest_time, y_min..y_max)
             .expect("failed to build chart");
@@ -384,7 +659,6 @@ impl Chart<Message> for CandlestickChart {
         ).expect("failed to draw chart data");
     }
 }
-
 struct LineChart {
     cache: Cache,
     data_points: VecDeque<(DateTime<Utc>, f32, f32, bool)>,
@@ -490,8 +764,8 @@ impl Chart<Message> for LineChart {
             }).collect();
 
             let mut chart = chart
-                .x_label_area_size(28)
-                .y_label_area_size(28)
+                .x_label_area_size(20)
+                .y_label_area_size(32)
                 .margin(20)
                 .build_cartesian_2d(oldest_time..newest_time, y_min..y_max)
                 .expect("failed to build chart");
@@ -554,7 +828,7 @@ impl Chart<Message> for LineChart {
             chart
                 .draw_series(
                     recent_data_points.iter().map(|&(time, price, qty, is_sell)| {
-                        let radius = 1.0 + (qty - qty_min) * (30.0 - 1.0) / (qty_max - qty_min);
+                        let radius = 1.0 + (qty - qty_min) * (35.0 - 1.0) / (qty_max - qty_min);
                         let color = if is_sell { RGBColor(192, 80, 77) } else { RGBColor(81, 205, 160)};
                         Circle::new(
                             (time, price),