fix(gtk4): derive the device-pixel factor from GTK's two sizes, and four more review fixes
The factor both the viewport and every forwarded input coordinate use came from `gtk_widget_get_scale_factor()`, which GTK documents as "the next higher integer value" under fractional scaling. It is now derived by `DeviceMapping::from_sizes()` from the widget's logical allocation and the device size `GtkGLArea::resize` reports -- the viewport that signal's default handler sets with `glViewport(0, 0, w, h)` -- so the viewport, the renderer and input are one value by construction. The public `int scale_factor()` became `f32 device_scale()`. `kSurfacePixelsPerNotch` is now 100.0 and documented as a *calibration* with a stated assumption, not a unit conversion attributed to axis_value120/Qt. `::unrealize` releases the renderer unconditionally, after making the context current: the error it used to check says nothing about whether GL names exist (creation failure is the path that returns before a renderer is built), and skipping the release left the deletes to the wrapper's destructor, with no context. The drag anchor caches its press point and offsets in logical pixels and scales the difference by the factor in force now, so a scale change mid-drag cannot become a phantom pan. The demo stops its producer in `on_teardown`, before destroying the window whose renderer the stream feeds.
This commit is contained in:
1 parent
36cd3421f3
commit
0445a1861b
6 files changed
+417
-137
No files matched your search
@@ -91,28 +91,19 @@ u32 gesture_mods(GtkEventController* controller) noexcept {
|
||||
return mods_from(static_cast<u32>(gtk_event_controller_get_current_event_state(controller)));
|
||||
}
|
||||
|
||||
/// The position of the event a controller is handling right now, in device
|
||||
/// pixels. False when there is no event or it has no position -- a key event with
|
||||
/// a scroll controller's anchor is the case that matters.
|
||||
bool current_event_position(GtkEventController* controller, f32 scale, Vec2* out) noexcept {
|
||||
/// The position of the event a controller is handling right now, in device pixels
|
||||
/// at `mapping`. False when there is no event or it has no position -- a key event
|
||||
/// with a scroll controller's anchor is the case that matters.
|
||||
bool current_event_position(GtkEventController* controller, const DeviceMapping& mapping,
|
||||
Vec2* out) noexcept {
|
||||
GdkEvent* event = gtk_event_controller_get_current_event(controller);
|
||||
if (event == nullptr) return false;
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
if (!gdk_event_get_position(event, &x, &y)) return false;
|
||||
*out = Vec2{static_cast<f32>(x) * scale, static_cast<f32>(y) * scale};
|
||||
*out = mapping.to_device(x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The widget's allocation in device pixels: what the plot is given, what GTK
|
||||
/// attaches as the framebuffer, and what `GtkGLArea::resize` reports. Zero-sized
|
||||
/// before the widget is allocated.
|
||||
Rect alloc_in_device(GtkWidget* widget) noexcept {
|
||||
const f32 scale = static_cast<f32>(gtk_widget_get_scale_factor(widget));
|
||||
return Rect{0.0f, 0.0f, static_cast<f32>(gtk_widget_get_width(widget)) * scale,
|
||||
static_cast<f32>(gtk_widget_get_height(widget)) * scale};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PlotArea* PlotArea::create(Plot& plot) noexcept {
|
||||
@@ -190,16 +181,15 @@ PlotArea* PlotArea::create(Plot& plot) noexcept {
|
||||
PlotArea::PlotArea(Plot& plot, GtkWidget* widget) noexcept
|
||||
: plot_(&plot), controller_(plot), widget_(widget) {}
|
||||
|
||||
Rect PlotArea::device_viewport() const noexcept { return alloc_in_device(widget_); }
|
||||
Rect PlotArea::device_viewport() const noexcept { return mapping_.viewport; }
|
||||
|
||||
int PlotArea::scale_factor() const noexcept { return gtk_widget_get_scale_factor(widget_); }
|
||||
f32 PlotArea::device_scale() const noexcept { return mapping_.factor; }
|
||||
|
||||
Vec2 PlotArea::to_device(double x, double y) const noexcept {
|
||||
// The factor `device_viewport()` scales the widget by, applied to a position and
|
||||
// to the offsets derived from two of them: one convention on both sides is what
|
||||
// The mapping `device_viewport()` is the viewport of, applied to a position and
|
||||
// to the offsets derived from two of them: one mapping on both sides is what
|
||||
// makes a gesture land on the data under the cursor.
|
||||
const f32 scale = static_cast<f32>(gtk_widget_get_scale_factor(widget_));
|
||||
return Vec2{static_cast<f32>(x) * scale, static_cast<f32>(y) * scale};
|
||||
return mapping_.to_device(x, y);
|
||||
}
|
||||
|
||||
void PlotArea::refresh_viewport() noexcept {
|
||||
@@ -270,16 +260,22 @@ void PlotArea::realize() noexcept {
|
||||
}
|
||||
|
||||
void PlotArea::unrealize() noexcept {
|
||||
// The context is still alive here and can still be made current (GTK clears it
|
||||
// after this signal), which is what the renderer's RAII handles need: a
|
||||
// `glDelete*` against a context that is going away -- or against no context at
|
||||
// all -- is worse than the leak it would avoid.
|
||||
if (renderer_) {
|
||||
gtk_gl_area_make_current(GTK_GL_AREA(widget_));
|
||||
if (gtk_gl_area_get_error(GTK_GL_AREA(widget_)) == nullptr) renderer_.reset();
|
||||
}
|
||||
// Made current first: the renderer's RAII handles issue `glDelete*` from their
|
||||
// destructors, and GTK clears the context in its own unrealize vfunc, which runs
|
||||
// after this signal's handlers -- so this is the last moment at which those
|
||||
// deletes can name a live context.
|
||||
gtk_gl_area_make_current(GTK_GL_AREA(widget_));
|
||||
// Released unconditionally. The area's error says nothing about whether the
|
||||
// renderer holds names to delete: it is set when context *creation* failed, and
|
||||
// that is the path `realize()` returns from before it builds a renderer, so a
|
||||
// renderer present here has GL objects behind it. Leaving it for the wrapper's
|
||||
// destructor -- which runs at widget finalisation, after GTK has freed the
|
||||
// context -- would issue those deletes with nothing current, which is exactly
|
||||
// what the old error check was trying to avoid. `make_current()` and `reset()`
|
||||
// are no-ops on a widget that never got a context and on an empty renderer.
|
||||
renderer_.reset();
|
||||
press_open_ = false;
|
||||
drag_active_ = false;
|
||||
drag_.have = false;
|
||||
}
|
||||
|
||||
void PlotArea::render() noexcept {
|
||||
@@ -341,16 +337,20 @@ int PlotArea::on_render(GtkGLArea*, GdkGLContext*, void* self) noexcept {
|
||||
}
|
||||
|
||||
void PlotArea::on_resize(GtkGLArea*, int width, int height, void* self) noexcept {
|
||||
(void)width;
|
||||
(void)height;
|
||||
// Nothing about the size is cached from these two, on purpose: device_viewport()
|
||||
// re-derives it from the live allocation, which is what the input mapping reads
|
||||
// as well. GTK's numbers are the same ones -- it reports the GL viewport in
|
||||
// device pixels, and this area's `width * scale` is that -- but one derivation
|
||||
// is what keeps a gesture, a frame and a screenshot from ever disagreeing.
|
||||
// Refreshing it here means a gesture arriving between the resize and the next
|
||||
// frame maps against the new geometry rather than the old one.
|
||||
static_cast<PlotArea*>(self)->refresh_viewport();
|
||||
auto* area = static_cast<PlotArea*>(self);
|
||||
// GTK's two sizes, and the one place the mapping is derived from them. `width`
|
||||
// and `height` are the device pixels of the viewport this signal's default
|
||||
// handler is about to set -- `glViewport(0, 0, width, height)` in GTK's own
|
||||
// `gtk_gl_area_resize()` -- and the widget's width and height, which GTK has
|
||||
// already updated when it emits this, are the logical pixels that same rectangle
|
||||
// is reported in. Their ratio is the factor for the viewport *and* for every
|
||||
// forwarded coordinate; refreshing here means a gesture arriving between this
|
||||
// resize and the next frame maps against the new geometry rather than the old.
|
||||
area->mapping_ = DeviceMapping::from_sizes(
|
||||
Vec2{static_cast<f32>(gtk_widget_get_width(area->widget_)),
|
||||
static_cast<f32>(gtk_widget_get_height(area->widget_))},
|
||||
Vec2{static_cast<f32>(width), static_cast<f32>(height)});
|
||||
area->refresh_viewport();
|
||||
}
|
||||
|
||||
// --- pointer input --------------------------------------------------------
|
||||
@@ -425,16 +425,14 @@ void PlotArea::on_click_stopped(GtkGestureClick* gesture, void* self) noexcept {
|
||||
// drag in progress (an exotic cancellation by another widget or a device) closes
|
||||
// the press, which it has to, or the controller would stay in whatever mode the
|
||||
// press began.
|
||||
if (area->press_open_ && !area->drag_active_) area->pointer_release(area->pointer_);
|
||||
if (area->press_open_ && !area->drag_.have) area->pointer_release(area->pointer_);
|
||||
}
|
||||
|
||||
void PlotArea::on_drag_begin(GtkGestureDrag* gesture, double start_x, double start_y,
|
||||
void* self) noexcept {
|
||||
auto* area = static_cast<PlotArea*>(self);
|
||||
area->drag_active_ = true;
|
||||
area->drag_start_ = area->to_device(start_x, start_y);
|
||||
area->drag_offset_ = Vec2{0.0f, 0.0f};
|
||||
area->pointer_ = area->drag_start_;
|
||||
area->drag_.begin(static_cast<f32>(start_x), static_cast<f32>(start_y));
|
||||
area->pointer_ = area->drag_.position(area->mapping_);
|
||||
area->have_pointer_ = true;
|
||||
|
||||
// GTK emits `drag-begin` before the click gesture's `pressed` (both while the
|
||||
@@ -458,21 +456,20 @@ void PlotArea::on_drag_update(GtkGestureDrag* gesture, double offset_x, double o
|
||||
// (see button_from): its motion must not reach the controller as a held-drag
|
||||
// move, which is the one event the controller acts on from its button state.
|
||||
if (!area->press_open_) return;
|
||||
const Vec2 offset = area->to_device(offset_x, offset_y);
|
||||
// The drag gesture reports the offset from the press point, the controller wants
|
||||
// the movement since the previous sample: the difference of two offsets, both
|
||||
// already scaled by the same factor as the viewport and the press.
|
||||
const Vec2 delta = Vec2{offset.x - area->drag_offset_.x, offset.y - area->drag_offset_.y};
|
||||
area->drag_offset_ = offset;
|
||||
const Vec2 position = Vec2{area->drag_start_.x + offset.x, area->drag_start_.y + offset.y};
|
||||
area->pointer_ = position;
|
||||
// The anchor stays logical (see `DragAnchor`): the gesture reports the offset
|
||||
// from the press point in logical pixels, and this is a logical difference
|
||||
// scaled by the factor in force *now*, so a factor change between two samples
|
||||
// cannot turn into a phantom movement of its own.
|
||||
const DragAnchor::Sample sample =
|
||||
area->drag_.update(offset_x, offset_y, area->mapping_);
|
||||
area->pointer_ = sample.position;
|
||||
area->have_pointer_ = true;
|
||||
|
||||
input::PointerEvent e;
|
||||
e.x = position.x;
|
||||
e.y = position.y;
|
||||
e.dx = delta.x;
|
||||
e.dy = delta.y;
|
||||
e.x = sample.position.x;
|
||||
e.y = sample.position.y;
|
||||
e.dx = sample.delta.x;
|
||||
e.dy = sample.delta.y;
|
||||
e.button = area->press_button_;
|
||||
e.mods = area->press_mods_;
|
||||
e.down = true;
|
||||
@@ -484,13 +481,13 @@ void PlotArea::on_drag_end(GtkGestureDrag* gesture, double offset_x, double offs
|
||||
void* self) noexcept {
|
||||
(void)gesture;
|
||||
auto* area = static_cast<PlotArea*>(self);
|
||||
area->drag_active_ = false;
|
||||
// The release carries the authoritative end of the gesture: the controller reads
|
||||
// its position to finish a box zoom's selection, so this is the press point plus
|
||||
// the final offset and not the last update's.
|
||||
const Vec2 offset = area->to_device(offset_x, offset_y);
|
||||
area->pointer_ = Vec2{area->drag_start_.x + offset.x, area->drag_start_.y + offset.y};
|
||||
area->pointer_release(area->pointer_);
|
||||
// the offset `drag-end` itself reports -- which may be past the last update's --
|
||||
// and not the last update's position.
|
||||
const Vec2 position = area->drag_.end(offset_x, offset_y, area->mapping_);
|
||||
area->pointer_ = position;
|
||||
area->pointer_release(position);
|
||||
}
|
||||
|
||||
int PlotArea::on_scroll(GtkEventControllerScroll* scroll, double dx, double dy,
|
||||
@@ -508,9 +505,8 @@ int PlotArea::on_scroll(GtkEventControllerScroll* scroll, double dx, double dy,
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
const f32 scale = static_cast<f32>(gtk_widget_get_scale_factor(area->widget_));
|
||||
Vec2 at{};
|
||||
if (!current_event_position(GTK_EVENT_CONTROLLER(scroll), scale, &at)) {
|
||||
if (!current_event_position(GTK_EVENT_CONTROLLER(scroll), area->mapping_, &at)) {
|
||||
if (!area->have_pointer_) return FALSE; // no anchor to zoom about
|
||||
at = area->pointer_; // the last position a pointer event reported
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user