50
50
51
51
namespace Rml {
52
52
53
- static constexpr float CURSOR_BLINK_TIME = 0 .7f ; // [s]
54
- static constexpr float OVERFLOW_TOLERANCE = 0 .5f ; // [px]
53
+ static constexpr float CURSOR_BLINK_TIME = 0 .7f ; // [s]
54
+ static constexpr float OVERFLOW_TOLERANCE = 0 .5f ; // [px]
55
+ static constexpr float COMPOSITION_UNDERLINE_WIDTH = 2 .f; // [px]
55
56
56
57
enum class CharacterClass { Word, Punctuation, Newline, Whitespace, Undefined };
57
58
static CharacterClass GetCharacterClass (char c)
@@ -193,6 +194,7 @@ WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) :
193
194
parent->SetProperty (PropertyId::Drag, Property (Style ::Drag::Drag));
194
195
parent->SetProperty (PropertyId::WordBreak, Property (Style ::WordBreak::BreakWord));
195
196
parent->SetProperty (PropertyId::TextTransform, Property (Style ::TextTransform::None));
197
+ parent->SetProperty (PropertyId::Clip, Property (Style ::Clip::Type::Auto));
196
198
197
199
parent->AddEventListener (EventId::Keydown, this , true );
198
200
parent->AddEventListener (EventId::Textinput, this , true );
@@ -242,6 +244,7 @@ WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) :
242
244
ime_composition_end_index = 0 ;
243
245
244
246
last_update_time = 0 ;
247
+ ink_overflow = false ;
245
248
246
249
ShowCursor (false );
247
250
}
@@ -467,8 +470,7 @@ void WidgetTextInput::OnRender()
467
470
ElementUtilities::SetClippingRegion (text_element);
468
471
469
472
Vector2f text_translation = parent->GetAbsoluteOffset () - Vector2f (parent->GetScrollLeft (), parent->GetScrollTop ());
470
- selection_geometry.Render (text_translation);
471
- ime_composition_geometry.Render (text_translation);
473
+ selection_composition_geometry.Render (text_translation);
472
474
473
475
if (cursor_visible && !parent->IsDisabled ())
474
476
{
@@ -1059,20 +1061,36 @@ int WidgetTextInput::CalculateLineIndex(float position) const
1059
1061
return Math::Clamp (line_index, 0 , (int )(lines.size () - 1 ));
1060
1062
}
1061
1063
1062
- float WidgetTextInput::GetAlignmentSpecificTextOffset (const char * p_begin, int line_index ) const
1064
+ float WidgetTextInput::GetAlignmentSpecificTextOffset (const Line& line ) const
1063
1065
{
1064
- const float available_width = GetAvailableWidth ();
1065
- const float total_width = (float )ElementUtilities::GetStringWidth (text_element, String (p_begin, lines[line_index].editable_length ));
1066
- auto text_align = GetElement ()->GetComputedValues ().text_align ();
1066
+ const String& value = GetValue ();
1067
+ StringView editable_line_string (value, line.value_offset , line.editable_length );
1068
+
1069
+ const Style ::TextAlign text_align = parent->GetComputedValues ().text_align ();
1070
+
1071
+ // Callback to avoid expensive calculation in the cases where it is not needed.
1072
+ auto RemainingWidth = [&] {
1073
+ const float total_width = (float )ElementUtilities::GetStringWidth (text_element, String (editable_line_string));
1074
+ return GetAvailableWidth () - total_width;
1075
+ };
1067
1076
1068
- // offset position depending on text align
1069
1077
switch (text_align)
1070
1078
{
1071
- case Style ::TextAlign::Right: return Math::Max (0 .0f , (available_width - total_width));
1072
- case Style ::TextAlign::Center: return Math::Max (0 .0f , ((available_width - total_width) / 2 ));
1073
- default : break ;
1079
+ case Style ::TextAlign::Left: return 0 ;
1080
+ case Style ::TextAlign::Right:
1081
+ {
1082
+ // For right alignment with soft-wrapped newlines, remove up to a single space to align the last word to the right edge.
1083
+ const bool is_last_line = (line.value_offset + line.size == (int )value.size ());
1084
+ const bool is_soft_wrapped = (!is_last_line && line.editable_length == line.size );
1085
+ if (is_soft_wrapped && editable_line_string.size () > 0 && *(editable_line_string.end () - 1 ) == ' ' )
1086
+ {
1087
+ editable_line_string = StringView (editable_line_string.begin (), editable_line_string.end () - 1 );
1088
+ }
1089
+ return Math::Max (0 .0f , RemainingWidth ());
1090
+ }
1091
+ case Style ::TextAlign::Center: return Math::Max (0 .0f , 0 .5f * RemainingWidth ());
1092
+ case Style ::TextAlign::Justify: return 0 ;
1074
1093
}
1075
-
1076
1094
return 0 ;
1077
1095
}
1078
1096
@@ -1083,11 +1101,12 @@ int WidgetTextInput::CalculateCharacterIndex(int line_index, float position)
1083
1101
1084
1102
ideal_cursor_position_to_the_right_of_cursor = true ;
1085
1103
1086
- const char * p_begin = GetValue ().data () + lines[line_index].value_offset ;
1104
+ const Line& line = lines[line_index];
1105
+ const char * p_begin = GetValue ().data () + line.value_offset ;
1087
1106
1088
- position -= GetAlignmentSpecificTextOffset (p_begin, line_index );
1107
+ position -= GetAlignmentSpecificTextOffset (line );
1089
1108
1090
- for (auto it = StringIteratorU8 (p_begin, p_begin, p_begin + lines[line_index] .editable_length ); it;)
1109
+ for (auto it = StringIteratorU8 (p_begin, p_begin, p_begin + line .editable_length ); it;)
1091
1110
{
1092
1111
++it;
1093
1112
const int offset = (int )it.offset ();
@@ -1130,8 +1149,11 @@ void WidgetTextInput::ShowCursor(bool show, bool move_to_cursor)
1130
1149
else if (parent->GetScrollTop () > cursor_position.y )
1131
1150
parent->SetScrollTop (cursor_position.y );
1132
1151
1152
+ const bool word_wrap = parent->GetComputedValues ().white_space () == Style ::WhiteSpace::Prewrap;
1133
1153
float minimum_scroll_left = (cursor_position.x + cursor_size.x ) - GetAvailableWidth ();
1134
- if (parent->GetScrollLeft () < minimum_scroll_left)
1154
+ if (word_wrap)
1155
+ parent->SetScrollLeft (0 .f );
1156
+ else if (parent->GetScrollLeft () < minimum_scroll_left)
1135
1157
parent->SetScrollLeft (minimum_scroll_left);
1136
1158
else if (parent->GetScrollLeft () > cursor_position.x )
1137
1159
parent->SetScrollLeft (cursor_position.x );
@@ -1231,25 +1253,10 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
1231
1253
Vector2f line_position = {0 , top_to_baseline};
1232
1254
bool last_line = false ;
1233
1255
1234
- auto text_align = GetElement ()->GetComputedValues ().text_align ();
1235
-
1236
- struct Segment {
1237
- Vector2f position;
1238
- int width;
1239
- String content;
1240
- bool selected;
1241
- int line_index;
1242
- };
1256
+ float max_selection_right_edge = 0 ;
1243
1257
1244
- Vector<Segment> segments;
1245
-
1246
- struct IMESegment {
1247
- Vector2f position;
1248
- int width;
1249
- int line_index;
1250
- };
1251
-
1252
- Vector<IMESegment> ime_segments;
1258
+ // Clear the selection background and IME composition geometry, and get the vertices and indices so the new geometry can be generated.
1259
+ Mesh selection_composition_mesh = selection_composition_geometry.Release (Geometry::ReleaseMode::ClearMesh);
1253
1260
1254
1261
// Keep generating lines until all the text content is placed.
1255
1262
do
@@ -1269,44 +1276,33 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
1269
1276
last_line =
1270
1277
text_element->GenerateLine (line_content, line.size , line_width, line_begin, available_width - cursor_size.x , 0 , false , false , false );
1271
1278
1272
- // If this line terminates in a soft-return (word wrap), then the line may be leaving a space or two behind as an orphan. If so, we must
1273
- // append the orphan onto the line even though it will push the line outside of the input field's bounds.
1274
- String orphan;
1275
- if (!last_line && (line_content.empty () || line_content.back () != ' \n ' ))
1279
+ // Check if the editable length needs to be truncated to dodge a trailing endline.
1280
+ line.editable_length = (int )line_content.size ();
1281
+ if (!line_content.empty () && line_content.back () == ' \n ' )
1282
+ line.editable_length -= 1 ;
1283
+
1284
+ // Include all spaces at the end of this line, if they were not included due to soft-wrapping in `GenerateLine`.
1285
+ // This helps prevent sudden shifts when whitespace wraps down to the next line.
1276
1286
{
1277
1287
const String& text = GetValue ();
1278
- for (int i = 1 ; i >= 0 ; --i)
1288
+ size_t i_space_begin = size_t (line_begin + line.editable_length );
1289
+ size_t i_space_end = Math::Min (text.find_first_not_of (' ' , i_space_begin), text.size ());
1290
+ size_t count = i_space_end - i_space_begin;
1291
+ if (count > 0 )
1279
1292
{
1280
- int index = line_begin + line.size + i;
1281
- if (index >= (int )text.size ())
1282
- continue ;
1283
-
1284
- if (text[index ] != ' ' )
1285
- {
1286
- orphan.clear ();
1287
- continue ;
1288
- }
1289
-
1290
- int next_index = index + 1 ;
1291
- if (!orphan.empty () || next_index >= (int )text.size () || text[next_index] != ' ' )
1292
- orphan += ' ' ;
1293
+ line_content.append (count, ' ' );
1294
+ line_width += ElementUtilities::GetStringWidth (text_element, " " ) * (int )count;
1295
+ line.editable_length += (int )count;
1296
+ line.size += (int )count;
1297
+ // Consume the hard wrap if we have one on this line, so that it doesn't make its own, empty line.
1298
+ if (text[i_space_end] == ' \n ' )
1299
+ line.size += 1 ;
1300
+ // If the spaces extend all the way to the end, we have consumed all the lines.
1301
+ if (i_space_end == text.size ())
1302
+ last_line = true ;
1293
1303
}
1294
1304
}
1295
1305
1296
- if (!orphan.empty ())
1297
- {
1298
- line_content += orphan;
1299
- line.size += (int )orphan.size ();
1300
- line_width += ElementUtilities::GetStringWidth (text_element, orphan);
1301
- }
1302
-
1303
- // visually remove trailing space if right aligned
1304
- if (!last_line && text_align == Style ::TextAlign::Right && !line_content.empty () && line_content.back () == ' ' )
1305
- {
1306
- line_content.pop_back ();
1307
- line_width -= ElementUtilities::GetStringWidth (text_element, " " );
1308
- }
1309
-
1310
1306
// Now that we have the string of characters appearing on the new line, we split it into
1311
1307
// three parts; the unselected text appearing before any selected text on the line, the
1312
1308
// selected text on the line, and any unselected text after the selection.
@@ -1318,7 +1314,7 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
1318
1314
if (!pre_selection.empty ())
1319
1315
{
1320
1316
const int width = ElementUtilities::GetStringWidth (text_element, pre_selection);
1321
- segments. push_back ({ line_position, width, pre_selection, false , ( int )lines. size ()} );
1317
+ text_element-> AddLine ( line_position + Vector2f{ GetAlignmentSpecificTextOffset (line), 0 }, pre_selection );
1322
1318
line_position.x += width;
1323
1319
}
1324
1320
@@ -1336,19 +1332,22 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
1336
1332
return float (width_kerning - width_no_kerning);
1337
1333
};
1338
1334
1339
- // Check if the editable length needs to be truncated to dodge a trailing endline.
1340
- line.editable_length = (int )line_content.size ();
1341
- if (!line_content.empty () && line_content.back () == ' \n ' )
1342
- line.editable_length -= 1 ;
1343
-
1344
1335
// If there is any selected text on this line, place it in the selected text element and
1345
1336
// generate the geometry for its background.
1346
1337
if (!selection.empty ())
1347
1338
{
1348
1339
line_position.x += GetKerningBetween (pre_selection, selection);
1340
+
1349
1341
const int selection_width = ElementUtilities::GetStringWidth (selected_text_element, selection);
1350
- segments.push_back ({line_position, selection_width, selection, true , (int )lines.size ()});
1342
+ const bool selection_contains_endline = (selection_begin_index + selection_length > line_begin + line.editable_length );
1343
+ const Vector2f selection_size = {float (selection_width + (selection_contains_endline ? endline_font_width : 0 )), line_height};
1344
+ const Vector2f aligned_position = line_position + Vector2f{GetAlignmentSpecificTextOffset (line), 0 };
1351
1345
1346
+ MeshUtilities::GenerateQuad (selection_composition_mesh, aligned_position - Vector2f (0 , top_to_baseline), selection_size,
1347
+ selection_colour);
1348
+ selected_text_element->AddLine (aligned_position, selection);
1349
+
1350
+ max_selection_right_edge = Math::Max (max_selection_right_edge, aligned_position.x + selection_size.x );
1352
1351
line_position.x += selection_width;
1353
1352
}
1354
1353
@@ -1357,8 +1356,7 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
1357
1356
if (!post_selection.empty ())
1358
1357
{
1359
1358
line_position.x += GetKerningBetween (selection, post_selection);
1360
- const int width = ElementUtilities::GetStringWidth (text_element, post_selection);
1361
- segments.push_back ({line_position, width, post_selection, false , (int )lines.size ()});
1359
+ text_element->AddLine (line_position + Vector2f{GetAlignmentSpecificTextOffset (line), 0 }, post_selection);
1362
1360
}
1363
1361
1364
1362
// We fetch the IME composition on the new line to highlight it.
@@ -1368,9 +1366,16 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
1368
1366
// If there is any IME composition string on the line, create a segment for its underline.
1369
1367
if (!ime_composition.empty ())
1370
1368
{
1369
+ const bool composition_contains_endline = (ime_composition_end_index > line_begin + line.editable_length );
1371
1370
const int composition_width = ElementUtilities::GetStringWidth (text_element, ime_composition);
1372
- const Vector2f composition_position (float (ElementUtilities::GetStringWidth (text_element, ime_pre_composition)), line_position.y );
1373
- ime_segments.push_back ({composition_position, composition_width, (int )lines.size ()});
1371
+ const Vector2f composition_position = {
1372
+ float (ElementUtilities::GetStringWidth (text_element, ime_pre_composition)) + GetAlignmentSpecificTextOffset (line),
1373
+ line_position.y - top_to_baseline + line_height - COMPOSITION_UNDERLINE_WIDTH,
1374
+ };
1375
+ Vector2f line_size = {float (composition_width + (composition_contains_endline ? endline_font_width : 0 )), COMPOSITION_UNDERLINE_WIDTH};
1376
+
1377
+ MeshUtilities::GenerateLine (selection_composition_mesh, composition_position, line_size,
1378
+ parent->GetComputedValues ().color ().ToPremultiplied ());
1374
1379
}
1375
1380
1376
1381
// Update variables for the next line.
@@ -1390,54 +1395,22 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
1390
1395
// Clamp the cursor to a valid range.
1391
1396
absolute_cursor_index = Math::Min (absolute_cursor_index, (int )GetValue ().size ());
1392
1397
1393
- // Clear the selection background geometry, and get the vertices and indices so the new geometry can be generated.
1394
- Mesh selection_mesh = selection_geometry.Release (Geometry::ReleaseMode::ClearMesh);
1395
-
1396
- // Transform segments according to text alignment
1397
- for (Segment& it : segments)
1398
+ selection_composition_geometry = parent->GetRenderManager ()->MakeGeometry (std::move (selection_composition_mesh));
1399
+
1400
+ // Overflow is automatically caught by any text overflowing the content area. However, sometimes it is possible that
1401
+ // the selection box extends beyond the text and outside the content area. This can even overflow the element
1402
+ // itself. In particular, when the selection includes newlines near the right edge. We don't want the selection box
1403
+ // to take part in the scrollable region of the element, which would be one way to ensure that it is always clipped.
1404
+ // Instead, we here detect such possible overflow manually and force the element to clip. This will clip any parts
1405
+ // of the selection box that is overflowing. Maybe in the future we'll have a better way to specify ink overflow and
1406
+ // have that automatically clipped.
1407
+ const bool new_ink_overflow = (max_selection_right_edge > available_width + parent->GetBox ().GetEdge (BoxArea::Padding, BoxEdge::Right));
1408
+ if (new_ink_overflow != ink_overflow)
1398
1409
{
1399
- const auto & line = lines[it.line_index ];
1400
- const char * p_begin = GetValue ().data () + line.value_offset ;
1401
- float offset = GetAlignmentSpecificTextOffset (p_begin, it.line_index );
1402
-
1403
- it.position .x += offset;
1404
-
1405
- if (it.selected )
1406
- {
1407
- const bool selection_contains_endline = (selection_begin_index + selection_length > line_begin + lines[it.line_index ].editable_length );
1408
- const Vector2f selection_size (float (it.width + (selection_contains_endline ? endline_font_width : 0 )), line_height);
1409
-
1410
- MeshUtilities::GenerateQuad (selection_mesh, it.position - Vector2f (0 , top_to_baseline), selection_size, selection_colour);
1411
-
1412
- selected_text_element->AddLine (it.position , it.content );
1413
- }
1414
- else
1415
- text_element->AddLine (it.position , it.content );
1410
+ ink_overflow = new_ink_overflow;
1411
+ parent->SetProperty (PropertyId::Clip, Property (ink_overflow ? Style ::Clip::Type::Always : Style ::Clip::Type::Auto));
1416
1412
}
1417
1413
1418
- selection_geometry = parent->GetRenderManager ()->MakeGeometry (std::move (selection_mesh));
1419
-
1420
- // Clear the IME composition geometry, and get the vertices and indices so the new geometry can be generated.
1421
- Mesh ime_composition_mesh = ime_composition_geometry.Release (Geometry::ReleaseMode::ClearMesh);
1422
-
1423
- // Transform IME segments according to text alignment.
1424
- for (auto & it : ime_segments)
1425
- {
1426
- const auto & line = lines[it.line_index ];
1427
- const char * p_begin = GetValue ().data () + line.value_offset ;
1428
- float offset = GetAlignmentSpecificTextOffset (p_begin, it.line_index );
1429
-
1430
- it.position .x += offset;
1431
- it.position .y += font_metrics.underline_position ;
1432
-
1433
- const bool composition_contains_endline = (ime_composition_end_index > line_begin + lines[it.line_index ].editable_length );
1434
- const Vector2f line_size (float (it.width + (composition_contains_endline ? endline_font_width : 0 )), font_metrics.underline_thickness );
1435
-
1436
- MeshUtilities::GenerateLine (ime_composition_mesh, it.position , line_size, parent->GetComputedValues ().color ().ToPremultiplied ());
1437
- }
1438
-
1439
- ime_composition_geometry = parent->GetRenderManager ()->MakeGeometry (std::move (ime_composition_mesh));
1440
-
1441
1414
return content_area;
1442
1415
}
1443
1416
@@ -1473,12 +1446,18 @@ void WidgetTextInput::UpdateCursorPosition(bool update_ideal_cursor_position)
1473
1446
GetRelativeCursorIndices (cursor_line_index, cursor_character_index);
1474
1447
1475
1448
const auto & line = lines[cursor_line_index];
1476
- const char * p_begin = GetValue ().data () + line.value_offset ;
1449
+ const int string_width_pre_cursor =
1450
+ ElementUtilities::GetStringWidth (text_element, String (StringView (GetValue (), line.value_offset , cursor_character_index)));
1451
+ const float alignment_offset = GetAlignmentSpecificTextOffset (line);
1477
1452
1478
- cursor_position.x = (float )ElementUtilities::GetStringWidth (text_element, String (p_begin, cursor_character_index));
1479
- cursor_position.y = -1 .f + (float )cursor_line_index * text_element->GetLineHeight ();
1453
+ cursor_position = {
1454
+ (float )string_width_pre_cursor + alignment_offset,
1455
+ -1 .f + (float )cursor_line_index * text_element->GetLineHeight (),
1456
+ };
1480
1457
1481
- cursor_position.x += GetAlignmentSpecificTextOffset (p_begin, cursor_line_index);
1458
+ const bool word_wrap = parent->GetComputedValues ().white_space () == Style ::WhiteSpace::Prewrap;
1459
+ if (word_wrap)
1460
+ cursor_position.x = Math::Min (cursor_position.x , GetAvailableWidth () - cursor_size.x );
1482
1461
1483
1462
if (update_ideal_cursor_position)
1484
1463
ideal_cursor_position = cursor_position.x ;
0 commit comments