Compare commits
2
Commits
f79a97be9b
...
e5b7faadec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5b7faadec | ||
|
|
7d8c453232 |
No files matched your search
+25
-21
@@ -455,9 +455,11 @@ void Host::set_figure(Figure* figure) noexcept {
|
|||||||
row_rects_.clear(); // no figure, no rows: the handlers fall back to `controller_`
|
row_rects_.clear(); // no figure, no rows: the handlers fall back to `controller_`
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const u32 rows = figure_->row_count();
|
// One controller per **plot** (a row of several cells has one for each), indexed by the
|
||||||
row_controllers_.reserve(rows);
|
// flattened plot index the figure uses.
|
||||||
for (u32 i = 0; i < rows; ++i) {
|
const u32 plots = figure_->plot_count();
|
||||||
|
row_controllers_.reserve(plots);
|
||||||
|
for (u32 i = 0; i < plots; ++i) {
|
||||||
row_controllers_.push_back(std::make_unique<input::Controller>(figure_->plot(i)));
|
row_controllers_.push_back(std::make_unique<input::Controller>(figure_->plot(i)));
|
||||||
}
|
}
|
||||||
// The new figure's rects go into the cache now, not at the next frame: an event
|
// The new figure's rects go into the cache now, not at the next frame: an event
|
||||||
@@ -469,8 +471,10 @@ void Host::set_figure(Figure* figure) noexcept {
|
|||||||
|
|
||||||
void Host::refresh_row_rects() noexcept {
|
void Host::refresh_row_rects() noexcept {
|
||||||
// The one `Figure::layout()` per call: every event handler reads `row_rects_`
|
// The one `Figure::layout()` per call: every event handler reads `row_rects_`
|
||||||
// instead of paying this again, which is the whole point of the cache (R7).
|
// instead of paying this again, which is the whole point of the cache (R7). The rects
|
||||||
row_rects_ = figure_ != nullptr ? figure_->layout().rows : std::vector<Rect>{};
|
// are the figure's **cells** (one per plot, flattened), which for a figure with one
|
||||||
|
// cell per row are exactly its row rects.
|
||||||
|
row_rects_ = figure_ != nullptr ? figure_->layout().cells : std::vector<Rect>{};
|
||||||
}
|
}
|
||||||
|
|
||||||
input::Controller* Host::row_controller(u32 row) noexcept {
|
input::Controller* Host::row_controller(u32 row) noexcept {
|
||||||
@@ -486,8 +490,7 @@ input::Controller& Host::controller_at(f32 x, f32 y) noexcept {
|
|||||||
// frame stale, which is the documented worst case.
|
// frame stale, which is the documented worst case.
|
||||||
const std::vector<Rect>& rows = row_rects_;
|
const std::vector<Rect>& rows = row_rects_;
|
||||||
if (rows.empty() || rows.size() != row_controllers_.size()) return controller_;
|
if (rows.empty() || rows.size() != row_controllers_.size()) return controller_;
|
||||||
(void)x; // the rows tile the figure vertically, so y alone picks one -- see row_in()
|
const u32 row = row_in(rows, x, y);
|
||||||
const u32 row = row_in(rows, y);
|
|
||||||
last_row_ = row;
|
last_row_ = row;
|
||||||
input::Controller& controller = *row_controllers_[row];
|
input::Controller& controller = *row_controllers_[row];
|
||||||
// The row's own viewport, so the geometry this controller hit-tests is the geometry
|
// The row's own viewport, so the geometry this controller hit-tests is the geometry
|
||||||
@@ -506,7 +509,6 @@ void Host::feed_pointer_event(const input::PointerEvent& e) noexcept {
|
|||||||
|
|
||||||
input::Controller& Host::pointer_target(f32 x, f32 y, input::Button button, bool pressed,
|
input::Controller& Host::pointer_target(f32 x, f32 y, input::Button button, bool pressed,
|
||||||
bool released) noexcept {
|
bool released) noexcept {
|
||||||
(void)x; // the rows tile the figure vertically, so y alone picks one -- see row_in()
|
|
||||||
if (figure_ == nullptr || row_controllers_.empty()) return controller_;
|
if (figure_ == nullptr || row_controllers_.empty()) return controller_;
|
||||||
const std::vector<Rect>& rows = row_rects_;
|
const std::vector<Rect>& rows = row_rects_;
|
||||||
if (rows.empty() || rows.size() != row_controllers_.size()) return controller_;
|
if (rows.empty() || rows.size() != row_controllers_.size()) return controller_;
|
||||||
@@ -523,7 +525,7 @@ input::Controller& Host::pointer_target(f32 x, f32 y, input::Button button, bool
|
|||||||
// drag, and its press must not steal the capture to the row it landed in.
|
// drag, and its press must not steal the capture to the row it landed in.
|
||||||
if (released && button == captured_button_) captured_ = false;
|
if (released && button == captured_button_) captured_ = false;
|
||||||
} else {
|
} else {
|
||||||
row = row_in(rows, y);
|
row = row_in(rows, x, y);
|
||||||
if (pressed) {
|
if (pressed) {
|
||||||
captured_ = true;
|
captured_ = true;
|
||||||
captured_row_ = row;
|
captured_row_ = row;
|
||||||
@@ -541,27 +543,29 @@ input::Controller& Host::pointer_target(f32 x, f32 y, input::Button button, bool
|
|||||||
|
|
||||||
u32 Host::pointer_row(const std::vector<Rect>& rows) noexcept {
|
u32 Host::pointer_row(const std::vector<Rect>& rows) noexcept {
|
||||||
if (captured_ && captured_row_ < rows.size()) return captured_row_;
|
if (captured_ && captured_row_ < rows.size()) return captured_row_;
|
||||||
return row_in(rows, last_pointer_y_);
|
return row_in(rows, last_pointer_x_, last_pointer_y_);
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 Host::row_in(const std::vector<Rect>& rows, f32 y) noexcept {
|
u32 Host::row_in(const std::vector<Rect>& rows, f32 x, f32 y) noexcept {
|
||||||
if (rows.empty()) return 0u;
|
if (rows.empty()) return 0u;
|
||||||
// The row the pixel is in, or -- when it is in none of them, which is the row gap a
|
// The cell the pixel is in, or -- when it is in none of them, which is the row gap or
|
||||||
// host has asked for with `Figure::set_row_gap()` or the figure's own margins -- the
|
// column gap a host has asked for with `Figure::set_row_gap()`/`set_col_gap()` or the
|
||||||
// row whose rect is nearest. Selection by distance rather than by "the first that
|
// figure's own margins -- the cell whose rect is nearest (squared distance from the
|
||||||
// matches" is what makes a pixel in a gap belong to the row it is *near*: the rows
|
// pixel to the rect). Selection by distance rather than by "the first that matches" is
|
||||||
// tile the figure at the default gap of 0, so this is unreachable until a host asks
|
// what makes a pixel in a gap belong to the cell it is *near*, and a gesture in a
|
||||||
// for one, and a gesture in a visible gap must not be routed to row 0 by default.
|
// visible gap must not be routed to plot 0 by default. For a figure with one cell per
|
||||||
|
// row every cell spans the figure's width, so this reduces to the nearest row by y.
|
||||||
u32 row = 0u;
|
u32 row = 0u;
|
||||||
f32 best = std::numeric_limits<f32>::infinity();
|
f32 best = std::numeric_limits<f32>::infinity();
|
||||||
for (u32 i = 0; i < rows.size(); ++i) {
|
for (u32 i = 0; i < rows.size(); ++i) {
|
||||||
const f32 top = rows[i].y;
|
const Rect& r = rows[i];
|
||||||
const f32 bottom = rows[i].y + rows[i].h;
|
const f32 dx = x < r.x ? r.x - x : (x >= r.right() ? x - r.right() : 0.0f);
|
||||||
const f32 distance = y < top ? top - y : (y >= bottom ? y - bottom : 0.0f);
|
const f32 dy = y < r.y ? r.y - y : (y >= r.bottom() ? y - r.bottom() : 0.0f);
|
||||||
|
const f32 distance = dx * dx + dy * dy;
|
||||||
if (distance < best) {
|
if (distance < best) {
|
||||||
best = distance;
|
best = distance;
|
||||||
row = i;
|
row = i;
|
||||||
if (distance == 0.0f) break; // inside this row: nothing can be nearer
|
if (distance == 0.0f) break; // inside this cell: nothing can be nearer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return row;
|
return row;
|
||||||
|
|||||||
+19
-15
@@ -121,18 +121,21 @@ class Host {
|
|||||||
/// `plot_` is then untouched by any of this.
|
/// `plot_` is then untouched by any of this.
|
||||||
void set_figure(Figure* figure) noexcept;
|
void set_figure(Figure* figure) noexcept;
|
||||||
Figure* figure() noexcept { return figure_; }
|
Figure* figure() noexcept { return figure_; }
|
||||||
/// One figure row's controller -- the one a gesture on that row reaches -- or
|
/// One figure plot's controller -- the one a gesture on that plot reaches -- or null
|
||||||
/// null while no figure is set or the row does not exist. A host reads a drag
|
/// while no figure is set or it does not exist. `row` is the **flattened plot index**
|
||||||
|
/// (`Figure::plot_count()` of them: the row count when no row holds several cells). A host reads a drag
|
||||||
/// mode or a hover out of it (`controller()` is the single-plot loop's, and is
|
/// mode or a hover out of it (`controller()` is the single-plot loop's, and is
|
||||||
/// not the one figure gestures go to).
|
/// not the one figure gestures go to).
|
||||||
input::Controller* row_controller(u32 row) noexcept;
|
input::Controller* row_controller(u32 row) noexcept;
|
||||||
/// The controller an event at framebuffer pixel (x, y) belongs to: `controller_` --
|
/// The controller an event at framebuffer pixel (x, y) belongs to: `controller_` --
|
||||||
/// the single-plot loop -- while no figure is set, and the controller of the figure
|
/// the single-plot loop -- while no figure is set, and the controller of the figure
|
||||||
/// row that pixel lands in otherwise. The row rects come from the cache
|
/// plot (cell) that pixel lands in otherwise -- x and y both pick it, as a row may hold
|
||||||
|
/// several cells side by side. The cell rects come from the cache
|
||||||
/// `refresh_row_rects()` fills -- the same ones the renderer draws -- so a gesture
|
/// `refresh_row_rects()` fills -- the same ones the renderer draws -- so a gesture
|
||||||
/// lands on the row it was aimed at and no event lays the figure out; a pixel
|
/// lands on the row it was aimed at and no event lays the figure out; a pixel
|
||||||
/// inside no row -- the row gap a host asked for with `Figure::set_row_gap()`,
|
/// inside no cell -- the row or column gap a host asked for with
|
||||||
/// or a margin -- is the *nearest* row rather than dropped or defaulted, so a drag
|
/// `Figure::set_row_gap()`/`set_col_gap()`, or a margin -- is the *nearest* cell rather than
|
||||||
|
/// dropped or defaulted, so a drag
|
||||||
/// that leaves the data area still ends where it should.
|
/// that leaves the data area still ends where it should.
|
||||||
///
|
///
|
||||||
/// It also hands that row its own viewport, which is what keeps the controller's
|
/// It also hands that row its own viewport, which is what keeps the controller's
|
||||||
@@ -217,11 +220,12 @@ class Host {
|
|||||||
void on_scroll(f64 dx, f64 dy) noexcept;
|
void on_scroll(f64 dx, f64 dy) noexcept;
|
||||||
void on_key(int key, int action, int mods) noexcept;
|
void on_key(int key, int action, int mods) noexcept;
|
||||||
|
|
||||||
/// The index of the row of `rows` a framebuffer y belongs to: the row it is in, or the nearest
|
/// The index of the cell of `rows` (the figure's flattened cell rects) a framebuffer pixel
|
||||||
/// row when it is in none (a row gap, a margin outside the figure). One rule, shared by the event
|
/// belongs to: the cell it is in, or the nearest cell when it is in none (a row or column gap,
|
||||||
/// routing and the crosshair sync -- and taking the rects rather than reading them, so a caller
|
/// a margin outside the figure). One rule, shared by the event routing and the crosshair sync
|
||||||
/// that has just laid the figure out does not make it lay out again for this.
|
/// -- and taking the rects rather than reading them, so a caller that has just laid the figure
|
||||||
static u32 row_in(const std::vector<Rect>& rows, f32 y) noexcept;
|
/// out does not make it lay out again for this.
|
||||||
|
static u32 row_in(const std::vector<Rect>& rows, f32 x, f32 y) noexcept;
|
||||||
|
|
||||||
/// The controller a pointer event at (x, y) belongs to, applying the button capture:
|
/// The controller a pointer event at (x, y) belongs to, applying the button capture:
|
||||||
/// a press claims the row it landed in (`captured_row_` + `captured_button_`) and
|
/// a press claims the row it landed in (`captured_row_` + `captured_button_`) and
|
||||||
@@ -231,10 +235,10 @@ class Host {
|
|||||||
input::Controller& pointer_target(f32 x, f32 y, input::Button button, bool pressed,
|
input::Controller& pointer_target(f32 x, f32 y, input::Button button, bool pressed,
|
||||||
bool released) noexcept;
|
bool released) noexcept;
|
||||||
|
|
||||||
/// The row the pointer's events currently go to: the captured row while a drag owns
|
/// The plot (flattened index) the pointer's events currently go to: the captured one
|
||||||
/// the button, the row under the pointer otherwise. The run loop uses it to clear
|
/// while a drag owns the button, the one under the pointer otherwise. The run loop uses
|
||||||
/// every *other* row's hot handles, so a row the pointer left cannot keep drawing a
|
/// it to clear every *other* plot's hot handles, so one the pointer left cannot keep
|
||||||
/// hover or a grab it last published.
|
/// drawing a hover or a grab it last published.
|
||||||
u32 pointer_row(const std::vector<Rect>& rows) noexcept;
|
u32 pointer_row(const std::vector<Rect>& rows) noexcept;
|
||||||
|
|
||||||
/// Spreads the hover over a whole `Figure` (spec §15).
|
/// Spreads the hover over a whole `Figure` (spec §15).
|
||||||
@@ -292,7 +296,7 @@ class Host {
|
|||||||
u32 captured_row_ = 0u;
|
u32 captured_row_ = 0u;
|
||||||
input::Button captured_button_ = input::Button::Left;
|
input::Button captured_button_ = input::Button::Left;
|
||||||
|
|
||||||
/// The figure's row rects as of the last `refresh_row_rects()` -- the cache the
|
/// The figure's cell rects (one per plot, flattened; the row rects when no row is split) as of the last `refresh_row_rects()` -- the cache the
|
||||||
/// event routing reads instead of laying the figure out per event (R7). Filled by
|
/// event routing reads instead of laying the figure out per event (R7). Filled by
|
||||||
/// `set_figure()` and once per frame by the run loop; empty while no figure is
|
/// `set_figure()` and once per frame by the run loop; empty while no figure is
|
||||||
/// set, which is the state `controller_at()`/`pointer_target()` fall back to the
|
/// set, which is the state `controller_at()`/`pointer_target()` fall back to the
|
||||||
|
|||||||
@@ -245,9 +245,11 @@ void PlotArea::set_figure(Figure* figure) noexcept {
|
|||||||
row_rects_.clear(); // no figure, no rows: the routing falls back to `controller_`
|
row_rects_.clear(); // no figure, no rows: the routing falls back to `controller_`
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const u32 rows = figure_->row_count();
|
// One controller per **plot** (a row of several cells has one for each), indexed by the
|
||||||
row_controllers_.reserve(rows);
|
// flattened plot index the figure uses.
|
||||||
for (u32 i = 0; i < rows; ++i) {
|
const u32 plots = figure_->plot_count();
|
||||||
|
row_controllers_.reserve(plots);
|
||||||
|
for (u32 i = 0; i < plots; ++i) {
|
||||||
row_controllers_.push_back(std::make_unique<input::Controller>(figure_->plot(i)));
|
row_controllers_.push_back(std::make_unique<input::Controller>(figure_->plot(i)));
|
||||||
}
|
}
|
||||||
// The new figure's geometry goes into the cache now, not at the next frame: an event
|
// The new figure's geometry goes into the cache now, not at the next frame: an event
|
||||||
@@ -269,8 +271,10 @@ void PlotArea::lay_figure_out(Rect px) noexcept {
|
|||||||
|
|
||||||
void PlotArea::refresh_row_rects() noexcept {
|
void PlotArea::refresh_row_rects() noexcept {
|
||||||
// The one `Figure::layout()` per call: every event handler reads `row_rects_`
|
// The one `Figure::layout()` per call: every event handler reads `row_rects_`
|
||||||
// instead of paying this again, which is the whole point of the cache.
|
// instead of paying this again, which is the whole point of the cache. The rects are
|
||||||
row_rects_ = figure_ != nullptr ? figure_->layout().rows : std::vector<Rect>{};
|
// the figure's **cells** (one per plot, flattened), which for a figure with one cell
|
||||||
|
// per row are exactly its row rects.
|
||||||
|
row_rects_ = figure_ != nullptr ? figure_->layout().cells : std::vector<Rect>{};
|
||||||
}
|
}
|
||||||
|
|
||||||
input::Controller* PlotArea::row_controller(u32 row) noexcept {
|
input::Controller* PlotArea::row_controller(u32 row) noexcept {
|
||||||
@@ -283,8 +287,7 @@ input::Controller& PlotArea::controller_at(f32 x, f32 y) noexcept {
|
|||||||
// -- and not a fresh `Figure::layout()`: an event pays no layout, or a burst of
|
// -- and not a fresh `Figure::layout()`: an event pays no layout, or a burst of
|
||||||
// pointer motion over a multi-row figure would re-lay every row out per move.
|
// pointer motion over a multi-row figure would re-lay every row out per move.
|
||||||
if (row_rects_.empty() || row_rects_.size() != row_controllers_.size()) return controller_;
|
if (row_rects_.empty() || row_rects_.size() != row_controllers_.size()) return controller_;
|
||||||
(void)x; // the rows tile the figure vertically, so y alone picks one -- see row_in()
|
const u32 row = row_in(row_rects_, x, y);
|
||||||
const u32 row = row_in(row_rects_, y);
|
|
||||||
input::Controller& controller = *row_controllers_[row];
|
input::Controller& controller = *row_controllers_[row];
|
||||||
controller.set_viewport(row_rects_[row]);
|
controller.set_viewport(row_rects_[row]);
|
||||||
return controller;
|
return controller;
|
||||||
@@ -292,11 +295,11 @@ input::Controller& PlotArea::controller_at(f32 x, f32 y) noexcept {
|
|||||||
|
|
||||||
u32 PlotArea::row_at(double x, double y) const noexcept {
|
u32 PlotArea::row_at(double x, double y) const noexcept {
|
||||||
if (figure_ == nullptr || row_rects_.empty()) return 0;
|
if (figure_ == nullptr || row_rects_.empty()) return 0;
|
||||||
(void)x;
|
const Vec2 p = to_device(x, y);
|
||||||
return row_in(row_rects_, to_device(x, y).y);
|
return row_in(row_rects_, p.x, p.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
input::Controller& PlotArea::target_controller(f32 y) noexcept {
|
input::Controller& PlotArea::target_controller(f32 x, f32 y) noexcept {
|
||||||
if (figure_ == nullptr || row_controllers_.empty()) return controller_;
|
if (figure_ == nullptr || row_controllers_.empty()) return controller_;
|
||||||
if (row_rects_.empty() || row_rects_.size() != row_controllers_.size()) return controller_;
|
if (row_rects_.empty() || row_rects_.size() != row_controllers_.size()) return controller_;
|
||||||
// The capture: while a press is open the gesture belongs to the row that press
|
// The capture: while a press is open the gesture belongs to the row that press
|
||||||
@@ -304,7 +307,7 @@ input::Controller& PlotArea::target_controller(f32 y) noexcept {
|
|||||||
// the release to the row the pointer is over *now* would leave the first row's drag
|
// the release to the row the pointer is over *now* would leave the first row's drag
|
||||||
// open for ever -- its controller never sees the release that ends it -- and let a
|
// open for ever -- its controller never sees the release that ends it -- and let a
|
||||||
// later gesture resume it from the stale anchor.
|
// later gesture resume it from the stale anchor.
|
||||||
u32 row = press_open_ ? press_row_ : row_in(row_rects_, y);
|
u32 row = press_open_ ? press_row_ : row_in(row_rects_, x, y);
|
||||||
// Re-read against this frame's rects: the widget can be resized mid-drag, and the
|
// Re-read against this frame's rects: the widget can be resized mid-drag, and the
|
||||||
// guard keeps a stale index from reaching past the controller list.
|
// guard keeps a stale index from reaching past the controller list.
|
||||||
if (row >= row_controllers_.size()) row = 0;
|
if (row >= row_controllers_.size()) row = 0;
|
||||||
@@ -314,23 +317,25 @@ input::Controller& PlotArea::target_controller(f32 y) noexcept {
|
|||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 PlotArea::row_in(const std::vector<Rect>& rows, f32 y) noexcept {
|
u32 PlotArea::row_in(const std::vector<Rect>& rows, f32 x, f32 y) noexcept {
|
||||||
if (rows.empty()) return 0;
|
if (rows.empty()) return 0;
|
||||||
// The row the pixel is in, or -- when it is in none of them, which is the row gap a
|
// The cell the pixel is in, or -- when it is in none of them, which is the row or
|
||||||
// host asked for with `Figure::set_row_gap()` or the figure's own margins -- the row
|
// column gap a host asked for with `Figure::set_row_gap()`/`set_col_gap()` or the
|
||||||
// whose rect is nearest. By distance rather than by "the first that matches": the
|
// figure's own margins -- the cell whose rect is nearest (squared distance from the
|
||||||
// rows tile the figure at the default gap of 0, so this is unreachable until a host
|
// pixel to the rect). By distance rather than by "the first that matches": a gesture
|
||||||
// asks for a gap, and a gesture in a visible gap must not default to row 0.
|
// in a visible gap must not default to plot 0. For a figure with one cell per row
|
||||||
|
// every cell spans the figure's width, so this reduces to the nearest row by y.
|
||||||
u32 row = 0;
|
u32 row = 0;
|
||||||
f32 best = std::numeric_limits<f32>::infinity();
|
f32 best = std::numeric_limits<f32>::infinity();
|
||||||
for (u32 i = 0; i < rows.size(); ++i) {
|
for (u32 i = 0; i < rows.size(); ++i) {
|
||||||
const f32 top = rows[i].y;
|
const Rect& r = rows[i];
|
||||||
const f32 bottom = rows[i].y + rows[i].h;
|
const f32 dx = x < r.x ? r.x - x : (x >= r.right() ? x - r.right() : 0.0f);
|
||||||
const f32 distance = y < top ? top - y : (y >= bottom ? y - bottom : 0.0f);
|
const f32 dy = y < r.y ? r.y - y : (y >= r.bottom() ? y - r.bottom() : 0.0f);
|
||||||
|
const f32 distance = dx * dx + dy * dy;
|
||||||
if (distance < best) {
|
if (distance < best) {
|
||||||
best = distance;
|
best = distance;
|
||||||
row = i;
|
row = i;
|
||||||
if (distance == 0.0f) break; // inside this row: nothing can be nearer
|
if (distance == 0.0f) break; // inside this cell: nothing can be nearer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return row;
|
return row;
|
||||||
@@ -341,7 +346,7 @@ u32 PlotArea::pointer_row() const noexcept {
|
|||||||
// The captured row while a press owns the gesture, so an in-flight drag keeps its
|
// The captured row while a press owns the gesture, so an in-flight drag keeps its
|
||||||
// highlight even after the pointer has left the row it began in.
|
// highlight even after the pointer has left the row it began in.
|
||||||
if (press_open_ && press_row_ < row_rects_.size()) return press_row_;
|
if (press_open_ && press_row_ < row_rects_.size()) return press_row_;
|
||||||
return row_in(row_rects_, pointer_.y);
|
return row_in(row_rects_, pointer_.x, pointer_.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlotArea::sync_selection_frames() noexcept {
|
void PlotArea::sync_selection_frames() noexcept {
|
||||||
@@ -616,7 +621,7 @@ void PlotArea::pointer_press(input::Button button, u32 mods, double x, double y,
|
|||||||
// The figure row this gesture belongs to for its whole life, claimed from the press's
|
// The figure row this gesture belongs to for its whole life, claimed from the press's
|
||||||
// own position *before* the event is dispatched (see `target_controller()`). Harmless
|
// own position *before* the event is dispatched (see `target_controller()`). Harmless
|
||||||
// and unread while no figure is set.
|
// and unread while no figure is set.
|
||||||
press_row_ = figure_ != nullptr ? row_in(row_rects_, p.y) : 0;
|
press_row_ = figure_ != nullptr ? row_in(row_rects_, p.x, p.y) : 0;
|
||||||
press_open_ = true;
|
press_open_ = true;
|
||||||
press_button_ = button;
|
press_button_ = button;
|
||||||
press_mods_ = mods;
|
press_mods_ = mods;
|
||||||
@@ -634,7 +639,7 @@ void PlotArea::pointer_press(input::Button button, u32 mods, double x, double y,
|
|||||||
e.down = true;
|
e.down = true;
|
||||||
e.pressed = true;
|
e.pressed = true;
|
||||||
e.time_s = pointer_time_s(time_ms); // the double-click detector differences these
|
e.time_s = pointer_time_s(time_ms); // the double-click detector differences these
|
||||||
target_controller(p.y).on_pointer(e);
|
target_controller(p.x, p.y).on_pointer(e);
|
||||||
gtk_gl_area_queue_render(GTK_GL_AREA(widget_));
|
gtk_gl_area_queue_render(GTK_GL_AREA(widget_));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,7 +648,7 @@ void PlotArea::pointer_release(Vec2 position, u32 time_ms) noexcept {
|
|||||||
// Resolved while the press is still open, so the release reaches the row the press
|
// Resolved while the press is still open, so the release reaches the row the press
|
||||||
// claimed rather than the row the pointer ended over -- the controller that owns the
|
// claimed rather than the row the pointer ended over -- the controller that owns the
|
||||||
// drag is the only one that can close it.
|
// drag is the only one that can close it.
|
||||||
input::Controller& target = target_controller(position.y);
|
input::Controller& target = target_controller(position.x, position.y);
|
||||||
press_open_ = false;
|
press_open_ = false;
|
||||||
|
|
||||||
input::PointerEvent e;
|
input::PointerEvent e;
|
||||||
@@ -748,7 +753,7 @@ void PlotArea::on_drag_update(GtkGestureDrag* gesture, double offset_x, double o
|
|||||||
gtk_event_controller_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)));
|
gtk_event_controller_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)));
|
||||||
// The press's row, not the row the pointer has reached: `press_open_` is true here,
|
// The press's row, not the row the pointer has reached: `press_open_` is true here,
|
||||||
// so a drag that crosses a row boundary stays with the controller that began it.
|
// so a drag that crosses a row boundary stays with the controller that began it.
|
||||||
area->target_controller(sample.position.y).on_pointer(e);
|
area->target_controller(sample.position.x, sample.position.y).on_pointer(e);
|
||||||
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,7 +800,7 @@ void PlotArea::on_zoom_scale_changed(GtkGestureZoom* gesture, double scale,
|
|||||||
static_cast<f32>(scale), Vec2{static_cast<f32>(cx), static_cast<f32>(cy)}, area->mapping_);
|
static_cast<f32>(scale), Vec2{static_cast<f32>(cx), static_cast<f32>(cy)}, area->mapping_);
|
||||||
// The folded event carries the focal point, which is the position this pinch belongs
|
// The folded event carries the focal point, which is the position this pinch belongs
|
||||||
// to; while a press is open the capture overrides it, as it does for a drag.
|
// to; while a press is open the capture overrides it, as it does for a drag.
|
||||||
area->target_controller(g.y).on_gesture(g);
|
area->target_controller(g.x, g.y).on_gesture(g);
|
||||||
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,7 +839,7 @@ int PlotArea::on_scroll(GtkEventControllerScroll* scroll, double dx, double dy,
|
|||||||
e.dx = static_cast<f32>(scroll_notches(dx, unit));
|
e.dx = static_cast<f32>(scroll_notches(dx, unit));
|
||||||
e.dy = static_cast<f32>(notches);
|
e.dy = static_cast<f32>(notches);
|
||||||
e.mods = gesture_mods(GTK_EVENT_CONTROLLER(scroll));
|
e.mods = gesture_mods(GTK_EVENT_CONTROLLER(scroll));
|
||||||
area->target_controller(at.y).on_wheel(e);
|
area->target_controller(at.x, at.y).on_wheel(e);
|
||||||
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
gtk_gl_area_queue_render(GTK_GL_AREA(area->widget_));
|
||||||
return TRUE; // consumed: the plot zoomed
|
return TRUE; // consumed: the plot zoomed
|
||||||
}
|
}
|
||||||
@@ -858,7 +863,7 @@ void PlotArea::on_motion(GtkEventControllerMotion* motion, double x, double y, v
|
|||||||
e.down = false;
|
e.down = false;
|
||||||
e.time_s = PlotArea::pointer_time_s(
|
e.time_s = PlotArea::pointer_time_s(
|
||||||
gtk_event_controller_get_current_event_time(GTK_EVENT_CONTROLLER(motion)));
|
gtk_event_controller_get_current_event_time(GTK_EVENT_CONTROLLER(motion)));
|
||||||
area->target_controller(p.y).on_pointer(e);
|
area->target_controller(p.x, p.y).on_pointer(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- keyboard -------------------------------------------------------------
|
// --- keyboard -------------------------------------------------------------
|
||||||
|
|||||||
@@ -260,15 +260,17 @@ class PlotArea {
|
|||||||
Figure* figure() noexcept { return figure_; }
|
Figure* figure() noexcept { return figure_; }
|
||||||
const Figure* figure() const noexcept { return figure_; }
|
const Figure* figure() const noexcept { return figure_; }
|
||||||
|
|
||||||
/// One figure row's controller -- the one a gesture on that row reaches -- or null
|
/// One figure plot's controller -- the one a gesture on that plot reaches -- or null
|
||||||
/// while no figure is set or the row does not exist. A host reads a drag mode or a
|
/// while no figure is set or it does not exist. `row` is the **flattened plot index**
|
||||||
|
/// (`Figure::plot_count()` of them: the row count when no row holds several cells). A host reads a drag mode or a
|
||||||
/// hover out of it.
|
/// hover out of it.
|
||||||
input::Controller* row_controller(u32 row) noexcept;
|
input::Controller* row_controller(u32 row) noexcept;
|
||||||
|
|
||||||
/// The controller an event at device pixel `(x, y)` belongs to: `controller()`
|
/// The controller an event at device pixel `(x, y)` belongs to: `controller()`
|
||||||
/// while no figure is set, and otherwise the controller of the figure row that
|
/// while no figure is set, and otherwise the controller of the figure plot (cell)
|
||||||
/// pixel lands in -- a pixel inside no row (a `Figure::set_row_gap()` gap, a
|
/// that pixel lands in -- x and y both pick it, as a row may hold several cells side
|
||||||
/// margin) is the *nearest* row rather than dropped, so a drag that leaves the data
|
/// by side. A pixel inside no cell (a `Figure::set_row_gap()`/`set_col_gap()` gap, a
|
||||||
|
/// margin) is the *nearest* cell rather than dropped, so a drag that leaves the data
|
||||||
/// area still ends where it should. It also hands that row its own viewport, which
|
/// area still ends where it should. It also hands that row its own viewport, which
|
||||||
/// is what keeps the controller's hit-test geometry in step with the layout between
|
/// is what keeps the controller's hit-test geometry in step with the layout between
|
||||||
/// two frames.
|
/// two frames.
|
||||||
@@ -277,10 +279,10 @@ class PlotArea {
|
|||||||
/// press capture, so a host may call it freely.
|
/// press capture, so a host may call it freely.
|
||||||
input::Controller& controller_at(f32 x, f32 y) noexcept;
|
input::Controller& controller_at(f32 x, f32 y) noexcept;
|
||||||
|
|
||||||
/// The figure row a point in the widget's **logical** coordinates lands in -- the
|
/// The flattened plot index a point in the widget's **logical** coordinates lands in -- the
|
||||||
/// coordinates every GTK event controller reports, so a host's own drop target or
|
/// coordinates every GTK event controller reports, so a host's own drop target or
|
||||||
/// context menu can name the row a pointer is over without converting anything. The
|
/// context menu can name the plot a pointer is over without converting anything. The
|
||||||
/// same nearest-row rule as `controller_at()`, and 0 while no figure is set.
|
/// same nearest-cell rule (x and y) as `controller_at()`, and 0 while no figure is set.
|
||||||
u32 row_at(double x, double y) const noexcept;
|
u32 row_at(double x, double y) const noexcept;
|
||||||
|
|
||||||
/// Re-reads the figure's row rects into the cache the event routing answers from --
|
/// Re-reads the figure's row rects into the cache the event routing answers from --
|
||||||
@@ -376,20 +378,21 @@ class PlotArea {
|
|||||||
/// into device pixels. The one place that mapping exists.
|
/// into device pixels. The one place that mapping exists.
|
||||||
Vec2 to_device(double x, double y) const noexcept;
|
Vec2 to_device(double x, double y) const noexcept;
|
||||||
|
|
||||||
/// The controller one event of this area's goes to, given the event's device y.
|
/// The controller one event of this area's goes to, given the event's device (x, y).
|
||||||
/// `controller_` while no figure is set; otherwise the row the gesture *belongs*
|
/// `controller_` while no figure is set; otherwise the row the gesture *belongs*
|
||||||
/// to: the row the open press claimed while a press is open, and the row under `y`
|
/// to: the row the open press claimed while a press is open, and the row under `y`
|
||||||
/// between gestures. That is the capture `controller_at()` deliberately omits, and
|
/// between gestures (x and y pick the cell). That is the capture `controller_at()` deliberately omits, and
|
||||||
/// it is why this -- not `controller_at()` -- is what every handler below calls.
|
/// it is why this -- not `controller_at()` -- is what every handler below calls.
|
||||||
///
|
///
|
||||||
/// Hands the chosen row its own viewport, so the geometry it hit-tests is the
|
/// Hands the chosen row its own viewport, so the geometry it hit-tests is the
|
||||||
/// geometry the last frame drew.
|
/// geometry the last frame drew.
|
||||||
input::Controller& target_controller(f32 y) noexcept;
|
input::Controller& target_controller(f32 x, f32 y) noexcept;
|
||||||
/// The row of `row_rects_` a device y belongs to: the row it is in, or the nearest
|
/// The cell of `row_rects_` (the figure's flattened cell rects) a device pixel belongs
|
||||||
/// one when it is in none (a row gap, a margin outside the figure). Selection by
|
/// to: the cell it is in, or the nearest one when it is in none (a row or column gap,
|
||||||
/// distance rather than "the first that matches" is what makes a pixel in a visible
|
/// a margin outside the figure). Selection by distance rather than "the first that
|
||||||
/// gap belong to the row it is *near* instead of to row 0.
|
/// matches" is what makes a pixel in a visible gap belong to the cell it is *near*
|
||||||
static u32 row_in(const std::vector<Rect>& rows, f32 y) noexcept;
|
/// instead of to plot 0.
|
||||||
|
static u32 row_in(const std::vector<Rect>& rows, f32 x, f32 y) noexcept;
|
||||||
/// The row this area's events currently go to: the press's row while one is open,
|
/// The row this area's events currently go to: the press's row while one is open,
|
||||||
/// so an in-flight gesture keeps its highlight, and the row under the last pointer
|
/// so an in-flight gesture keeps its highlight, and the row under the last pointer
|
||||||
/// position otherwise. 0 with no rows.
|
/// position otherwise. 0 with no rows.
|
||||||
@@ -457,7 +460,8 @@ class PlotArea {
|
|||||||
/// `set_figure()` is called, and the single-plot path reads neither.
|
/// `set_figure()` is called, and the single-plot path reads neither.
|
||||||
Figure* figure_ = nullptr;
|
Figure* figure_ = nullptr;
|
||||||
std::vector<std::unique_ptr<input::Controller>> row_controllers_;
|
std::vector<std::unique_ptr<input::Controller>> row_controllers_;
|
||||||
/// The figure's row rects as of the last `refresh_row_rects()`: the cache the event
|
/// The figure's cell rects (one per plot, flattened; the row rects when no row is split)
|
||||||
|
/// as of the last `refresh_row_rects()`: the cache the event
|
||||||
/// routing reads instead of laying the figure out per event. Empty while no figure
|
/// routing reads instead of laying the figure out per event. Empty while no figure
|
||||||
/// is set, which is the state the routing falls back to `controller_` on.
|
/// is set, which is the state the routing falls back to `controller_` on.
|
||||||
std::vector<Rect> row_rects_;
|
std::vector<Rect> row_rects_;
|
||||||
|
|||||||
@@ -27,8 +27,15 @@ struct FigureLayout {
|
|||||||
/// strips and axes come from its own `Plot::layout()` against this rect, so a row
|
/// strips and axes come from its own `Plot::layout()` against this rect, so a row
|
||||||
/// with a y title keeps its column and no two rows can disagree about chrome.
|
/// with a y title keeps its column and no two rows can disagree about chrome.
|
||||||
std::vector<Rect> rows;
|
std::vector<Rect> rows;
|
||||||
|
/// One rect per plot, flattened row-major (row 0's cells left to right, then row 1's,
|
||||||
|
/// ...): the **viewport** each `Plot` was given. A row of one cell has exactly its row
|
||||||
|
/// rect here; a row of several tiles its rect left to right with `col_gap` between
|
||||||
|
/// cells, the cells' widths summing to the row's less the gaps.
|
||||||
|
std::vector<Rect> cells;
|
||||||
/// The strip the figure's one shared x axis is drawn in: the bottom row's own
|
/// The strip the figure's one shared x axis is drawn in: the bottom row's own
|
||||||
/// `PlotLayout::x_label_area`, at the bottom of the bottom row's rect and so as
|
/// `PlotLayout::x_label_area` (for a bottom row split into several cells, only the **last
|
||||||
|
/// cell's** strip -- the others' strips are their own `plot(i).layout().x_label_area`, which
|
||||||
|
/// is where the controller's axis-zoom hit test reads each cell's gestures from), at the bottom of the bottom row's rect and so as
|
||||||
/// wide as that row's axes rather than the whole figure. Read back from the row
|
/// wide as that row's axes rather than the whole figure. Read back from the row
|
||||||
/// rather than derived here, so a host and the frame cannot disagree about where
|
/// rather than derived here, so a host and the frame cannot disagree about where
|
||||||
/// the axis is. It is nonzero whenever the figure has rows and the bottom row
|
/// the axis is. It is nonzero whenever the figure has rows and the bottom row
|
||||||
@@ -44,6 +51,10 @@ struct FigureLayout {
|
|||||||
/// figure edge to edge, which is the default; it is dropped for a figure too
|
/// figure edge to edge, which is the default; it is dropped for a figure too
|
||||||
/// short to hold it, so the rows never lose their height to a gap.
|
/// short to hold it, so the rows never lose their height to a gap.
|
||||||
f32 row_gap = 0.0f;
|
f32 row_gap = 0.0f;
|
||||||
|
/// The horizontal gap between two cells of a row, in device pixels. `set_col_gap()`'s
|
||||||
|
/// value, or 0 when any row holding several cells was too narrow to hold it and
|
||||||
|
/// dropped it, so the cells never lose their width to a gap.
|
||||||
|
f32 col_gap = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Several plots stacked in one column, sharing one x axis.
|
/// Several plots stacked in one column, sharing one x axis.
|
||||||
@@ -94,13 +105,30 @@ class Figure {
|
|||||||
/// which is the *unlinked* group: rows that share nothing, each with the range
|
/// which is the *unlinked* group: rows that share nothing, each with the range
|
||||||
/// its own plot was given.
|
/// its own plot was given.
|
||||||
explicit Figure(u32 rows);
|
explicit Figure(u32 rows);
|
||||||
|
/// A figure of `cells_per_row.size()` rows, row `r` holding `cells_per_row[r]` plots side
|
||||||
|
/// by side (a count of 0 is clamped to 1). Every row of one cell makes this the same
|
||||||
|
/// figure as `Figure(rows)`. There is deliberately no `initializer_list` constructor, so
|
||||||
|
/// `Figure{3u}` keeps meaning three rows.
|
||||||
|
explicit Figure(std::span<const u32> cells_per_row);
|
||||||
|
|
||||||
u32 row_count() const noexcept { return static_cast<u32>(plots_.size()); }
|
/// The number of figure rows -- the bands stacked top to bottom -- which equals
|
||||||
/// One row's plot. A row that does not exist yields a default-constructed `Plot`
|
/// `plot_count()` for a figure no row of which is split.
|
||||||
/// rather than reading out of bounds -- the rule every accessor on `Plot` follows
|
u32 row_count() const noexcept { return static_cast<u32>(cells_per_row_.size()); }
|
||||||
/// -- so a stale row can only ever affect itself.
|
/// The number of plots, i.e. cells over all rows. `plot()`, `link_x()`, `x_link()` and
|
||||||
Plot& plot(u32 row) noexcept;
|
/// the hovered plot of `set_crosshair()` index this flattened (row-major) range.
|
||||||
const Plot& plot(u32 row) const noexcept;
|
u32 plot_count() const noexcept { return static_cast<u32>(plots_.size()); }
|
||||||
|
/// How many cells row `row` holds; 0 for a row that does not exist.
|
||||||
|
u32 cells_in_row(u32 row) const noexcept;
|
||||||
|
/// The flattened index of row `row`'s first cell; `plot_count()` for `row ==
|
||||||
|
/// row_count()` and for anything beyond.
|
||||||
|
u32 first_plot_of_row(u32 row) const noexcept;
|
||||||
|
/// The row that holds flattened plot `plot` (the last row for an index past the end).
|
||||||
|
u32 row_of_plot(u32 plot) const noexcept;
|
||||||
|
/// One plot, by flattened cell index. A plot that does not exist yields a
|
||||||
|
/// default-constructed `Plot` rather than reading out of bounds -- the rule every
|
||||||
|
/// accessor on `Plot` follows -- so a stale index can only ever affect itself.
|
||||||
|
Plot& plot(u32 index) noexcept;
|
||||||
|
const Plot& plot(u32 index) const noexcept;
|
||||||
|
|
||||||
// --- geometry ---------------------------------------------------------
|
// --- geometry ---------------------------------------------------------
|
||||||
|
|
||||||
@@ -119,6 +147,21 @@ class Figure {
|
|||||||
void clear_row_weights() noexcept { row_weights_.clear(); }
|
void clear_row_weights() noexcept { row_weights_.clear(); }
|
||||||
bool has_row_weights() const noexcept { return !row_weights_.empty(); }
|
bool has_row_weights() const noexcept { return !row_weights_.empty(); }
|
||||||
|
|
||||||
|
/// How much of row `row`'s width each of its cells gets: `weights[i]` is cell i's
|
||||||
|
/// share of what is left after every cell has its floor width (64 logical pixels,
|
||||||
|
/// capped at an equal share of a narrow row). Normalised. Refused, changing nothing,
|
||||||
|
/// when the row does not exist, the count is not the row's cell count, or a weight is
|
||||||
|
/// not a positive finite number. A row with no weights divides equally.
|
||||||
|
Status set_cell_weights(u32 row, std::span<const f32> weights);
|
||||||
|
/// Back to an equal division of row `row`; a row that does not exist is ignored.
|
||||||
|
void clear_cell_weights(u32 row) noexcept;
|
||||||
|
bool has_cell_weights(u32 row) const noexcept;
|
||||||
|
/// The horizontal gap left between two cells of a row, in device pixels (0.0 by
|
||||||
|
/// default). Negative values are clamped to 0, and a gap too wide for a row to hold is
|
||||||
|
/// dropped for that layout rather than squeezing the cells to nothing.
|
||||||
|
void set_col_gap(f32 device_px) noexcept { col_gap_ = device_px > 0.0f ? device_px : 0.0f; }
|
||||||
|
f32 col_gap() const noexcept { return col_gap_; }
|
||||||
|
|
||||||
/// The gap left between two rows, in device pixels (0.0 by default, so the rows
|
/// The gap left between two rows, in device pixels (0.0 by default, so the rows
|
||||||
/// tile the figure). Negative values are clamped to 0, and a gap too large for the
|
/// tile the figure). Negative values are clamped to 0, and a gap too large for the
|
||||||
/// figure to hold is dropped for that layout rather than squeezing the rows to
|
/// figure to hold is dropped for that layout rather than squeezing the rows to
|
||||||
@@ -195,17 +238,18 @@ class Figure {
|
|||||||
/// is not a number; such a call therefore leaves every row's crosshair as it was.
|
/// is not a number; such a call therefore leaves every row's crosshair as it was.
|
||||||
void set_crosshair(f64 x, bool active);
|
void set_crosshair(f64 x, bool active);
|
||||||
|
|
||||||
/// Places the crosshair with the pointer's **row** and that row's local y (spec §17): the
|
/// Places the crosshair with the pointer's **plot** and that plot's local y (spec §17): the
|
||||||
/// shared x goes to every row -- vertical lines everywhere, as the plain overload does -- and
|
/// shared x goes to every plot -- vertical lines everywhere, as the plain overload does -- and
|
||||||
/// the horizontal line is drawn **only in `hovered_row`**, where its y is a value of *that*
|
/// the horizontal line is drawn **only in `hovered_row`** -- the **flattened plot index**, not
|
||||||
/// row's own scale. It is the fix for the reported defect -- a figure showing no horizontal
|
/// the figure-row index; they differ when a row holds several cells -- where its y is a value of
|
||||||
|
/// *that* plot's own scale. It is the fix for the reported defect -- a figure showing no horizontal
|
||||||
/// line at all -- without inventing a figure-wide y, which would mean a different value in
|
/// line at all -- without inventing a figure-wide y, which would mean a different value in
|
||||||
/// every row: the line appears where the pointer is, and no row ever shows another row's y.
|
/// every row: the line appears where the pointer is, and no row ever shows another row's y.
|
||||||
///
|
///
|
||||||
/// `y_in_row` is a data coordinate on `hovered_row`'s y axis 0, exactly as `Crosshair::y`'s
|
/// `y_in_row` is a data coordinate on `hovered_row`'s y axis 0, exactly as `Crosshair::y`'s
|
||||||
/// two-coordinate placement is; a host reads it from the row's own y transform at the
|
/// two-coordinate placement is; a host reads it from the row's own y transform at the
|
||||||
/// pointer's pixel row. `hovered_row` outside `[0, row_count())` is treated as "no row under
|
/// pointer's pixel row. `hovered_row` outside `[0, plot_count())` is treated as "no plot under
|
||||||
/// the pointer": every row gets the vertical line and none gets a horizontal one, the same
|
/// the pointer": every plot gets the vertical line and none gets a horizontal one, the same
|
||||||
/// state the single-coordinate overload produces.
|
/// state the single-coordinate overload produces.
|
||||||
///
|
///
|
||||||
/// Refused, changing nothing, when `x` is not a number -- matching `set_crosshair(x, active)`
|
/// Refused, changing nothing, when `x` is not a number -- matching `set_crosshair(x, active)`
|
||||||
@@ -226,9 +270,10 @@ class Figure {
|
|||||||
/// How many groups there are, excluding the unlinked group 0.
|
/// How many groups there are, excluding the unlinked group 0.
|
||||||
u32 x_link_count() const noexcept { return static_cast<u32>(links_.size()); }
|
u32 x_link_count() const noexcept { return static_cast<u32>(links_.size()); }
|
||||||
|
|
||||||
/// Moves one row into a link group and applies that group's range to it at once,
|
/// Moves one plot into a link group and applies that group's range to it at once; `row`
|
||||||
/// so the row is on the group's window from this call rather than from the next
|
/// is the **flattened plot index** (see `plot_count()`), which is the row index only for a
|
||||||
/// frame. `group` 0 takes the row back out of its group, leaving it with the range
|
/// figure with one cell per row. The plot is on the group's window from this call rather
|
||||||
|
/// than from the next frame. `group` 0 takes the plot back out of its group, leaving it with the range
|
||||||
/// it holds now.
|
/// it holds now.
|
||||||
///
|
///
|
||||||
/// A group with no range yet -- one that no row and no `set_x_link_range()` has
|
/// A group with no range yet -- one that no row and no `set_x_link_range()` has
|
||||||
@@ -242,7 +287,7 @@ class Figure {
|
|||||||
/// per-row window. `XFollow::Off` is the group's to set
|
/// per-row window. `XFollow::Off` is the group's to set
|
||||||
/// (`set_x_link_follow()`), not the axis'.
|
/// (`set_x_link_follow()`), not the axis'.
|
||||||
Status link_x(u32 row, u32 group);
|
Status link_x(u32 row, u32 group);
|
||||||
/// The group a row is in, or 0 when it is in none.
|
/// The group the plot with flattened index `row` is in, or 0 when it is in none.
|
||||||
u32 x_link(u32 row) const noexcept;
|
u32 x_link(u32 row) const noexcept;
|
||||||
|
|
||||||
/// Sets a group's shared range and pushes it to every member.
|
/// Sets a group's shared range and pushes it to every member.
|
||||||
@@ -325,7 +370,18 @@ class Figure {
|
|||||||
/// a following group's window, push both to all members.
|
/// a following group's window, push both to all members.
|
||||||
void sync_x_links();
|
void sync_x_links();
|
||||||
|
|
||||||
|
/// Fills the row bookkeeping (`cells_per_row_`, `row_first_`, `cell_weights_`) from a
|
||||||
|
/// per-row cell count, clamping each to at least one. Does not touch `plots_`, which the
|
||||||
|
/// constructors size from `total_cells()`.
|
||||||
|
void build_cells(std::span<const u32> cells);
|
||||||
|
|
||||||
std::vector<Plot> plots_;
|
std::vector<Plot> plots_;
|
||||||
|
std::vector<u32> cells_per_row_;
|
||||||
|
/// Prefix sums of `cells_per_row_`: `rows + 1` entries, `row_first_[r]` the flattened
|
||||||
|
/// index of row r's first cell and the last entry the plot count.
|
||||||
|
std::vector<u32> row_first_;
|
||||||
|
std::vector<std::vector<f32>> cell_weights_;
|
||||||
|
f32 col_gap_ = 0.0f;
|
||||||
/// What `plot()` yields for a row that does not exist, so a stale row can be used
|
/// What `plot()` yields for a row that does not exist, so a stale row can be used
|
||||||
/// without reading out of bounds. Owned rather than a static, so one figure's
|
/// without reading out of bounds. Owned rather than a static, so one figure's
|
||||||
/// stray row can never write into another's.
|
/// stray row can never write into another's.
|
||||||
|
|||||||
+148
-24
@@ -23,6 +23,56 @@ namespace {
|
|||||||
/// wants a taller floor gives the figure a taller viewport, or gives the rows weights.
|
/// wants a taller floor gives the figure a taller viewport, or gives the rows weights.
|
||||||
constexpr f32 kMinRowHeightLogical = 24.0f;
|
constexpr f32 kMinRowHeightLogical = 24.0f;
|
||||||
|
|
||||||
|
/// The width a cell never drops below, in logical pixels, converted exactly as the row
|
||||||
|
/// height floor is. It yields to the row -- capped at an equal share of the width there
|
||||||
|
/// is -- so a row too narrow for its cells still tiles instead of overflowing.
|
||||||
|
constexpr f32 kMinCellWidthLogical = 64.0f;
|
||||||
|
|
||||||
|
/// The number of plots a per-row cell count asks for, each row holding at least one.
|
||||||
|
u32 total_cells(std::span<const u32> cells) noexcept {
|
||||||
|
u32 total = 0;
|
||||||
|
for (const u32 c : cells) total += std::max<u32>(c, 1u);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits the row rect `r` into `n` cells in `out[0..n)` and returns the gap actually used.
|
||||||
|
/// The cells tile `r` exactly: the gap is dropped when `r` cannot hold it, every cell first
|
||||||
|
/// gets the floor width and then its weight's share of what is left, and the last cell
|
||||||
|
/// closes the row on its right edge, absorbing the rounding of the ones before it.
|
||||||
|
f32 place_cells(const Rect& r, u32 n, const std::vector<f32>& weights, f32 col_gap,
|
||||||
|
f32 ui_scale, Rect* out) {
|
||||||
|
if (n == 1u) {
|
||||||
|
out[0] = r;
|
||||||
|
return col_gap;
|
||||||
|
}
|
||||||
|
f32 gap = col_gap;
|
||||||
|
f32 usable = r.w - gap * static_cast<f32>(n - 1u);
|
||||||
|
if (usable <= 0.0f) {
|
||||||
|
gap = 0.0f;
|
||||||
|
usable = r.w;
|
||||||
|
}
|
||||||
|
usable = std::max(0.0f, usable);
|
||||||
|
const f32 floor_device = ui_scale > 0.0f ? kMinCellWidthLogical * ui_scale
|
||||||
|
: kMinCellWidthLogical;
|
||||||
|
const f32 floor_px = std::min(floor_device, usable / static_cast<f32>(n));
|
||||||
|
const f32 extra = usable - floor_px * static_cast<f32>(n);
|
||||||
|
f32 total = 0.0f;
|
||||||
|
const bool weighted = weights.size() == n;
|
||||||
|
if (weighted) {
|
||||||
|
for (const f32 w : weights) total += w;
|
||||||
|
}
|
||||||
|
f32 x = r.x;
|
||||||
|
f32 consumed = 0.0f;
|
||||||
|
for (u32 i = 0; i < n; ++i) {
|
||||||
|
const f32 share = weighted && total > 0.0f ? weights[i] / total : 1.0f / static_cast<f32>(n);
|
||||||
|
const f32 end = i + 1u == n ? usable : consumed + floor_px + extra * share;
|
||||||
|
out[i] = Rect{x, r.y, end - consumed, r.h};
|
||||||
|
consumed = end;
|
||||||
|
x = out[i].right() + gap;
|
||||||
|
}
|
||||||
|
return gap;
|
||||||
|
}
|
||||||
|
|
||||||
/// A coordinate in the shared axis' **axis space**: `log10` when the figure's one
|
/// A coordinate in the shared axis' **axis space**: `log10` when the figure's one
|
||||||
/// x axis -- the bottom row's, the row the shared strip belongs to -- is
|
/// x axis -- the bottom row's, the row the shared strip belongs to -- is
|
||||||
/// logarithmic, the value itself otherwise. A following group's span and offset
|
/// logarithmic, the value itself otherwise. A following group's span and offset
|
||||||
@@ -41,18 +91,72 @@ f64 from_link_space(f64 v, bool log) noexcept {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Figure::Figure(u32 rows) : plots_(rows) {}
|
Figure::Figure(u32 rows) : plots_(rows) {
|
||||||
|
const std::vector<u32> ones(rows, 1u);
|
||||||
Plot& Figure::plot(u32 row) noexcept {
|
build_cells(std::span<const u32>(ones));
|
||||||
return row < plots_.size() ? plots_[row] : fallback_;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Plot& Figure::plot(u32 row) const noexcept {
|
Figure::Figure(std::span<const u32> cells_per_row) : plots_(total_cells(cells_per_row)) {
|
||||||
return row < plots_.size() ? plots_[row] : fallback_;
|
build_cells(cells_per_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Figure::build_cells(std::span<const u32> cells) {
|
||||||
|
cells_per_row_.assign(cells.begin(), cells.end());
|
||||||
|
for (u32& c : cells_per_row_) c = std::max<u32>(c, 1u);
|
||||||
|
row_first_.assign(cells_per_row_.size() + 1u, 0u);
|
||||||
|
for (std::size_t r = 0; r < cells_per_row_.size(); ++r) {
|
||||||
|
row_first_[r + 1u] = row_first_[r] + cells_per_row_[r];
|
||||||
|
}
|
||||||
|
cell_weights_.assign(cells_per_row_.size(), {});
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 Figure::cells_in_row(u32 row) const noexcept {
|
||||||
|
return row < cells_per_row_.size() ? cells_per_row_[row] : 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 Figure::first_plot_of_row(u32 row) const noexcept {
|
||||||
|
return row < cells_per_row_.size() ? row_first_[row] : plot_count();
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 Figure::row_of_plot(u32 plot) const noexcept {
|
||||||
|
u32 r = 0;
|
||||||
|
while (r + 1u < cells_per_row_.size() && row_first_[r + 1u] <= plot) ++r;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plot& Figure::plot(u32 index) noexcept {
|
||||||
|
return index < plots_.size() ? plots_[index] : fallback_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Plot& Figure::plot(u32 index) const noexcept {
|
||||||
|
return index < plots_.size() ? plots_[index] : fallback_;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status Figure::set_cell_weights(u32 row, std::span<const f32> weights) {
|
||||||
|
if (row >= cells_per_row_.size() || weights.size() != cells_per_row_[row]) {
|
||||||
|
return Status{ErrorCode::InvalidArgument, "cell weights must name every cell of an existing row"};
|
||||||
|
}
|
||||||
|
for (const f32 w : weights) {
|
||||||
|
if (!std::isfinite(w) || !(w > 0.0f)) {
|
||||||
|
return Status{ErrorCode::InvalidArgument,
|
||||||
|
"cell weights must be positive finite numbers: a weight is a share of "
|
||||||
|
"the row, and a cell with none of it has no rect"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cell_weights_[row].assign(weights.begin(), weights.end());
|
||||||
|
return Status{};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Figure::clear_cell_weights(u32 row) noexcept {
|
||||||
|
if (row < cell_weights_.size()) cell_weights_[row].clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Figure::has_cell_weights(u32 row) const noexcept {
|
||||||
|
return row < cell_weights_.size() && !cell_weights_[row].empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
Status Figure::set_row_weights(std::span<const f32> weights) {
|
Status Figure::set_row_weights(std::span<const f32> weights) {
|
||||||
if (weights.size() != plots_.size()) {
|
if (weights.size() != cells_per_row_.size()) {
|
||||||
return Status{ErrorCode::InvalidArgument, "row weights must name every row"};
|
return Status{ErrorCode::InvalidArgument, "row weights must name every row"};
|
||||||
}
|
}
|
||||||
f32 total = 0.0f;
|
f32 total = 0.0f;
|
||||||
@@ -369,6 +473,8 @@ void Figure::sync_x_links() {
|
|||||||
const FigureLayout& Figure::layout() {
|
const FigureLayout& Figure::layout() {
|
||||||
const u32 rows = row_count();
|
const u32 rows = row_count();
|
||||||
layout_.rows.assign(rows, Rect{});
|
layout_.rows.assign(rows, Rect{});
|
||||||
|
layout_.cells.clear();
|
||||||
|
layout_.col_gap = 0.0f;
|
||||||
layout_.row_gap = row_gap_;
|
layout_.row_gap = row_gap_;
|
||||||
layout_.x_axis_strip = Rect{};
|
layout_.x_axis_strip = Rect{};
|
||||||
if (rows == 0u) {
|
if (rows == 0u) {
|
||||||
@@ -441,20 +547,35 @@ const FigureLayout& Figure::layout() {
|
|||||||
// short to hold the one asked for.
|
// short to hold the one asked for.
|
||||||
layout_.row_gap = gap;
|
layout_.row_gap = gap;
|
||||||
|
|
||||||
|
// Each row rect is split into its cells; a one-cell row's cell is the rect itself, so an
|
||||||
|
// unsplit figure lays out exactly as it always did.
|
||||||
|
layout_.cells.assign(plots_.size(), Rect{});
|
||||||
|
layout_.col_gap = col_gap_;
|
||||||
|
for (u32 row = 0; row < rows; ++row) {
|
||||||
|
const u32 n = cells_per_row_[row];
|
||||||
|
const f32 used = place_cells(layout_.rows[row], n, cell_weights_[row], col_gap_,
|
||||||
|
floor_scale, &layout_.cells[row_first_[row]]);
|
||||||
|
if (n > 1u && used < col_gap_) layout_.col_gap = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
const u32 bottom_row = rows - 1u;
|
const u32 bottom_row = rows - 1u;
|
||||||
for (u32 row = 0; row < rows; ++row) {
|
for (u32 row = 0; row < rows; ++row) {
|
||||||
Plot& p = plots_[row];
|
// One x axis for the whole figure, and it is the bottom row's: every plot keeps its
|
||||||
p.set_viewport(layout_.rows[row]);
|
|
||||||
// One x axis for the whole figure, and it is the bottom row's: every row keeps its
|
|
||||||
// x chrome -- labels, title and the strip the controller's axis zoom is found in --
|
// x chrome -- labels, title and the strip the controller's axis zoom is found in --
|
||||||
// and every other row gives all of it to that one, so the labels and the title are
|
// and every plot above the bottom row gives all of it to that row, so the labels and
|
||||||
// not drawn once per row. A figure told to label every row keeps them everywhere,
|
// the title are not drawn once per row. A figure told to label every row keeps them
|
||||||
// and then there is no shared axis to speak of: each row has its own.
|
// everywhere, and then there is no shared axis to speak of: each plot has its own. A
|
||||||
p.set_x_strip_mode(labels_bottom_only_ && row != bottom_row ? XStrip::None : XStrip::Both);
|
// split bottom row labels each of its cells, each against its own x axis.
|
||||||
// Before either pass below, and the same value for every row: nothing is measured on
|
const XStrip strip = labels_bottom_only_ && row != bottom_row ? XStrip::None : XStrip::Both;
|
||||||
// the right, so unlike the left gutter this needs no measurement pass -- the rows end
|
for (u32 i = row_first_[row]; i < row_first_[row + 1u]; ++i) {
|
||||||
// on a common column because they are all told the one width.
|
Plot& p = plots_[i];
|
||||||
p.set_right_gutter(right_gutter_);
|
p.set_viewport(layout_.cells[i]);
|
||||||
|
p.set_x_strip_mode(strip);
|
||||||
|
// Before either pass below, and the same value for every plot: nothing is measured on
|
||||||
|
// the right, so unlike the left gutter this needs no measurement pass -- the plots
|
||||||
|
// end on a common column because they are all told the one width.
|
||||||
|
p.set_right_gutter(right_gutter_);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// After the viewports, so a row's ticks and grid -- built from its axes rect -- and
|
// After the viewports, so a row's ticks and grid -- built from its axes rect -- and
|
||||||
// its transforms describe this frame's rects; and before the rows lay themselves
|
// its transforms describe this frame's rects; and before the rows lay themselves
|
||||||
@@ -486,18 +607,21 @@ const FigureLayout& Figure::layout() {
|
|||||||
// `set_min_y_gutter()` seeds the maximum rather than being imposed on a row before the
|
// `set_min_y_gutter()` seeds the maximum rather than being imposed on a row before the
|
||||||
// measurement, so a reserved floor cannot widen what a row reports and then be widened
|
// measurement, so a reserved floor cannot widen what a row reports and then be widened
|
||||||
// by its own reservation on the next frame.
|
// by its own reservation on the next frame.
|
||||||
|
//
|
||||||
|
// Over the flattened plots: every cell of every row takes part, so the y axes of a split
|
||||||
|
// row line up with each other and with the rows above and below.
|
||||||
f32 gutter = min_y_gutter_;
|
f32 gutter = min_y_gutter_;
|
||||||
for (u32 row = 0; row < rows; ++row) {
|
for (Plot& p : plots_) {
|
||||||
plots_[row].set_min_y_gutter(0.0f);
|
p.set_min_y_gutter(0.0f);
|
||||||
const PlotLayout& row_layout = plots_[row].layout();
|
const PlotLayout& row_layout = p.layout();
|
||||||
gutter = std::max(gutter, row_layout.y_axis_width + row_layout.y_title_width);
|
gutter = std::max(gutter, row_layout.y_axis_width + row_layout.y_title_width);
|
||||||
}
|
}
|
||||||
// A one-row figure imposes exactly what that row measured, so it lays out as the plot
|
// A one-row figure imposes exactly what that row measured, so it lays out as the plot
|
||||||
// would on its own; a figure whose rows all need the same strip imposes that, which is
|
// would on its own; a figure whose rows all need the same strip imposes that, which is
|
||||||
// also what each would have done alone.
|
// also what each would have done alone.
|
||||||
for (u32 row = 0; row < rows; ++row) {
|
for (Plot& p : plots_) {
|
||||||
plots_[row].set_min_y_gutter(gutter);
|
p.set_min_y_gutter(gutter);
|
||||||
plots_[row].layout();
|
p.layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The figure's one x axis is the bottom row's, so the strip it is drawn in is that
|
// The figure's one x axis is the bottom row's, so the strip it is drawn in is that
|
||||||
|
|||||||
@@ -471,7 +471,7 @@ void Renderer::draw(Figure& figure) {
|
|||||||
// Every row is prepared before the figure lays itself out, because a row's strips
|
// Every row is prepared before the figure lays itself out, because a row's strips
|
||||||
// are measured from the metrics this installs: a row measuring with one backend and
|
// are measured from the metrics this installs: a row measuring with one backend and
|
||||||
// drawing with another is the defect the propagation point exists to prevent.
|
// drawing with another is the defect the propagation point exists to prevent.
|
||||||
const u32 rows = figure.row_count();
|
const u32 rows = figure.plot_count();
|
||||||
for (u32 row = 0; row < rows; ++row) prepare_plot(figure.plot(row));
|
for (u32 row = 0; row < rows; ++row) prepare_plot(figure.plot(row));
|
||||||
|
|
||||||
// The figure's one layout for the frame. It hands every row its rect and its
|
// The figure's one layout for the frame. It hands every row its rect and its
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <plot/render/embedded_mono_font.hpp>
|
#include <plot/render/embedded_mono_font.hpp>
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <span>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -1141,3 +1142,136 @@ PLOT_TEST(figure_crosshair_draws_a_horizontal_line_only_in_the_hovered_row) {
|
|||||||
PLOT_CHECK(figure.plot(1u).layout().crosshair.vertical.w > 0.0f);
|
PLOT_CHECK(figure.plot(1u).layout().crosshair.vertical.w > 0.0f);
|
||||||
PLOT_CHECK(figure.plot(1u).layout().crosshair.horizontal.h == 0.0f);
|
PLOT_CHECK(figure.plot(1u).layout().crosshair.horizontal.h == 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ cell grid
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// A figure whose rows hold `cells[r]` plots each (the flattened plots in row-major
|
||||||
|
/// order), with an x and a y axis on every plot unless `axes` is off.
|
||||||
|
plot::Figure split_figure(const std::vector<plot::u32>& cells, bool axes = true) {
|
||||||
|
plot::Figure figure{std::span<const plot::u32>(cells)}; // braces: parentheses would be a vexing parse
|
||||||
|
figure.set_viewport(whole_figure());
|
||||||
|
if (axes) {
|
||||||
|
for (plot::u32 i = 0; i < figure.plot_count(); ++i) {
|
||||||
|
figure.plot(i).add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
||||||
|
figure.plot(i).add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return figure;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cells_all_ones_equals_rows) {
|
||||||
|
plot::Figure split = split_figure({1, 1, 1});
|
||||||
|
plot::Figure plain = figure_with_axes(3u);
|
||||||
|
const plot::FigureLayout& sl = split.layout();
|
||||||
|
const plot::FigureLayout& pl = plain.layout();
|
||||||
|
PLOT_CHECK(split.row_count() == 3u && split.plot_count() == 3u);
|
||||||
|
PLOT_CHECK(sl.cells.size() == 3u);
|
||||||
|
for (plot::u32 i = 0; i < 3u; ++i) {
|
||||||
|
PLOT_CHECK_NEAR(sl.cells[i].y, pl.rows[i].y, 0.01);
|
||||||
|
PLOT_CHECK_NEAR(sl.cells[i].h, pl.rows[i].h, 0.01);
|
||||||
|
PLOT_CHECK_NEAR(sl.cells[i].w, pl.rows[i].w, 0.01);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cells_tile_exactly) {
|
||||||
|
plot::Figure figure = split_figure({1, 2, 1});
|
||||||
|
figure.set_col_gap(10.0f);
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
PLOT_CHECK(figure.row_count() == 3u && figure.plot_count() == 4u);
|
||||||
|
PLOT_CHECK(figure.cells_in_row(1u) == 2u);
|
||||||
|
PLOT_CHECK(figure.first_plot_of_row(1u) == 1u && figure.first_plot_of_row(3u) == 4u);
|
||||||
|
PLOT_CHECK(figure.row_of_plot(2u) == 1u);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[2].x - fl.cells[1].right(), 10.0, 0.01);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[2].right(), fl.rows[1].right(), 0.001);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[1].x, fl.rows[1].x, 0.001);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[1].y, fl.cells[2].y, 0.001);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[1].h, fl.cells[2].h, 0.001);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[1].w, fl.cells[2].w, 0.01);
|
||||||
|
PLOT_CHECK_NEAR(fl.col_gap, 10.0, 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cell_weights_and_floor) {
|
||||||
|
plot::Figure figure = split_figure({2});
|
||||||
|
figure.set_col_gap(0.0f);
|
||||||
|
const std::vector<plot::f32> w{3.0f, 1.0f};
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(0u, w));
|
||||||
|
PLOT_CHECK(figure.has_cell_weights(0u));
|
||||||
|
{
|
||||||
|
const auto& c = figure.layout().cells;
|
||||||
|
PLOT_CHECK_NEAR((c[0].w - 64.0), (c[1].w - 64.0) * 3.0, 0.01);
|
||||||
|
}
|
||||||
|
const std::vector<plot::f32> tiny{1000.0f, 0.001f};
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(0u, tiny));
|
||||||
|
PLOT_CHECK(figure.layout().cells[1].w >= 64.0f - 0.001f);
|
||||||
|
const std::vector<plot::f32> three{1.0f, 1.0f, 1.0f};
|
||||||
|
const std::vector<plot::f32> zero{1.0f, 0.0f};
|
||||||
|
PLOT_CHECK(!figure.set_cell_weights(0u, three));
|
||||||
|
PLOT_CHECK(!figure.set_cell_weights(0u, zero));
|
||||||
|
PLOT_CHECK(!figure.set_cell_weights(5u, w));
|
||||||
|
figure.clear_cell_weights(0u);
|
||||||
|
PLOT_CHECK(!figure.has_cell_weights(0u));
|
||||||
|
const auto& c = figure.layout().cells;
|
||||||
|
PLOT_CHECK_NEAR(c[0].w, c[1].w, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cells_narrow_degrades) {
|
||||||
|
plot::Figure figure = split_figure({4}, false);
|
||||||
|
figure.set_viewport(plot::Rect{0.0f, 0.0f, 20.0f, 100.0f});
|
||||||
|
for (plot::u32 i = 0; i < 4u; ++i) {
|
||||||
|
figure.plot(i).add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
||||||
|
figure.plot(i).add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||||
|
}
|
||||||
|
figure.set_col_gap(10.0f);
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
double sum = 0.0;
|
||||||
|
for (const plot::Rect& c : fl.cells) {
|
||||||
|
PLOT_CHECK(c.w >= 0.0f && c.h >= 0.0f);
|
||||||
|
PLOT_CHECK_NEAR(c.h, 100.0, 0.01);
|
||||||
|
sum += c.w;
|
||||||
|
}
|
||||||
|
PLOT_CHECK_NEAR(sum, 20.0, 0.01);
|
||||||
|
PLOT_CHECK_NEAR(fl.col_gap, 0.0, 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cells_share_x_link_and_crosshair) {
|
||||||
|
plot::Figure figure = split_figure({2, 1});
|
||||||
|
const plot::u32 group = figure.add_x_link();
|
||||||
|
for (plot::u32 i = 0; i < 3u; ++i) PLOT_CHECK(figure.link_x(i, group));
|
||||||
|
figure.plot(1u).set_x_range(0u, {3.0, 9.0});
|
||||||
|
figure.layout();
|
||||||
|
for (plot::u32 i = 0; i < 3u; ++i) {
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(i).x_range(0u).min, 3.0, 1e-9);
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(i).x_range(0u).max, 9.0, 1e-9);
|
||||||
|
}
|
||||||
|
figure.set_crosshair(4.0, true);
|
||||||
|
for (plot::u32 i = 0; i < 3u; ++i) {
|
||||||
|
PLOT_CHECK(figure.plot(i).crosshair().active);
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(i).crosshair().x, 4.0, 1e-9);
|
||||||
|
}
|
||||||
|
figure.clear_crosshair();
|
||||||
|
for (plot::u32 i = 0; i < 3u; ++i) PLOT_CHECK(!figure.plot(i).crosshair().active);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cells_x_labels_bottom_row_only) {
|
||||||
|
plot::Figure figure = split_figure({1, 2});
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
PLOT_CHECK(figure.plot(0u).layout().x_label_area.h == 0.0f);
|
||||||
|
PLOT_CHECK(figure.plot(1u).layout().x_label_area.h > 0.0f);
|
||||||
|
PLOT_CHECK(figure.plot(2u).layout().x_label_area.h > 0.0f);
|
||||||
|
PLOT_CHECK_NEAR(fl.x_axis_strip.h, figure.plot(2u).layout().x_label_area.h, 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cells_common_y_gutter) {
|
||||||
|
plot::Figure figure = split_figure({2}, false);
|
||||||
|
give_magnitude_row(figure.plot(0u), 0u);
|
||||||
|
give_magnitude_row(figure.plot(1u), 1u);
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
const double g0 = figure.plot(0u).layout().axes.x - fl.cells[0].x;
|
||||||
|
const double g1 = figure.plot(1u).layout().axes.x - fl.cells[1].x;
|
||||||
|
PLOT_CHECK(g0 > 0.0);
|
||||||
|
PLOT_CHECK_NEAR(g0, g1, 0.01);
|
||||||
|
}
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
#include <glfw/glfw_host.hpp>
|
#include <glfw/glfw_host.hpp>
|
||||||
#include <plot/plot/figure.hpp>
|
#include <plot/plot/figure.hpp>
|
||||||
|
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
/// A figure of `rows` rows over a known rect, with an x and a y axis in every row (so
|
/// A figure of `rows` rows over a known rect, with an x and a y axis in every row (so
|
||||||
@@ -315,3 +318,31 @@ PLOT_TEST(the_host_keeps_its_own_controller_while_no_figure_is_set) {
|
|||||||
PLOT_CHECK(host.row_controller(0u) == nullptr);
|
PLOT_CHECK(host.row_controller(0u) == nullptr);
|
||||||
PLOT_CHECK(&host.controller_at(10.0f, 10.0f) == &host.controller());
|
PLOT_CHECK(&host.controller_at(10.0f, 10.0f) == &host.controller());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(glfw_host_routes_by_cell) {
|
||||||
|
// A figure of two rows, the first of one cell and the second of two side by side: the
|
||||||
|
// pointer's x as well as its y picks the plot, and the controllers are indexed by the
|
||||||
|
// flattened plot.
|
||||||
|
std::vector<plot::u32> cells{1, 2};
|
||||||
|
plot::Figure figure{std::span<const plot::u32>(cells)};
|
||||||
|
figure.set_viewport(plot::Rect{0, 0, 300, 600});
|
||||||
|
figure.set_col_gap(6.0f);
|
||||||
|
for (plot::u32 i = 0; i < 3; ++i) {
|
||||||
|
figure.plot(i).add_x_axis({"t", {0.0, 1.0}, false, plot::AxisPlace::Bottom, false});
|
||||||
|
figure.plot(i).add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||||
|
}
|
||||||
|
plot::glfw::Host host;
|
||||||
|
host.set_figure(&figure);
|
||||||
|
const std::vector<plot::Rect> c = figure.layout().cells;
|
||||||
|
PLOT_CHECK(c.size() == 3u);
|
||||||
|
for (plot::u32 i = 0; i < 3; ++i) {
|
||||||
|
PLOT_CHECK(host.row_controller(i) != nullptr);
|
||||||
|
const double cx = c[i].x + c[i].w * 0.5, cy = c[i].y + c[i].h * 0.5;
|
||||||
|
PLOT_CHECK(&host.controller_at(static_cast<plot::f32>(cx), static_cast<plot::f32>(cy)) ==
|
||||||
|
host.row_controller(i));
|
||||||
|
}
|
||||||
|
PLOT_CHECK(host.row_controller(3u) == nullptr);
|
||||||
|
// in the column gap: the nearest cell wins
|
||||||
|
PLOT_CHECK(&host.controller_at(c[1].right() + 1.0f, c[1].y + 5.0f) == host.row_controller(1));
|
||||||
|
PLOT_CHECK(&host.controller_at(c[2].x - 1.0f, c[2].y + 5.0f) == host.row_controller(2));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user