Skip to main content

sandbox_quant/ui/
dashboard.rs

1use chrono::TimeZone;
2use ratatui::{
3    buffer::Buffer,
4    layout::{Constraint, Direction, Layout, Rect},
5    style::{Color, Modifier, Style},
6    text::{Line, Span},
7    widgets::{Block, Borders, Paragraph, Widget, Wrap},
8};
9
10use crate::event::{EvSnapshotEntry, ExitPolicyEntry};
11use crate::model::order::OrderSide;
12use crate::model::position::Position;
13use crate::model::signal::Signal;
14use crate::order_manager::OrderUpdate;
15
16pub struct PositionPanel<'a> {
17    position: &'a Position,
18    current_price: Option<f64>,
19    last_applied_fee: &'a str,
20    ev_snapshot: Option<&'a EvSnapshotEntry>,
21    exit_policy: Option<&'a ExitPolicyEntry>,
22}
23
24impl<'a> PositionPanel<'a> {
25    pub fn new(
26        position: &'a Position,
27        current_price: Option<f64>,
28        last_applied_fee: &'a str,
29        ev_snapshot: Option<&'a EvSnapshotEntry>,
30        exit_policy: Option<&'a ExitPolicyEntry>,
31    ) -> Self {
32        Self {
33            position,
34            current_price,
35            last_applied_fee,
36            ev_snapshot,
37            exit_policy,
38        }
39    }
40}
41
42impl Widget for PositionPanel<'_> {
43    fn render(self, area: Rect, buf: &mut Buffer) {
44        let side_str = match self.position.side {
45            Some(OrderSide::Buy) => "LONG",
46            Some(OrderSide::Sell) => "SHORT",
47            None => "FLAT",
48        };
49        let side_color = match self.position.side {
50            Some(OrderSide::Buy) => Color::Green,
51            Some(OrderSide::Sell) => Color::Red,
52            None => Color::DarkGray,
53        };
54
55        let pnl_color = |val: f64| {
56            if val > 0.0 {
57                Color::Green
58            } else if val < 0.0 {
59                Color::Red
60            } else {
61                Color::White
62            }
63        };
64
65        let price_str = self
66            .current_price
67            .map(|p| format!("{:.2}", p))
68            .unwrap_or_else(|| "---".to_string());
69
70        let lines = vec![
71            Line::from(vec![
72                Span::styled("Price:", Style::default().fg(Color::DarkGray)),
73                Span::styled(
74                    format!(" {}", price_str),
75                    Style::default()
76                        .fg(Color::White)
77                        .add_modifier(Modifier::BOLD),
78                ),
79            ]),
80            Line::from(vec![
81                Span::styled("Side: ", Style::default().fg(Color::DarkGray)),
82                Span::styled(
83                    side_str,
84                    Style::default().fg(side_color).add_modifier(Modifier::BOLD),
85                ),
86            ]),
87            Line::from(vec![
88                Span::styled("Qty:  ", Style::default().fg(Color::DarkGray)),
89                Span::styled(
90                    format!("{:.5}", self.position.qty),
91                    Style::default().fg(Color::White),
92                ),
93            ]),
94            Line::from(vec![
95                Span::styled("Entry:", Style::default().fg(Color::DarkGray)),
96                Span::styled(
97                    format!(" {:.2}", self.position.entry_price),
98                    Style::default().fg(Color::White),
99                ),
100            ]),
101            Line::from(vec![
102                Span::styled("UnrPL:", Style::default().fg(Color::DarkGray)),
103                Span::styled(
104                    format!(" {:.4}", self.position.unrealized_pnl),
105                    Style::default().fg(pnl_color(self.position.unrealized_pnl)),
106                ),
107            ]),
108            Line::from(vec![
109                Span::styled("Fee:  ", Style::default().fg(Color::DarkGray)),
110                Span::styled(self.last_applied_fee, Style::default().fg(Color::LightBlue)),
111            ]),
112            Line::from(vec![
113                Span::styled("EV@entry: ", Style::default().fg(Color::DarkGray)),
114                Span::styled(
115                    self.ev_snapshot
116                        .map(|e| format!("{:+.4}", e.ev))
117                        .unwrap_or_else(|| "--".to_string()),
118                    Style::default().fg(self.ev_snapshot.map_or(Color::DarkGray, |e| {
119                        if e.ev > 0.0 {
120                            Color::Green
121                        } else if e.ev < 0.0 {
122                            Color::Red
123                        } else {
124                            Color::White
125                        }
126                    })),
127                ),
128                Span::styled("  pW@entry:", Style::default().fg(Color::DarkGray)),
129                Span::styled(
130                    self.ev_snapshot
131                        .map(|e| format!("{:.2}", e.p_win))
132                        .unwrap_or_else(|| "--".to_string()),
133                    Style::default().fg(Color::Cyan),
134                ),
135            ]),
136            Line::from(vec![
137                Span::styled("Gate: ", Style::default().fg(Color::DarkGray)),
138                Span::styled(
139                    self.ev_snapshot
140                        .map(|e| {
141                            if e.gate_blocked {
142                                format!("{} (BLOCK)", e.gate_mode)
143                            } else {
144                                e.gate_mode.clone()
145                            }
146                        })
147                        .unwrap_or_else(|| "--".to_string()),
148                    Style::default().fg(self.ev_snapshot.map_or(Color::DarkGray, |e| {
149                        if e.gate_blocked {
150                            Color::Red
151                        } else if e.gate_mode.eq_ignore_ascii_case("soft") {
152                            Color::Yellow
153                        } else {
154                            Color::White
155                        }
156                    })),
157                ),
158            ]),
159            Line::from(vec![
160                Span::styled("Stop: ", Style::default().fg(Color::DarkGray)),
161                Span::styled(
162                    self.exit_policy
163                        .and_then(|p| p.stop_price)
164                        .map(|v| format!("{:.2}", v))
165                        .unwrap_or_else(|| "--".to_string()),
166                    Style::default().fg(self.exit_policy.map_or(Color::DarkGray, |p| {
167                        match p.protective_stop_ok {
168                            Some(true) => Color::Green,
169                            Some(false) => Color::Red,
170                            None => Color::Yellow,
171                        }
172                    })),
173                ),
174                Span::styled("  Hold:", Style::default().fg(Color::DarkGray)),
175                Span::styled(
176                    self.exit_policy
177                        .and_then(|p| p.expected_holding_ms)
178                        .map(|v| format!("{}s", v / 1000))
179                        .unwrap_or_else(|| "--".to_string()),
180                    Style::default().fg(Color::White),
181                ),
182            ]),
183        ];
184
185        let block = Block::default()
186            .title(" Position ")
187            .borders(Borders::ALL)
188            .border_style(Style::default().fg(Color::DarkGray));
189
190        Paragraph::new(lines).block(block).render(area, buf);
191    }
192}
193
194pub struct StrategyMetricsPanel<'a> {
195    strategy_label: &'a str,
196    trade_count: u32,
197    win_count: u32,
198    lose_count: u32,
199    realized_pnl: f64,
200}
201
202impl<'a> StrategyMetricsPanel<'a> {
203    pub fn new(
204        strategy_label: &'a str,
205        trade_count: u32,
206        win_count: u32,
207        lose_count: u32,
208        realized_pnl: f64,
209    ) -> Self {
210        Self {
211            strategy_label,
212            trade_count,
213            win_count,
214            lose_count,
215            realized_pnl,
216        }
217    }
218}
219
220impl Widget for StrategyMetricsPanel<'_> {
221    fn render(self, area: Rect, buf: &mut Buffer) {
222        let pnl_color = if self.realized_pnl > 0.0 {
223            Color::Green
224        } else if self.realized_pnl < 0.0 {
225            Color::Red
226        } else {
227            Color::White
228        };
229        let win_rate = if self.trade_count == 0 {
230            0.0
231        } else {
232            (self.win_count as f64 / self.trade_count as f64) * 100.0
233        };
234        let lines = vec![
235            Line::from(vec![
236                Span::styled("Strategy: ", Style::default().fg(Color::DarkGray)),
237                Span::styled(
238                    self.strategy_label,
239                    Style::default()
240                        .fg(Color::Magenta)
241                        .add_modifier(Modifier::BOLD),
242                ),
243            ]),
244            Line::from(vec![
245                Span::styled("Trades: ", Style::default().fg(Color::DarkGray)),
246                Span::styled(
247                    self.trade_count.to_string(),
248                    Style::default().fg(Color::White),
249                ),
250            ]),
251            Line::from(vec![
252                Span::styled("Win: ", Style::default().fg(Color::DarkGray)),
253                Span::styled(
254                    self.win_count.to_string(),
255                    Style::default().fg(Color::Green),
256                ),
257            ]),
258            Line::from(vec![
259                Span::styled("Lose: ", Style::default().fg(Color::DarkGray)),
260                Span::styled(self.lose_count.to_string(), Style::default().fg(Color::Red)),
261            ]),
262            Line::from(vec![
263                Span::styled("WinRate: ", Style::default().fg(Color::DarkGray)),
264                Span::styled(
265                    format!("{:.1}%", win_rate),
266                    Style::default().fg(Color::Cyan),
267                ),
268            ]),
269            Line::from(vec![
270                Span::styled("RlzPL: ", Style::default().fg(Color::DarkGray)),
271                Span::styled(
272                    format!("{:+.4}", self.realized_pnl),
273                    Style::default().fg(pnl_color),
274                ),
275            ]),
276        ];
277
278        let block = Block::default()
279            .title(" Strategy Metrics ")
280            .borders(Borders::ALL)
281            .border_style(Style::default().fg(Color::DarkGray));
282        Paragraph::new(lines).block(block).render(area, buf);
283    }
284}
285
286pub struct OrderLogPanel<'a> {
287    last_signal: &'a Option<Signal>,
288    last_order: &'a Option<OrderUpdate>,
289    fast_sma: Option<f64>,
290    slow_sma: Option<f64>,
291    trade_count: u32,
292    win_count: u32,
293    lose_count: u32,
294    realized_pnl: f64,
295}
296
297impl<'a> OrderLogPanel<'a> {
298    pub fn new(
299        last_signal: &'a Option<Signal>,
300        last_order: &'a Option<OrderUpdate>,
301        fast_sma: Option<f64>,
302        slow_sma: Option<f64>,
303        trade_count: u32,
304        win_count: u32,
305        lose_count: u32,
306        realized_pnl: f64,
307    ) -> Self {
308        Self {
309            last_signal,
310            last_order,
311            fast_sma,
312            slow_sma,
313            trade_count,
314            win_count,
315            lose_count,
316            realized_pnl,
317        }
318    }
319}
320
321impl Widget for OrderLogPanel<'_> {
322    fn render(self, area: Rect, buf: &mut Buffer) {
323        let signal_str = match self.last_signal {
324            Some(Signal::Buy { .. }) => "BUY".to_string(),
325            Some(Signal::Sell { .. }) => "SELL".to_string(),
326            Some(Signal::Hold) | None => "---".to_string(),
327        };
328
329        let order_str = match self.last_order {
330            Some(OrderUpdate::Filled {
331                client_order_id,
332                avg_price,
333                ..
334            }) => format!(
335                "FILLED {} @ {:.2}",
336                &client_order_id[..client_order_id.len().min(12)],
337                avg_price
338            ),
339            Some(OrderUpdate::Submitted {
340                client_order_id, ..
341            }) => format!(
342                "SUBMITTED {}",
343                &client_order_id[..client_order_id.len().min(12)]
344            ),
345            Some(OrderUpdate::Rejected { reason, .. }) => {
346                format!("REJECTED: {}", &reason[..reason.len().min(30)])
347            }
348            None => "---".to_string(),
349        };
350
351        let fast_str = self
352            .fast_sma
353            .map(|v| format!("{:.2}", v))
354            .unwrap_or_else(|| "---".to_string());
355        let slow_str = self
356            .slow_sma
357            .map(|v| format!("{:.2}", v))
358            .unwrap_or_else(|| "---".to_string());
359
360        let lines = vec![
361            Line::from(vec![
362                Span::styled("Signal: ", Style::default().fg(Color::DarkGray)),
363                Span::styled(&signal_str, Style::default().fg(Color::Yellow)),
364            ]),
365            Line::from(vec![
366                Span::styled("Order:  ", Style::default().fg(Color::DarkGray)),
367                Span::styled(&order_str, Style::default().fg(Color::Cyan)),
368            ]),
369            Line::from(vec![
370                Span::styled("Fast SMA: ", Style::default().fg(Color::Green)),
371                Span::styled(&fast_str, Style::default().fg(Color::White)),
372                Span::styled("  Slow SMA: ", Style::default().fg(Color::Yellow)),
373                Span::styled(&slow_str, Style::default().fg(Color::White)),
374            ]),
375            Line::from(vec![
376                Span::styled("Trades: ", Style::default().fg(Color::DarkGray)),
377                Span::styled(
378                    format!("{}", self.trade_count),
379                    Style::default().fg(Color::White),
380                ),
381                Span::styled("  Win: ", Style::default().fg(Color::DarkGray)),
382                Span::styled(
383                    format!("{}", self.win_count),
384                    Style::default().fg(Color::Green),
385                ),
386                Span::styled("  Lose: ", Style::default().fg(Color::DarkGray)),
387                Span::styled(
388                    format!("{}", self.lose_count),
389                    Style::default().fg(Color::Red),
390                ),
391                Span::styled("  PnL: ", Style::default().fg(Color::DarkGray)),
392                Span::styled(
393                    format!("{:.4}", self.realized_pnl),
394                    Style::default().fg(if self.realized_pnl >= 0.0 {
395                        Color::Green
396                    } else {
397                        Color::Red
398                    }),
399                ),
400            ]),
401        ];
402
403        let block = Block::default()
404            .title(" Orders & Signals ")
405            .borders(Borders::ALL)
406            .border_style(Style::default().fg(Color::DarkGray));
407
408        Paragraph::new(lines).block(block).render(area, buf);
409    }
410}
411
412pub struct StatusBar<'a> {
413    pub symbol: &'a str,
414    pub strategy_label: &'a str,
415    pub ws_connected: bool,
416    pub paused: bool,
417    pub timeframe: &'a str,
418    pub last_price_update_ms: Option<u64>,
419    pub last_price_latency_ms: Option<u64>,
420    pub last_order_history_update_ms: Option<u64>,
421    pub last_order_history_latency_ms: Option<u64>,
422    pub close_all_status: Option<&'a str>,
423    pub close_all_running: bool,
424}
425
426impl Widget for StatusBar<'_> {
427    fn render(self, area: Rect, buf: &mut Buffer) {
428        let fmt_update = |ts_ms: Option<u64>| -> String {
429            ts_ms
430                .and_then(|ts| chrono::Utc.timestamp_millis_opt(ts as i64).single())
431                .map(|dt| {
432                    dt.with_timezone(&chrono::Local)
433                        .format("%H:%M:%S")
434                        .to_string()
435                })
436                .unwrap_or_else(|| "--:--:--".to_string())
437        };
438        let fmt_age = |lat_ms: Option<u64>| -> String {
439            lat_ms
440                .map(|v| format!("{}ms", v))
441                .unwrap_or_else(|| "--".to_string())
442        };
443
444        let conn_status = if self.ws_connected {
445            Span::styled("CONNECTED", Style::default().fg(Color::Green))
446        } else {
447            Span::styled(
448                "DISCONNECTED",
449                Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
450            )
451        };
452
453        let pause_status = if self.paused {
454            Span::styled(
455                " STRAT OFF ",
456                Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
457            )
458        } else {
459            Span::styled(" STRAT ON ", Style::default().fg(Color::Green))
460        };
461
462        let mut spans = vec![
463            Span::styled(
464                " sandbox-quant ",
465                Style::default()
466                    .fg(Color::White)
467                    .add_modifier(Modifier::BOLD),
468            ),
469            Span::styled("| ", Style::default().fg(Color::DarkGray)),
470            Span::styled(self.symbol, Style::default().fg(Color::Cyan)),
471            Span::styled(" | ", Style::default().fg(Color::DarkGray)),
472            Span::styled(self.strategy_label, Style::default().fg(Color::Magenta)),
473            Span::styled(" | ", Style::default().fg(Color::DarkGray)),
474            Span::styled(
475                self.timeframe.to_uppercase(),
476                Style::default()
477                    .fg(Color::Yellow)
478                    .add_modifier(Modifier::BOLD),
479            ),
480            Span::styled(" | ", Style::default().fg(Color::DarkGray)),
481            conn_status,
482            Span::styled(" | ", Style::default().fg(Color::DarkGray)),
483            pause_status,
484            Span::styled(" | ", Style::default().fg(Color::DarkGray)),
485            Span::styled(
486                format!(
487                    "updated:{} lat:{}",
488                    fmt_update(self.last_price_update_ms),
489                    fmt_age(self.last_price_latency_ms)
490                ),
491                Style::default().fg(Color::Blue),
492            ),
493            Span::styled(" | ", Style::default().fg(Color::DarkGray)),
494            Span::styled(
495                format!(
496                    "order-updated:{} lat:{}",
497                    fmt_update(self.last_order_history_update_ms),
498                    fmt_age(self.last_order_history_latency_ms)
499                ),
500                Style::default().fg(Color::Cyan),
501            ),
502        ];
503        if let Some(status) = self.close_all_status {
504            spans.push(Span::styled(" | ", Style::default().fg(Color::DarkGray)));
505            spans.push(Span::styled(
506                status,
507                Style::default().fg(if self.close_all_running {
508                    Color::Yellow
509                } else {
510                    Color::LightGreen
511                }),
512            ));
513        }
514        let line = Line::from(spans);
515
516        buf.set_line(area.x, area.y, &line, area.width);
517    }
518}
519
520/// Scrolling order history panel that shows recent order events.
521pub struct OrderHistoryPanel<'a> {
522    open_messages: &'a [String],
523    filled_messages: &'a [String],
524}
525
526impl<'a> OrderHistoryPanel<'a> {
527    pub fn new(open_messages: &'a [String], filled_messages: &'a [String]) -> Self {
528        Self {
529            open_messages,
530            filled_messages,
531        }
532    }
533}
534
535impl Widget for OrderHistoryPanel<'_> {
536    fn render(self, area: Rect, buf: &mut Buffer) {
537        let block = Block::default()
538            .title(" Order History ")
539            .borders(Borders::ALL)
540            .border_style(Style::default().fg(Color::DarkGray));
541        let inner = block.inner(area);
542        block.render(area, buf);
543
544        let cols = Layout::default()
545            .direction(Direction::Horizontal)
546            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
547            .split(inner);
548
549        let render_list = |title: &str, messages: &[String], area: Rect, buf: &mut Buffer| {
550            let sub_block = Block::default()
551                .title(title)
552                .borders(Borders::ALL)
553                .border_style(Style::default().fg(Color::DarkGray));
554            let inner_height = sub_block.inner(area).height as usize;
555            let visible: Vec<Line> = messages
556                .iter()
557                .rev()
558                .take(inner_height)
559                .rev()
560                .map(|msg| {
561                    let color = if msg.contains("REJECTED") {
562                        Color::Red
563                    } else if msg.contains("FILLED") {
564                        Color::Green
565                    } else if msg.contains("SUBMITTED") || msg.contains("PARTIALLY_FILLED") {
566                        Color::Cyan
567                    } else {
568                        Color::DarkGray
569                    };
570                    Line::from(Span::styled(msg.as_str(), Style::default().fg(color)))
571                })
572                .collect();
573
574            Paragraph::new(visible)
575                .block(sub_block)
576                .wrap(Wrap { trim: true })
577                .render(area, buf);
578        };
579
580        render_list(" Open ", self.open_messages, cols[0], buf);
581        render_list(" Filled ", self.filled_messages, cols[1], buf);
582    }
583}
584
585/// Scrolling system log panel that shows recent events.
586pub struct LogPanel<'a> {
587    messages: &'a [String],
588}
589
590impl<'a> LogPanel<'a> {
591    pub fn new(messages: &'a [String]) -> Self {
592        Self { messages }
593    }
594}
595
596impl Widget for LogPanel<'_> {
597    fn render(self, area: Rect, buf: &mut Buffer) {
598        let block = Block::default()
599            .title(" System Log ")
600            .borders(Borders::ALL)
601            .border_style(Style::default().fg(Color::DarkGray));
602        let inner_height = block.inner(area).height as usize;
603
604        // Take the last N messages that fit in the panel
605        let visible: Vec<Line> = self
606            .messages
607            .iter()
608            .rev()
609            .take(inner_height)
610            .rev()
611            .map(|msg| {
612                let (color, text) = if msg.starts_with("[ERR]") {
613                    (Color::Red, msg.as_str())
614                } else if msg.starts_with("[WARN]") {
615                    (Color::Yellow, msg.as_str())
616                } else if msg.contains("FILLED") || msg.contains("Connected") {
617                    (Color::Green, msg.as_str())
618                } else {
619                    (Color::DarkGray, msg.as_str())
620                };
621                Line::from(Span::styled(text, Style::default().fg(color)))
622            })
623            .collect();
624
625        Paragraph::new(visible)
626            .block(block)
627            .wrap(Wrap { trim: true })
628            .render(area, buf);
629    }
630}
631
632pub struct KeybindBar;
633
634impl Widget for KeybindBar {
635    fn render(self, area: Rect, buf: &mut Buffer) {
636        let line = Line::from(vec![
637            Span::styled(" [Q]", Style::default().fg(Color::Yellow)),
638            Span::styled("quit ", Style::default().fg(Color::DarkGray)),
639            Span::styled("[P]", Style::default().fg(Color::Yellow)),
640            Span::styled("/[R] ", Style::default().fg(Color::DarkGray)),
641            Span::styled("pause/resume ", Style::default().fg(Color::DarkGray)),
642            Span::styled("[B]", Style::default().fg(Color::Green)),
643            Span::styled("/[S] ", Style::default().fg(Color::DarkGray)),
644            Span::styled("buy/sell ", Style::default().fg(Color::DarkGray)),
645            Span::styled("[Z] ", Style::default().fg(Color::Red)),
646            Span::styled("close-all ", Style::default().fg(Color::DarkGray)),
647            Span::styled("[G]", Style::default().fg(Color::Magenta)),
648            Span::styled(" grid ", Style::default().fg(Color::DarkGray)),
649            Span::styled("| ", Style::default().fg(Color::DarkGray)),
650            Span::styled("TF:", Style::default().fg(Color::Cyan)),
651            Span::styled(" 0/1/H/D/W/M ", Style::default().fg(Color::DarkGray)),
652            Span::styled("| ", Style::default().fg(Color::DarkGray)),
653            Span::styled("More:", Style::default().fg(Color::Magenta)),
654            Span::styled(" T/Y/A/I ", Style::default().fg(Color::DarkGray)),
655        ]);
656
657        buf.set_line(area.x, area.y, &line, area.width);
658    }
659}
660
661pub struct GridKeybindBar;
662
663impl Widget for GridKeybindBar {
664    fn render(self, area: Rect, buf: &mut Buffer) {
665        let line = Line::from(vec![
666            Span::styled(" [Q]", Style::default().fg(Color::Yellow)),
667            Span::styled("quit ", Style::default().fg(Color::DarkGray)),
668            Span::styled("[Tab]", Style::default().fg(Color::Yellow)),
669            Span::styled(" panel ", Style::default().fg(Color::DarkGray)),
670            Span::styled("[J/K]", Style::default().fg(Color::Yellow)),
671            Span::styled(" select ", Style::default().fg(Color::DarkGray)),
672            Span::styled("[H/L]", Style::default().fg(Color::Yellow)),
673            Span::styled(" symbol ", Style::default().fg(Color::DarkGray)),
674            Span::styled("[O]", Style::default().fg(Color::Yellow)),
675            Span::styled(" toggle ", Style::default().fg(Color::DarkGray)),
676            Span::styled("[N]", Style::default().fg(Color::Yellow)),
677            Span::styled(" new ", Style::default().fg(Color::DarkGray)),
678            Span::styled("[C]", Style::default().fg(Color::Yellow)),
679            Span::styled(" cfg ", Style::default().fg(Color::DarkGray)),
680            Span::styled("[X]", Style::default().fg(Color::Yellow)),
681            Span::styled(" del ", Style::default().fg(Color::DarkGray)),
682            Span::styled("[Enter]", Style::default().fg(Color::Yellow)),
683            Span::styled(" run ", Style::default().fg(Color::DarkGray)),
684            Span::styled("[Esc]", Style::default().fg(Color::Yellow)),
685            Span::styled(" close ", Style::default().fg(Color::DarkGray)),
686            Span::styled("[Z]", Style::default().fg(Color::Red)),
687            Span::styled(" close-all ", Style::default().fg(Color::DarkGray)),
688        ]);
689
690        buf.set_line(area.x, area.y, &line, area.width);
691    }
692}