| 24 |
24 |
|
//! `act` does: egui owns focus, which is the rule the crate header states.
|
| 25 |
25 |
|
|
| 26 |
26 |
|
use egui::{Align, Layout, Response, RichText, Sense, Ui, Vec2};
|
| 27 |
|
- |
use makeover_layout::{Act, Awaiting, Figure, Meter, State, Token, Tone};
|
|
27 |
+ |
use makeover_layout::{Act, Awaiting, Bar, Chart, Figure, Meter, State, Token, Tone};
|
| 28 |
28 |
|
use makeover_timing::activity_blink;
|
| 29 |
29 |
|
use std::time::Duration;
|
| 30 |
30 |
|
|
| 45 |
45 |
|
/// a bar in a wide pane are the same description, and the available width is
|
| 46 |
46 |
|
/// the only thing either of them knows.
|
| 47 |
47 |
|
pub meter_width: Option<f32>,
|
|
48 |
+ |
/// How tall a chart stands, in points.
|
|
49 |
+ |
///
|
|
50 |
+ |
/// A chart's own, not [`meter_height`](Self::meter_height): a meter is a
|
|
51 |
+ |
/// rule set into a line of text and a chart is a figure with room of its
|
|
52 |
+ |
/// own. `makeover-webview` defers the same number to `--chart-height` for
|
|
53 |
+ |
/// the same reason, and 200 is the same default.
|
|
54 |
+ |
pub chart_height: f32,
|
| 48 |
55 |
|
/// The corner radius on a meter's trough and on a token.
|
| 49 |
56 |
|
pub radius: u8,
|
| 50 |
57 |
|
/// Inside a token, around its label.
|
| 72 |
79 |
|
Self {
|
| 73 |
80 |
|
meter_height: 6.0,
|
| 74 |
81 |
|
meter_width: None,
|
|
82 |
+ |
chart_height: 200.0,
|
| 75 |
83 |
|
radius: 3,
|
| 76 |
84 |
|
token_padding: Vec2::new(6.0, 2.0),
|
| 77 |
85 |
|
figure_gap: 2.0,
|
| 437 |
445 |
|
.inner
|
| 438 |
446 |
|
}
|
| 439 |
447 |
|
|
|
448 |
+ |
/// A chart, as bars standing on a shared axis.
|
|
449 |
+ |
///
|
|
450 |
+ |
/// The webview's drawing rather than the terminal's: this renderer paints into
|
|
451 |
+ |
/// a rectangle it asks for, so columns cost it nothing and are what a reader
|
|
452 |
+ |
/// expects of a chart. `makeover-tui` lays its bars down instead, because a
|
|
453 |
+ |
/// terminal has rows to spend and cells to draw with; both are honest answers
|
|
454 |
+ |
/// to the same description and neither is the other's fallback.
|
|
455 |
+ |
///
|
|
456 |
+ |
/// # The axis is the caller's height and the description's maximum
|
|
457 |
+ |
///
|
|
458 |
+ |
/// [`WidgetStyle::chart_height`] says how tall the figure stands and
|
|
459 |
+ |
/// [`Chart::most`] says what a full bar means, which is the same split
|
|
460 |
+ |
/// `--chart-height` and `--most` make in the stylesheet. An axis of zero draws
|
|
461 |
+ |
/// its bars at nothing rather than dividing by it.
|
|
462 |
+ |
///
|
|
463 |
+ |
/// [`Bar::note`] is not painted. There is nowhere to put it without a hover
|
|
464 |
+ |
/// surface this crate does not own, and the reading is the fact worth the room.
|
|
465 |
+ |
pub fn chart(
|
|
466 |
+ |
ui: &mut Ui,
|
|
467 |
+ |
chart: &Chart<'_>,
|
|
468 |
+ |
bars: &[Bar<'_>],
|
|
469 |
+ |
palette: &Palette,
|
|
470 |
+ |
style: &WidgetStyle,
|
|
471 |
+ |
) -> Response {
|
|
472 |
+ |
let width = ui.available_width().max(1.0);
|
|
473 |
+ |
let (rect, response) =
|
|
474 |
+ |
ui.allocate_exact_size(Vec2::new(width, style.chart_height), Sense::hover());
|
|
475 |
+ |
|
|
476 |
+ |
// The places on the axis take a band at the bottom, and the bars stand on
|
|
477 |
+ |
// top of it. Reserved out of the figure's own height rather than added to
|
|
478 |
+ |
// it, so `chart_height` is what a caller laying out a screen can measure
|
|
479 |
+ |
// against -- the same promise `--chart-height` makes in the stylesheet.
|
|
480 |
+ |
let font = egui::TextStyle::Small.resolve(ui.style());
|
|
481 |
+ |
let band = ui.text_style_height(&egui::TextStyle::Small);
|
|
482 |
+ |
let floor = (rect.bottom() - band).max(rect.top());
|
|
483 |
+ |
let standing = floor - rect.top();
|
|
484 |
+ |
|
|
485 |
+ |
if bars.is_empty() {
|
|
486 |
+ |
return response;
|
|
487 |
+ |
}
|
|
488 |
+ |
|
|
489 |
+ |
#[expect(
|
|
490 |
+ |
clippy::cast_precision_loss,
|
|
491 |
+ |
reason = "a bar count is small and this is a width in points"
|
|
492 |
+ |
)]
|
|
493 |
+ |
let each = rect.width() / bars.len() as f32;
|
|
494 |
+ |
for (index, bar) in bars.iter().enumerate() {
|
|
495 |
+ |
#[expect(
|
|
496 |
+ |
clippy::cast_precision_loss,
|
|
497 |
+ |
reason = "an index into the bars, which are few"
|
|
498 |
+ |
)]
|
|
499 |
+ |
let left = rect.left() + each * index as f32;
|
|
500 |
+ |
let reached = standing * bar.fraction(chart);
|
|
501 |
+ |
let mut column = egui::Rect::from_min_size(
|
|
502 |
+ |
egui::Pos2::new(left, floor - reached),
|
|
503 |
+ |
Vec2::new(each, reached),
|
|
504 |
+ |
);
|
|
505 |
+ |
// A bar of nothing still says it is there, which is what the
|
|
506 |
+ |
// stylesheet's `min-height` does in the other renderer.
|
|
507 |
+ |
if column.height() < 1.0 {
|
|
508 |
+ |
column.set_top(floor - 1.0);
|
|
509 |
+ |
}
|
|
510 |
+ |
ui.painter()
|
|
511 |
+ |
.rect_filled(column, style.radius, palette.tone(chart.tone));
|
|
512 |
+ |
|
|
513 |
+ |
// Centred under the column it belongs to. Drawn by the painter rather
|
|
514 |
+ |
// than laid out as a row of labels, because a row lays itself out and
|
|
515 |
+ |
// the labels would then sit where the text put them instead of under
|
|
516 |
+ |
// their own bars, which is the one thing a place on an axis has to do.
|
|
517 |
+ |
ui.painter().text(
|
|
518 |
+ |
egui::Pos2::new(left + each / 2.0, floor),
|
|
519 |
+ |
egui::Align2::CENTER_TOP,
|
|
520 |
+ |
bar.at,
|
|
521 |
+ |
font.clone(),
|
|
522 |
+ |
palette.content_muted,
|
|
523 |
+ |
);
|
|
524 |
+ |
}
|
|
525 |
+ |
|
|
526 |
+ |
response
|
|
527 |
+ |
}
|
|
528 |
+ |
|
| 440 |
529 |
|
#[cfg(test)]
|
| 441 |
530 |
|
mod tests {
|
| 442 |
531 |
|
use super::*;
|