main.rs 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037
  1. #![windows_subsystem = "windows"]
  2. mod style;
  3. mod charts;
  4. mod window;
  5. mod layout;
  6. mod logger;
  7. mod screen;
  8. mod widget;
  9. mod tooltip;
  10. mod tickers_table;
  11. mod data_providers;
  12. use tooltip::tooltip;
  13. use tickers_table::TickersTable;
  14. use layout::{SerializableDashboard, Sidebar};
  15. use style::{get_icon_text, Icon, ICON_BYTES};
  16. use screen::{
  17. create_button, dashboard, handle_error, Notification, UserTimezone,
  18. dashboard::{Dashboard, pane},
  19. modal::{confirmation_modal, dashboard_modal}
  20. };
  21. use data_providers::{
  22. binance, bybit, Exchange, MarketType, StreamType, Ticker, TickerInfo, TickerStats, Timeframe
  23. };
  24. use window::{window_events, Window, WindowEvent};
  25. use iced::{
  26. widget::{button, pick_list, Space, column, container, row, text, center, responsive, pane_grid},
  27. padding, Alignment, Element, Length, Point, Size, Subscription, Task, Theme,
  28. };
  29. use iced_futures::MaybeSend;
  30. use futures::TryFutureExt;
  31. use std::{collections::HashMap, vec, future::Future};
  32. fn main() {
  33. logger::setup(false, false).expect("Failed to initialize logger");
  34. std::thread::spawn(|| {
  35. let data_dir_path = std::path::Path::new("data/futures/um/daily/aggTrades");
  36. layout::cleanup_old_data(data_dir_path)
  37. });
  38. let saved_state = layout::load_saved_state("dashboard_state.json");
  39. let window_size = saved_state.window_size.unwrap_or((1600.0, 900.0));
  40. let window_position = saved_state.window_position;
  41. let window_settings = window::Settings {
  42. size: iced::Size::new(window_size.0, window_size.1),
  43. position: {
  44. if let Some(position) = window_position {
  45. iced::window::Position::Specific(Point {
  46. x: position.0,
  47. y: position.1,
  48. })
  49. } else {
  50. iced::window::Position::Centered
  51. }
  52. },
  53. platform_specific: {
  54. #[cfg(target_os = "macos")]
  55. {
  56. iced::window::settings::PlatformSpecific {
  57. title_hidden: true,
  58. titlebar_transparent: true,
  59. fullsize_content_view: true,
  60. }
  61. }
  62. #[cfg(not(target_os = "macos"))]
  63. {
  64. Default::default()
  65. }
  66. },
  67. exit_on_close_request: false,
  68. min_size: Some(iced::Size::new(800.0, 600.0)),
  69. ..Default::default()
  70. };
  71. let _ = iced::daemon("Flowsurface", State::update, State::view)
  72. .settings(iced::Settings {
  73. default_text_size: iced::Pixels(12.0),
  74. antialiasing: true,
  75. ..Default::default()
  76. })
  77. .theme(State::theme)
  78. .subscription(State::subscription)
  79. .font(ICON_BYTES)
  80. .run_with(move || State::new(saved_state, window_settings));
  81. }
  82. #[derive(thiserror::Error, Debug, Clone)]
  83. enum InternalError {
  84. #[error("Fetch error: {0}")]
  85. Fetch(String),
  86. }
  87. #[derive(Debug, Clone, PartialEq)]
  88. enum DashboardModal {
  89. Layout,
  90. Settings,
  91. None,
  92. }
  93. #[derive(Debug, Clone)]
  94. enum Message {
  95. Notification(Notification),
  96. ErrorOccurred(InternalError),
  97. ToggleModal(DashboardModal),
  98. MarketWsEvent(Exchange, data_providers::Event),
  99. ToggleTradeFetch(bool),
  100. WindowEvent(WindowEvent),
  101. SaveAndExit(HashMap<window::Id, (Point, Size)>),
  102. ToggleLayoutLock,
  103. ResetCurrentLayout,
  104. LayoutSelected(layout::LayoutId),
  105. ThemeSelected(Theme),
  106. Dashboard(dashboard::Message),
  107. SetTickersInfo(Exchange, HashMap<Ticker, Option<TickerInfo>>),
  108. SetTimezone(UserTimezone),
  109. SidebarPosition(layout::Sidebar),
  110. TickersTable(tickers_table::Message),
  111. ToggleTickersDashboard,
  112. UpdateTickersTable(Exchange, HashMap<Ticker, TickerStats>),
  113. FetchAndUpdateTickersTable,
  114. LoadLayout(layout::LayoutId),
  115. ToggleDialogModal(Option<String>),
  116. }
  117. struct State {
  118. theme: Theme,
  119. layouts: HashMap<layout::LayoutId, Dashboard>,
  120. last_active_layout: layout::LayoutId,
  121. main_window: Window,
  122. active_modal: DashboardModal,
  123. sidebar_location: Sidebar,
  124. notification: Option<Notification>,
  125. ticker_info_map: HashMap<Exchange, HashMap<Ticker, Option<TickerInfo>>>,
  126. show_tickers_dashboard: bool,
  127. tickers_table: TickersTable,
  128. confirmation_dialog: Option<String>,
  129. layout_locked: bool,
  130. timezone: UserTimezone,
  131. }
  132. #[allow(dead_code)]
  133. impl State {
  134. fn new(
  135. saved_state: layout::SavedState,
  136. window_settings: window::Settings,
  137. ) -> (Self, Task<Message>) {
  138. let (main_window, open_main_window) = window::open(window_settings);
  139. let last_active_layout = saved_state.last_active_layout;
  140. let mut ticker_info_map = HashMap::new();
  141. let exchange_fetch_tasks = {
  142. Exchange::MARKET_TYPES.iter()
  143. .flat_map(|(exchange, market_type)| {
  144. ticker_info_map.insert(*exchange, HashMap::new());
  145. let ticksizes_task = match exchange {
  146. Exchange::BinanceFutures | Exchange::BinanceSpot => {
  147. fetch_ticker_info(*exchange, binance::fetch_ticksize(*market_type))
  148. }
  149. Exchange::BybitLinear | Exchange::BybitSpot => {
  150. fetch_ticker_info(*exchange, bybit::fetch_ticksize(*market_type))
  151. }
  152. };
  153. let prices_task = match exchange {
  154. Exchange::BinanceFutures | Exchange::BinanceSpot => {
  155. fetch_ticker_prices(*exchange, binance::fetch_ticker_prices(*market_type))
  156. }
  157. Exchange::BybitLinear | Exchange::BybitSpot => {
  158. fetch_ticker_prices(*exchange, bybit::fetch_ticker_prices(*market_type))
  159. }
  160. };
  161. vec![ticksizes_task, prices_task]
  162. })
  163. .collect::<Vec<_>>()
  164. };
  165. (
  166. Self {
  167. theme: saved_state.selected_theme.theme,
  168. layouts: saved_state.layouts,
  169. last_active_layout,
  170. main_window: Window::new(main_window),
  171. active_modal: DashboardModal::None,
  172. notification: None,
  173. ticker_info_map,
  174. show_tickers_dashboard: false,
  175. sidebar_location: saved_state.sidebar,
  176. tickers_table: TickersTable::new(saved_state.favorited_tickers),
  177. confirmation_dialog: None,
  178. layout_locked: false,
  179. timezone: saved_state.timezone,
  180. },
  181. open_main_window
  182. .then(|_| Task::none())
  183. .chain(Task::batch(vec![
  184. Task::done(Message::LoadLayout(last_active_layout)),
  185. Task::done(Message::SetTimezone(saved_state.timezone)),
  186. Task::batch(exchange_fetch_tasks),
  187. ])),
  188. )
  189. }
  190. fn update(&mut self, message: Message) -> Task<Message> {
  191. match message {
  192. Message::SetTickersInfo(exchange, tickers_info) => {
  193. log::info!("Received tickers info for {exchange}, len: {}", tickers_info.len());
  194. self.ticker_info_map.insert(exchange, tickers_info);
  195. self.layouts.values_mut().for_each(|dashboard| {
  196. dashboard.set_tickers_info(self.ticker_info_map.clone());
  197. });
  198. }
  199. Message::MarketWsEvent(exchange, event) => {
  200. let main_window_id = self.main_window.id;
  201. let dashboard = self.get_mut_dashboard(self.last_active_layout);
  202. match event {
  203. data_providers::Event::Connected(_) => {
  204. log::info!("a stream connected to {exchange} WS");
  205. }
  206. data_providers::Event::Disconnected(reason) => {
  207. log::info!("a stream disconnected from {exchange} WS: {reason:?}");
  208. }
  209. data_providers::Event::DepthReceived(
  210. ticker,
  211. depth_update_t,
  212. depth,
  213. trades_buffer,
  214. ) => {
  215. return dashboard
  216. .update_depth_and_trades(
  217. &StreamType::DepthAndTrades { exchange, ticker },
  218. depth_update_t,
  219. depth,
  220. trades_buffer,
  221. main_window_id,
  222. )
  223. .map(Message::Dashboard);
  224. }
  225. data_providers::Event::KlineReceived(ticker, kline, timeframe) => {
  226. return dashboard
  227. .update_latest_klines(
  228. &StreamType::Kline {
  229. exchange,
  230. ticker,
  231. timeframe,
  232. },
  233. &kline,
  234. main_window_id,
  235. )
  236. .map(Message::Dashboard);
  237. }
  238. }
  239. }
  240. Message::ToggleLayoutLock => {
  241. self.layout_locked = !self.layout_locked;
  242. self.get_mut_dashboard(self.last_active_layout).focus = None;
  243. }
  244. Message::WindowEvent(event) => match event {
  245. WindowEvent::CloseRequested(window) => {
  246. if window != self.main_window.id {
  247. self.get_mut_dashboard(self.last_active_layout)
  248. .popout
  249. .remove(&window);
  250. return window::close(window);
  251. }
  252. let mut opened_windows: Vec<window::Id> = self
  253. .get_dashboard(self.last_active_layout)
  254. .popout
  255. .keys()
  256. .copied()
  257. .collect::<Vec<_>>();
  258. opened_windows.push(self.main_window.id);
  259. return window::collect_window_specs(
  260. opened_windows,
  261. Message::SaveAndExit
  262. );
  263. }
  264. }
  265. Message::SaveAndExit(windows) => {
  266. self.get_mut_dashboard(self.last_active_layout)
  267. .popout
  268. .iter_mut()
  269. .for_each(|(id, (_, (pos, size)))| {
  270. if let Some((new_pos, new_size)) = windows.get(id) {
  271. *pos = *new_pos;
  272. *size = *new_size;
  273. }
  274. });
  275. let mut layouts = HashMap::new();
  276. for (id, dashboard) in &self.layouts {
  277. let serialized_dashboard = SerializableDashboard::from(dashboard);
  278. layouts.insert(*id, serialized_dashboard);
  279. }
  280. let favorited_tickers = self.tickers_table.get_favorited_tickers();
  281. let size: Option<Size> = windows
  282. .iter()
  283. .find(|(id, _)| **id == self.main_window.id)
  284. .map(|(_, (_, size))| *size);
  285. let position: Option<Point> = windows
  286. .iter()
  287. .find(|(id, _)| **id == self.main_window.id)
  288. .map(|(_, (position, _))| *position);
  289. let layout = layout::SerializableState::from_parts(
  290. layouts,
  291. self.theme.clone(),
  292. favorited_tickers,
  293. self.last_active_layout,
  294. size,
  295. position,
  296. self.timezone,
  297. self.sidebar_location,
  298. );
  299. match serde_json::to_string(&layout) {
  300. Ok(layout_str) => {
  301. if let Err(e) =
  302. layout::write_json_to_file(&layout_str, "dashboard_state.json")
  303. {
  304. log::error!("Failed to write layout state to file: {}", e);
  305. } else {
  306. log::info!("Successfully wrote layout state to dashboard_state.json");
  307. }
  308. }
  309. Err(e) => log::error!("Failed to serialize layout: {}", e),
  310. }
  311. return iced::exit();
  312. }
  313. Message::ToggleModal(modal) => {
  314. if modal == self.active_modal {
  315. self.active_modal = DashboardModal::None;
  316. } else {
  317. self.active_modal = modal;
  318. }
  319. }
  320. Message::Notification(notification) => {
  321. self.notification = Some(notification);
  322. }
  323. Message::ErrorOccurred(err) => {
  324. return match err {
  325. InternalError::Fetch(err) => handle_error(
  326. &err,
  327. "Failed to fetch data",
  328. Message::Notification,
  329. ),
  330. };
  331. }
  332. Message::ThemeSelected(theme) => {
  333. self.theme = theme;
  334. }
  335. Message::ResetCurrentLayout => {
  336. let dashboard = self.get_mut_dashboard(self.last_active_layout);
  337. let active_popout_keys = dashboard.popout.keys().copied().collect::<Vec<_>>();
  338. let window_tasks = Task::batch(
  339. active_popout_keys
  340. .iter()
  341. .map(|&popout_id| window::close(popout_id))
  342. .collect::<Vec<_>>(),
  343. )
  344. .then(|_: Task<window::Id>| Task::none());
  345. return window_tasks.chain(
  346. dashboard
  347. .reset_layout()
  348. .map(Message::Dashboard)
  349. );
  350. }
  351. Message::LayoutSelected(new_layout_id) => {
  352. let active_popout_keys = self
  353. .get_dashboard(self.last_active_layout)
  354. .popout
  355. .keys()
  356. .copied()
  357. .collect::<Vec<_>>();
  358. let window_tasks = Task::batch(
  359. active_popout_keys
  360. .iter()
  361. .map(|&popout_id| window::close(popout_id))
  362. .collect::<Vec<_>>(),
  363. )
  364. .then(|_: Task<window::Id>| Task::none());
  365. return window::collect_window_specs(
  366. active_popout_keys,
  367. dashboard::Message::SavePopoutSpecs,
  368. )
  369. .map(Message::Dashboard)
  370. .chain(window_tasks)
  371. .chain(Task::done(Message::LoadLayout(new_layout_id)));
  372. }
  373. Message::LoadLayout(layout_id) => {
  374. self.last_active_layout = layout_id;
  375. return self
  376. .get_mut_dashboard(layout_id)
  377. .load_layout()
  378. .map(Message::Dashboard);
  379. }
  380. Message::Dashboard(message) => {
  381. if let Some(dashboard) = self.layouts.get_mut(&self.last_active_layout) {
  382. let command = dashboard.update(message, &self.main_window);
  383. return Task::batch(vec![command.map(Message::Dashboard)]);
  384. }
  385. }
  386. Message::ToggleTickersDashboard => {
  387. self.show_tickers_dashboard = !self.show_tickers_dashboard;
  388. }
  389. Message::UpdateTickersTable(exchange, tickers_info) => {
  390. self.tickers_table.update_table(exchange, tickers_info);
  391. }
  392. Message::FetchAndUpdateTickersTable => {
  393. let bybit_linear_fetch = fetch_ticker_prices(
  394. Exchange::BybitLinear,
  395. bybit::fetch_ticker_prices(MarketType::LinearPerps),
  396. );
  397. let binance_linear_fetch = fetch_ticker_prices(
  398. Exchange::BinanceFutures,
  399. binance::fetch_ticker_prices(MarketType::LinearPerps),
  400. );
  401. let binance_spot_fetch = fetch_ticker_prices(
  402. Exchange::BinanceSpot,
  403. binance::fetch_ticker_prices(MarketType::Spot),
  404. );
  405. let bybit_spot_fetch = fetch_ticker_prices(
  406. Exchange::BybitSpot,
  407. bybit::fetch_ticker_prices(MarketType::Spot),
  408. );
  409. return Task::batch(vec![
  410. bybit_linear_fetch,
  411. binance_linear_fetch,
  412. binance_spot_fetch,
  413. bybit_spot_fetch,
  414. ]);
  415. }
  416. Message::TickersTable(message) => {
  417. if let tickers_table::Message::TickerSelected(ticker, exchange, content) = message {
  418. let main_window_id = self.main_window.id;
  419. let command = self
  420. .get_mut_dashboard(self.last_active_layout)
  421. .init_pane_task(main_window_id, ticker, exchange, &content);
  422. return Task::batch(vec![command.map(Message::Dashboard)]);
  423. } else {
  424. let command = self.tickers_table.update(message);
  425. return Task::batch(vec![command.map(Message::TickersTable)]);
  426. }
  427. }
  428. Message::SetTimezone(tz) => {
  429. self.timezone = tz;
  430. }
  431. Message::SidebarPosition(pos) => {
  432. self.sidebar_location = pos;
  433. }
  434. Message::ToggleTradeFetch(checked) => {
  435. self.layouts.values_mut().for_each(|dashboard| {
  436. dashboard.toggle_trade_fetch(checked, &self.main_window);
  437. });
  438. if checked {
  439. self.confirmation_dialog = None;
  440. }
  441. }
  442. Message::ToggleDialogModal(dialog) => {
  443. self.confirmation_dialog = dialog;
  444. }
  445. }
  446. Task::none()
  447. }
  448. fn view(&self, id: window::Id) -> Element<'_, Message> {
  449. let dashboard = self.get_dashboard(self.last_active_layout);
  450. if id != self.main_window.id {
  451. return container(
  452. dashboard
  453. .view_window(
  454. id,
  455. &self.main_window,
  456. self.layout_locked,
  457. &self.timezone,
  458. )
  459. .map(Message::Dashboard)
  460. )
  461. .padding(padding::top(if cfg!(target_os = "macos") { 20 } else { 0 }))
  462. .into();
  463. } else {
  464. let tooltip_position = if self.sidebar_location == Sidebar::Left {
  465. tooltip::Position::Right
  466. } else {
  467. tooltip::Position::Left
  468. };
  469. let sidebar = {
  470. let nav_buttons = {
  471. let layout_lock_button = {
  472. create_button(
  473. get_icon_text(
  474. if self.layout_locked {
  475. Icon::Locked
  476. } else {
  477. Icon::Unlocked
  478. },
  479. 14,
  480. ).width(24).align_x(Alignment::Center),
  481. Message::ToggleLayoutLock,
  482. Some("Layout Lock"),
  483. tooltip_position,
  484. |theme: &Theme, status: button::Status|
  485. style::button_transparent(theme, status, false),
  486. )
  487. };
  488. let settings_modal_button = {
  489. let is_active = matches!(self.active_modal, DashboardModal::Settings);
  490. create_button(
  491. get_icon_text(Icon::Cog, 14)
  492. .width(24)
  493. .align_x(Alignment::Center),
  494. Message::ToggleModal(if is_active {
  495. DashboardModal::None
  496. } else {
  497. DashboardModal::Settings
  498. }),
  499. Some("Settings"),
  500. tooltip_position,
  501. move |theme: &Theme, status: button::Status| {
  502. style::button_transparent(theme, status, is_active)
  503. },
  504. )
  505. };
  506. let layout_modal_button = {
  507. let is_active = matches!(self.active_modal, DashboardModal::Layout);
  508. create_button(
  509. get_icon_text(Icon::Layout, 14)
  510. .width(24)
  511. .align_x(Alignment::Center),
  512. Message::ToggleModal(if is_active {
  513. DashboardModal::None
  514. } else {
  515. DashboardModal::Layout
  516. }),
  517. Some("Manage Layouts"),
  518. tooltip_position,
  519. move |theme: &Theme, status: button::Status| {
  520. style::button_transparent(theme, status, is_active)
  521. },
  522. )
  523. };
  524. let ticker_search_button = {
  525. let is_active = self.show_tickers_dashboard;
  526. create_button(
  527. get_icon_text(Icon::Search, 14)
  528. .width(24)
  529. .align_x(Alignment::Center),
  530. Message::ToggleTickersDashboard,
  531. Some("Search Tickers"),
  532. tooltip_position,
  533. move |theme: &Theme, status: button::Status| {
  534. style::button_transparent(theme, status, is_active)
  535. },
  536. )
  537. };
  538. column![
  539. ticker_search_button,
  540. layout_modal_button,
  541. layout_lock_button,
  542. Space::with_height(Length::Fill),
  543. settings_modal_button,
  544. ]
  545. .width(32)
  546. .spacing(4)
  547. };
  548. let tickers_table = {
  549. if self.show_tickers_dashboard {
  550. column![
  551. responsive(move |size| {
  552. self.tickers_table.view(size).map(Message::TickersTable)
  553. })
  554. ]
  555. .width(200)
  556. } else {
  557. column![]
  558. }
  559. };
  560. match self.sidebar_location {
  561. Sidebar::Left => {
  562. row![
  563. nav_buttons,
  564. tickers_table,
  565. ]
  566. }
  567. Sidebar::Right => {
  568. row![
  569. tickers_table,
  570. nav_buttons,
  571. ]
  572. }
  573. }
  574. .spacing(4)
  575. };
  576. let dashboard_view = dashboard
  577. .view(
  578. &self.main_window,
  579. self.layout_locked,
  580. &self.timezone,
  581. )
  582. .map(Message::Dashboard);
  583. let base = column![
  584. {
  585. #[cfg(target_os = "macos")] {
  586. center(
  587. text("FLOWSURFACE")
  588. .font(
  589. iced::Font {
  590. weight: iced::font::Weight::Bold,
  591. ..Default::default()
  592. }
  593. )
  594. .size(16)
  595. .style(style::branding_text)
  596. .align_x(Alignment::Center)
  597. )
  598. .height(20)
  599. .align_y(Alignment::Center)
  600. .padding(padding::right(8).top(4))
  601. }
  602. #[cfg(not(target_os = "macos"))] {
  603. column![]
  604. }
  605. },
  606. match self.sidebar_location {
  607. Sidebar::Left => row![
  608. sidebar,
  609. dashboard_view,
  610. ],
  611. Sidebar::Right => row![
  612. dashboard_view,
  613. sidebar
  614. ],
  615. }
  616. .spacing(4)
  617. .padding(8),
  618. ];
  619. match self.active_modal {
  620. DashboardModal::Settings => {
  621. let settings_modal = {
  622. let mut all_themes: Vec<Theme> = Theme::ALL.to_vec();
  623. all_themes.push(Theme::Custom(style::custom_theme().into()));
  624. let trade_fetch_checkbox = {
  625. let is_active = dashboard.trade_fetch_enabled;
  626. let checkbox = iced::widget::checkbox("Fetch trades (Binance)", is_active)
  627. .on_toggle(|checked| {
  628. if checked {
  629. Message::ToggleDialogModal(
  630. Some(
  631. "This might be unreliable and take some time to complete"
  632. .to_string()
  633. ),
  634. )
  635. } else {
  636. Message::ToggleTradeFetch(false)
  637. }
  638. }
  639. );
  640. tooltip(
  641. checkbox,
  642. Some("Try to fetch trades for footprint charts"),
  643. tooltip::Position::Top,
  644. )
  645. };
  646. let theme_picklist =
  647. pick_list(all_themes, Some(self.theme.clone()), Message::ThemeSelected);
  648. let timezone_picklist = pick_list(
  649. [UserTimezone::Utc, UserTimezone::Local],
  650. Some(self.timezone),
  651. Message::SetTimezone,
  652. );
  653. let sidebar_pos = pick_list(
  654. [Sidebar::Left, Sidebar::Right],
  655. Some(self.sidebar_location),
  656. Message::SidebarPosition,
  657. );
  658. container(
  659. column![
  660. column![
  661. text("Sidebar").size(14),
  662. sidebar_pos,
  663. ].spacing(4),
  664. column![text("Time zone").size(14), timezone_picklist,].spacing(4),
  665. column![text("Theme").size(14), theme_picklist,].spacing(4),
  666. column![
  667. text("Experimental").size(14),
  668. trade_fetch_checkbox,
  669. ].spacing(4),
  670. ]
  671. .spacing(16),
  672. )
  673. .align_x(Alignment::Start)
  674. .max_width(500)
  675. .padding(24)
  676. .style(style::dashboard_modal)
  677. };
  678. let (align_x, padding) = match self.sidebar_location {
  679. Sidebar::Left => (Alignment::Start, padding::left(48).top(8)),
  680. Sidebar::Right => (Alignment::End, padding::right(48).top(8)),
  681. };
  682. let base_content = dashboard_modal(
  683. base,
  684. settings_modal,
  685. Message::ToggleModal(DashboardModal::None),
  686. padding,
  687. Alignment::End,
  688. align_x,
  689. );
  690. if let Some(confirm_dialog) = &self.confirmation_dialog {
  691. let dialog_content = container(
  692. column![
  693. text(confirm_dialog).size(14),
  694. row![
  695. button(text("Cancel"))
  696. .style(|theme, status| style::button_transparent(theme, status, false))
  697. .on_press(Message::ToggleDialogModal(None)),
  698. button(text("Confirm"))
  699. .on_press(Message::ToggleTradeFetch(true)),
  700. ]
  701. .spacing(8),
  702. ]
  703. .align_x(Alignment::Center)
  704. .spacing(16),
  705. )
  706. .padding(24)
  707. .style(style::dashboard_modal);
  708. confirmation_modal(
  709. base_content,
  710. dialog_content,
  711. Message::ToggleDialogModal(None)
  712. )
  713. } else {
  714. base_content
  715. }
  716. }
  717. DashboardModal::Layout => {
  718. let layout_picklist = pick_list(
  719. &layout::LayoutId::ALL[..],
  720. Some(self.last_active_layout),
  721. move |layout: layout::LayoutId| Message::LayoutSelected(layout),
  722. );
  723. let reset_layout_button = tooltip(
  724. button(text("Reset").align_x(Alignment::Center))
  725. .width(iced::Length::Fill)
  726. .on_press(Message::ResetCurrentLayout),
  727. Some("Reset current layout"),
  728. tooltip::Position::Top,
  729. );
  730. let info_text = tooltip(
  731. button(text("i")).style(move |theme, status| {
  732. style::button_transparent(theme, status, false)
  733. }),
  734. Some("Layouts won't be saved if app exited abruptly"),
  735. tooltip::Position::Top,
  736. );
  737. // Pane management
  738. let reset_pane_button = tooltip(
  739. button(text("Reset").align_x(Alignment::Center))
  740. .width(iced::Length::Fill)
  741. .on_press(Message::Dashboard(dashboard::Message::Pane(
  742. id,
  743. pane::Message::ReplacePane(if let Some(focus) = dashboard.focus {
  744. focus.1
  745. } else {
  746. *dashboard.panes.iter().next().unwrap().0
  747. }),
  748. ))),
  749. Some("Reset selected pane"),
  750. tooltip::Position::Top,
  751. );
  752. let split_pane_button = tooltip(
  753. button(text("Split").align_x(Alignment::Center))
  754. .width(iced::Length::Fill)
  755. .on_press(Message::Dashboard(dashboard::Message::Pane(
  756. id,
  757. pane::Message::SplitPane(
  758. pane_grid::Axis::Horizontal,
  759. if let Some(focus) = dashboard.focus {
  760. focus.1
  761. } else {
  762. *dashboard.panes.iter().next().unwrap().0
  763. },
  764. ),
  765. ))),
  766. Some("Split selected pane horizontally"),
  767. tooltip::Position::Top,
  768. );
  769. let manage_layout_modal = {
  770. container(
  771. column![
  772. column![
  773. text("Panes").size(14),
  774. if dashboard.focus.is_some() {
  775. row![reset_pane_button, split_pane_button,].spacing(8)
  776. } else {
  777. row![text("No pane selected"),]
  778. },
  779. ]
  780. .align_x(Alignment::Center)
  781. .spacing(8),
  782. column![
  783. text("Layouts").size(14),
  784. row![info_text, layout_picklist, reset_layout_button,].spacing(8),
  785. ]
  786. .align_x(Alignment::Center)
  787. .spacing(8),
  788. ]
  789. .align_x(Alignment::Center)
  790. .spacing(32),
  791. )
  792. .width(280)
  793. .padding(24)
  794. .style(style::dashboard_modal)
  795. };
  796. let (align_x, padding) = match self.sidebar_location {
  797. Sidebar::Left => (Alignment::Start, padding::left(48).top(40)),
  798. Sidebar::Right => (Alignment::End, padding::right(48).top(40)),
  799. };
  800. dashboard_modal(
  801. base,
  802. manage_layout_modal,
  803. Message::ToggleModal(DashboardModal::None),
  804. padding,
  805. Alignment::Start,
  806. align_x,
  807. )
  808. }
  809. DashboardModal::None => base.into(),
  810. }
  811. }
  812. }
  813. fn theme(&self, _window: window::Id) -> Theme {
  814. self.theme.clone()
  815. }
  816. fn subscription(&self) -> Subscription<Message> {
  817. let mut market_subscriptions: Vec<Subscription<Message>> = Vec::new();
  818. self.get_dashboard(self.last_active_layout)
  819. .pane_streams
  820. .iter()
  821. .for_each(|(exchange, stream)| {
  822. let mut depth_streams: Vec<Subscription<Message>> = Vec::new();
  823. let mut kline_streams: Vec<(Ticker, Timeframe)> = Vec::new();
  824. let exchange: Exchange = *exchange;
  825. stream
  826. .values()
  827. .flat_map(|stream_types| stream_types.iter())
  828. .for_each(|stream_type| match stream_type {
  829. StreamType::Kline {
  830. ticker, timeframe, ..
  831. } => {
  832. kline_streams.push((*ticker, *timeframe));
  833. }
  834. StreamType::DepthAndTrades { ticker, .. } => {
  835. let ticker: Ticker = *ticker;
  836. let depth_stream = match exchange {
  837. Exchange::BinanceFutures => Subscription::run_with_id(
  838. ticker,
  839. binance::connect_market_stream(ticker),
  840. )
  841. .map(move |event| Message::MarketWsEvent(exchange, event)),
  842. Exchange::BybitLinear => Subscription::run_with_id(
  843. ticker,
  844. bybit::connect_market_stream(ticker),
  845. )
  846. .map(move |event| Message::MarketWsEvent(exchange, event)),
  847. Exchange::BinanceSpot => Subscription::run_with_id(
  848. ticker,
  849. binance::connect_market_stream(ticker),
  850. )
  851. .map(move |event| Message::MarketWsEvent(exchange, event)),
  852. Exchange::BybitSpot => Subscription::run_with_id(
  853. ticker,
  854. bybit::connect_market_stream(ticker),
  855. )
  856. .map(move |event| Message::MarketWsEvent(exchange, event)),
  857. };
  858. depth_streams.push(depth_stream);
  859. }
  860. StreamType::None => {}
  861. });
  862. if !kline_streams.is_empty() {
  863. let kline_streams_id: Vec<(Ticker, Timeframe)> = kline_streams.clone();
  864. let kline_subscription = match exchange {
  865. Exchange::BinanceFutures => Subscription::run_with_id(
  866. kline_streams_id,
  867. binance::connect_kline_stream(kline_streams, MarketType::LinearPerps),
  868. )
  869. .map(move |event| Message::MarketWsEvent(exchange, event)),
  870. Exchange::BybitLinear => Subscription::run_with_id(
  871. kline_streams_id,
  872. bybit::connect_kline_stream(kline_streams, MarketType::LinearPerps),
  873. )
  874. .map(move |event| Message::MarketWsEvent(exchange, event)),
  875. Exchange::BinanceSpot => Subscription::run_with_id(
  876. kline_streams_id,
  877. binance::connect_kline_stream(kline_streams, MarketType::Spot),
  878. )
  879. .map(move |event| Message::MarketWsEvent(exchange, event)),
  880. Exchange::BybitSpot => Subscription::run_with_id(
  881. kline_streams_id,
  882. bybit::connect_kline_stream(kline_streams, MarketType::Spot),
  883. )
  884. .map(move |event| Message::MarketWsEvent(exchange, event)),
  885. };
  886. market_subscriptions.push(kline_subscription);
  887. }
  888. if !depth_streams.is_empty() {
  889. market_subscriptions.push(Subscription::batch(depth_streams));
  890. }
  891. });
  892. let tickers_table_fetch = iced::time::every(std::time::Duration::from_secs(
  893. if self.show_tickers_dashboard { 25 } else { 300 },
  894. ))
  895. .map(|_| Message::FetchAndUpdateTickersTable);
  896. let window_events = window_events().map(Message::WindowEvent);
  897. Subscription::batch(vec![
  898. Subscription::batch(market_subscriptions),
  899. tickers_table_fetch,
  900. window_events,
  901. ])
  902. }
  903. fn get_mut_dashboard(&mut self, layout_id: layout::LayoutId) -> &mut Dashboard {
  904. self.layouts.get_mut(&layout_id).expect("No active layout")
  905. }
  906. fn get_dashboard(&self, layout_id: layout::LayoutId) -> &Dashboard {
  907. self.layouts.get(&layout_id).expect("No active layout")
  908. }
  909. }
  910. fn fetch_ticker_info<F>(exchange: Exchange, fetch_fn: F) -> Task<Message>
  911. where
  912. F: Future<
  913. Output = Result<
  914. HashMap<Ticker, Option<data_providers::TickerInfo>>,
  915. data_providers::StreamError,
  916. >,
  917. > + MaybeSend
  918. + 'static,
  919. {
  920. Task::perform(
  921. fetch_fn.map_err(|err| format!("{err}")),
  922. move |ticksize| match ticksize {
  923. Ok(ticksize) => Message::SetTickersInfo(exchange, ticksize),
  924. Err(err) => Message::ErrorOccurred(InternalError::Fetch(err)),
  925. },
  926. )
  927. }
  928. fn fetch_ticker_prices<F>(exchange: Exchange, fetch_fn: F) -> Task<Message>
  929. where
  930. F: Future<Output = Result<HashMap<Ticker, TickerStats>, data_providers::StreamError>>
  931. + MaybeSend
  932. + 'static,
  933. {
  934. Task::perform(
  935. fetch_fn.map_err(|err| format!("{err}")),
  936. move |tickers_table| match tickers_table {
  937. Ok(tickers_table) => Message::UpdateTickersTable(exchange, tickers_table),
  938. Err(err) => Message::ErrorOccurred(InternalError::Fetch(err)),
  939. },
  940. )
  941. }