Skip to content

Commit 1fe34f5

Browse files
committed
src/qml/controls/subtitletext: render text layer using a text edit
Renders the text layer using a TextEdit instead of a PathText. Positioning isn't perfect at all sizes, but it is within a subpixel from what I can tell which is good enough. The best part of this is that there is no aliasing at all on the text layer and selection is handled by the text edit rather than a path greatly simplifying code. This also moves the TextEdit out of the shape since I don't want the TextEdit to have any MSAA applied. Player is now given the green light to position the subtitles across subpixels again.
1 parent bc4c320 commit 1fe34f5

3 files changed

Lines changed: 99 additions & 119 deletions

File tree

src/qml/controls/Player.qml

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -465,19 +465,23 @@ MpvPlayer {
465465
root.controller.setSubtitleVisibility(!subtitleText.visible);
466466
}
467467

468-
anchors.horizontalCenter: root.horizontalCenter
469-
/* Makes sure UI elements don't obscure the subtitles or that they go offscreen */
470-
y: {
471-
let minValue = controls.visible ? controls.height : 0;
472-
let maxValue = root.height - subtitleText.height;
473-
if (Features.platform !== Features.MacOS && menu.visible)
474-
{
475-
maxValue -= menu.height;
468+
anchors {
469+
horizontalCenter: root.horizontalCenter
470+
bottom: root.bottom
471+
472+
/* Makes sure UI elements don't obscure the subtitles or that they go offscreen */
473+
bottomMargin: {
474+
let minValue = controls.visible ? controls.height : 0;
475+
let maxValue = root.height - subtitleText.height;
476+
if (Features.platform !== Features.MacOS && menu.visible)
477+
{
478+
maxValue -= menu.height;
479+
}
480+
let margin = root.height * MementoSettings.interfaceSubtitleOffset;
481+
margin = Math.max(margin, minValue);
482+
margin = Math.min(margin, maxValue);
483+
return margin;
476484
}
477-
let margin = root.height * MementoSettings.interfaceSubtitleOffset;
478-
margin = Math.max(margin, minValue);
479-
margin = Math.min(margin, maxValue);
480-
return Math.round(root.height - (subtitleText.height + margin));
481485
}
482486

483487
/* Prevents text from being wider than the window */

src/qml/controls/StrokeLabel.qml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Item {
1212
property real strokeSize: 1.0
1313
property real lineSpacing: 0
1414

15-
readonly property real margin: root.strokeSize / 2
15+
readonly property int margin: Math.ceil(root.strokeSize / 2.0)
1616

1717
/**
1818
* Creates a JSON model for a given string where each \n is it's own object.

src/qml/controls/SubtitleText.qml

Lines changed: 82 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Item {
1919
readonly property string selectedText: root.text.substring(
2020
root.selectionStart, root.selectionEnd)
2121

22-
readonly property real margin: root.strokeSize / 2
22+
readonly property int margin: Math.ceil(root.strokeSize / 2.0)
2323

2424
signal clicked()
2525
signal doubleClicked()
@@ -142,131 +142,107 @@ Item {
142142
id: repeaterShape
143143

144144
model: root.makeTextModel(root.text)
145-
delegate: Shape {
146-
id: shape
145+
delegate: Item {
146+
id: delegateItem
147147
anchors.horizontalCenter: parent.horizontalCenter
148-
antialiasing: true
149-
layer.enabled: true
150-
layer.samples: 4
151-
layer.smooth: true
152-
153-
// This draws the background
154-
ShapePath {
155-
fillColor: root.background
156-
strokeColor: "transparent"
157-
strokeWidth: 0
158-
159-
PathRectangle {
160-
x: 0
161-
y: 0
162-
width: shape.width
163-
height: shape.height
164-
}
165-
}
166-
167-
// This draws the text stroke
168-
ShapePath {
169-
strokeWidth: root.strokeSize
170-
strokeColor: root.stroke
171-
fillColor: "transparent"
172-
fillRule: ShapePath.WindingFill
173-
joinStyle: ShapePath.RoundJoin
174-
capStyle: ShapePath.RoundCap
175-
176-
PathText {
177-
id: pathTextStroke
178-
x: root.margin
179-
y: root.margin
180-
font: root.font
181-
text: modelData.text
182-
}
148+
width: Math.max(shape.width, textMetrics.tightBoundingRect.width)
149+
height: Math.max(shape.height, textMetrics.tightBoundingRect.height)
150+
clip: true
151+
152+
TextMetrics {
153+
id: textMetrics
154+
font: textEdit.font
155+
text: textEdit.text
156+
renderType: textEdit.renderType
183157
}
184158

185-
// This draws the selection if there is one
186-
ShapePath {
187-
id: selectionPath
188-
189-
strokeWidth: 0
190-
fillColor: MementoPalette.highlight
191-
192-
readonly property rect area: selectionPath.getSelectionBounds(
193-
root.selectionStart, root.selectionEnd)
194-
195-
/**
196-
* Returns a rectangle of the area to draw.
197-
* @param start The starting index in root.text (inclusive)
198-
* @param end The ending index in root.text (exclusive)
199-
* @return A rect representing the area to draw on this line
200-
*/
201-
function getSelectionBounds(start, end) {
202-
const minIndex = modelData.offset;
203-
const maxIndex = minIndex + modelData.text.length;
204-
205-
const overlap = minIndex <= end && start <= maxIndex;
206-
if (!overlap)
207-
{
208-
return Qt.rect(0, 0, 0, 0);
159+
Shape {
160+
id: shape
161+
antialiasing: true
162+
layer.enabled: true
163+
layer.samples: 4
164+
layer.smooth: true
165+
166+
// This draws the background
167+
ShapePath {
168+
fillColor: root.background
169+
strokeColor: "transparent"
170+
strokeWidth: 0
171+
172+
PathRectangle {
173+
x: 0
174+
y: 0
175+
width: shape.width
176+
height: shape.height
209177
}
210-
211-
start = Math.max(start - minIndex, 0);
212-
end = Math.min(end - minIndex, modelData.text.length);
213-
if (start >= end)
214-
{
215-
return Qt.rect(0, 0, 0, 0);
216-
}
217-
218-
const startRect = textEdit.positionToRectangle(start);
219-
const endRect = textEdit.positionToRectangle(end);
220-
221-
const startX = root.margin + startRect.x;
222-
const endX = root.margin + endRect.x + endRect.width;
223-
const startY = 0
224-
const endY = shape.height;
225-
return Qt.rect(startX, startY, endX - startX, endY - startY);
226-
}
227-
228-
PathRectangle {
229-
x: selectionPath.area.x
230-
y: selectionPath.area.y
231-
width: selectionPath.area.width
232-
height: selectionPath.area.height
233178
}
234-
}
235-
236-
// This fills in the text on top of the stroke layer
237-
ShapePath {
238-
strokeWidth: 0
239-
fillColor: root.color
240-
fillRule: ShapePath.WindingFill
241179

242-
PathText {
243-
id: pathTextFill
244-
x: root.margin
245-
y: root.margin
246-
font: root.font
247-
text: modelData.text
180+
// This draws the text stroke
181+
ShapePath {
182+
strokeWidth: root.strokeSize
183+
strokeColor: root.stroke
184+
fillColor: "transparent"
185+
fillRule: ShapePath.WindingFill
186+
joinStyle: ShapePath.RoundJoin
187+
capStyle: ShapePath.RoundCap
188+
189+
PathText {
190+
id: pathTextStroke
191+
x: root.margin
192+
y: root.margin
193+
font: root.font
194+
text: modelData.text
195+
}
248196
}
249197
}
250198

251-
// This text edit is only used for hit testing, it is not shown
252199
TextEdit {
253200
id: textEdit
254201

255202
x: root.margin
256203
y: root.margin - (textMetrics.tightBoundingRect.y - textMetrics.boundingRect.y)
257204
font: root.font
258-
color: "transparent"
205+
color: root.color
259206
readOnly: true
260207
selectByMouse: false
261208
selectByKeyboard: false
209+
selectionColor: MementoPalette.highlight
262210
wrapMode: TextEdit.NoWrap
263211
text: modelData.text
264212

265-
TextMetrics {
266-
id: textMetrics
267-
font: textEdit.font
268-
text: textEdit.text
269-
renderType: textEdit.renderType
213+
Connections {
214+
target: root
215+
function onSelectionStartChanged() {
216+
textEdit.updateSelection();
217+
}
218+
}
219+
Connections {
220+
target: root
221+
function onSelectionEndChanged() {
222+
textEdit.updateSelection();
223+
}
224+
}
225+
226+
/**
227+
* Update the selection with the new selectionStart and selectionEnd values.
228+
*/
229+
function updateSelection() {
230+
if (root.selectionStart < 0 || root.selectionEnd < 0)
231+
{
232+
textEdit.deselect();
233+
return;
234+
}
235+
236+
let start = root.selectionStart - modelData.offset;
237+
start = Math.max(start, 0);
238+
let end = root.selectionEnd - modelData.offset;
239+
end = Math.min(end, textEdit.text.length);
240+
if (start >= end)
241+
{
242+
textEdit.deselect();
243+
return;
244+
}
245+
textEdit.select(start, end);
270246
}
271247

272248
/**
@@ -291,7 +267,7 @@ Item {
291267
cursorShape: root.cursorShape
292268
onPositionChanged: function(mouse) {
293269
let x = mouse.x - root.margin;
294-
if (x >= 0)
270+
if (x >= 0 && x < textEdit.width)
295271
{
296272
let index = textEdit.getCharacterIndexAt(x);
297273
root.hoverIndex = index;

0 commit comments

Comments
 (0)