gnome-shell/js/ui/slider.js
Sebastian Keller 7fc7724e85 slider: Align handle size with with pixel grid
Since the handle radius is used to calculate the width and height of the
slider, having the calculated size be a non-integer value can cause the
following widgets in a box-like container to be unaligned with the pixel
grid, which can lead to blurriness or other visual issues.

This for example could be observed with the interface font set to
"Cantarell 12", which results in a handle radius of 8.278. In quick
settings when showing two consecutive sliders, the second slider then
gets rendered at a non-integer vertical offset.

Further having a non-integer size for a StDrawingArea can cause the
texture to get slightly squished or stretched as the size of the cairo
surface is rounded to the nearest pixel, but rendered using the
unrounded actor size.

This commit changes the border radius rather than ceiling the preferred
width/height so that the handle size always matches the width or height
of the widget and there are no visual gaps caused by a partially filled
pixel.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3648>
2025-04-28 15:52:14 +00:00

229 lines
6.7 KiB
JavaScript

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import * as BarLevel from './barLevel.js';
const SLIDER_SCROLL_STEP = 0.02; /* Slider scrolling step in % */
export const Slider = GObject.registerClass({
Signals: {
'drag-begin': {},
'drag-end': {},
},
}, class Slider extends BarLevel.BarLevel {
_init(value) {
super._init({
value,
style_class: 'slider',
can_focus: true,
reactive: true,
track_hover: true,
hover: false,
accessible_role: Atk.Role.SLIDER,
x_expand: true,
});
this._releaseId = 0;
this._dragging = false;
this._handleRadius = 0;
this._customAccessible.connect('get-minimum-increment', this._getMinimumIncrement.bind(this));
}
vfunc_style_changed() {
super.vfunc_style_changed();
const themeNode = this.get_theme_node();
this._handleRadius =
Math.round(2 * themeNode.get_length('-slider-handle-radius')) / 2;
}
vfunc_repaint() {
super.vfunc_repaint();
// Add handle
let cr = this.get_context();
let themeNode = this.get_theme_node();
let [width, height] = this.get_surface_size();
const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
const handleY = height / 2;
let handleX = this._handleRadius +
(width - 2 * this._handleRadius) * this._value / this._maxValue;
if (rtl)
handleX = width - handleX;
let color = themeNode.get_foreground_color();
cr.setSourceColor(color);
cr.arc(handleX, handleY, this._handleRadius, 0, 2 * Math.PI);
cr.fillPreserve();
cr.$dispose();
}
_getPreferredHeight() {
const barHeight = super._getPreferredHeight();
const handleHeight = 2 * this._handleRadius;
return Math.max(barHeight, handleHeight);
}
_getPreferredWidth() {
const barWidth = super._getPreferredWidth();
const handleWidth = 2 * this._handleRadius;
return Math.max(barWidth, handleWidth);
}
vfunc_button_press_event(event) {
return this.startDragging(event);
}
startDragging(event) {
if (this._dragging)
return Clutter.EVENT_PROPAGATE;
this._dragging = true;
let device = event.get_device();
let sequence = event.get_event_sequence();
this._grab = global.stage.grab(this);
this._grabbedDevice = device;
this._grabbedSequence = sequence;
// We need to emit 'drag-begin' before moving the handle to make
// sure that no 'notify::value' signal is emitted before this one.
this.emit('drag-begin');
let absX, absY;
[absX, absY] = event.get_coords();
this._moveHandle(absX, absY);
return Clutter.EVENT_STOP;
}
_endDragging() {
if (this._dragging) {
if (this._releaseId) {
this.disconnect(this._releaseId);
this._releaseId = 0;
}
if (this._grab) {
this._grab.dismiss();
this._grab = null;
}
this._grabbedSequence = null;
this._grabbedDevice = null;
this._dragging = false;
this.emit('drag-end');
}
return Clutter.EVENT_STOP;
}
vfunc_button_release_event() {
if (this._dragging && !this._grabbedSequence)
return this._endDragging();
return Clutter.EVENT_PROPAGATE;
}
vfunc_touch_event(event) {
let sequence = event.get_event_sequence();
if (!this._dragging &&
event.type() === Clutter.EventType.TOUCH_BEGIN) {
this.startDragging(event);
return Clutter.EVENT_STOP;
} else if (this._grabbedSequence &&
sequence.get_slot() === this._grabbedSequence.get_slot()) {
if (event.type() === Clutter.EventType.TOUCH_UPDATE)
return this._motionEvent(this, event);
else if (event.type() === Clutter.EventType.TOUCH_END)
return this._endDragging();
}
return Clutter.EVENT_PROPAGATE;
}
scroll(event) {
let direction = event.get_scroll_direction();
let delta = 0;
if (event.get_flags() & Clutter.EventFlags.FLAG_POINTER_EMULATED)
return Clutter.EVENT_PROPAGATE;
if (direction === Clutter.ScrollDirection.DOWN) {
delta = -SLIDER_SCROLL_STEP;
} else if (direction === Clutter.ScrollDirection.UP) {
delta = SLIDER_SCROLL_STEP;
} else if (direction === Clutter.ScrollDirection.SMOOTH) {
let [, dy] = event.get_scroll_delta();
// Even though the slider is horizontal, use dy to match
// the UP/DOWN above.
delta = -dy * SLIDER_SCROLL_STEP;
}
this.value = Math.min(Math.max(0, this._value + delta), this._maxValue);
return Clutter.EVENT_STOP;
}
vfunc_scroll_event(event) {
return this.scroll(event);
}
vfunc_motion_event(event) {
if (this._dragging && !this._grabbedSequence)
return this._motionEvent(this, event);
return Clutter.EVENT_PROPAGATE;
}
_motionEvent(actor, event) {
let absX, absY;
[absX, absY] = event.get_coords();
this._moveHandle(absX, absY);
return Clutter.EVENT_STOP;
}
vfunc_key_press_event(event) {
let key = event.get_key_symbol();
if (key === Clutter.KEY_Right || key === Clutter.KEY_Left) {
const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
const increaseKey = rtl ? Clutter.KEY_Left : Clutter.KEY_Right;
const delta = key === increaseKey ? 0.1 : -0.1;
this.value = Math.max(0, Math.min(this._value + delta, this._maxValue));
return Clutter.EVENT_STOP;
}
return super.vfunc_key_press_event(event);
}
_moveHandle(absX, _absY) {
let relX, sliderX;
[sliderX] = this.get_transformed_position();
const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
let width = this._barLevelWidth;
relX = absX - sliderX;
if (rtl)
relX = width - relX;
let newvalue;
if (relX < this._handleRadius)
newvalue = 0;
else if (relX > width - this._handleRadius)
newvalue = 1;
else
newvalue = (relX - this._handleRadius) / (width - 2 * this._handleRadius);
this.value = newvalue * this._maxValue;
}
_getMinimumIncrement() {
return 0.1;
}
});