Skip to content

Commit d6e3fc0

Browse files
committed
fold method
1 parent 4e79675 commit d6e3fc0

File tree

3 files changed

+219
-0
lines changed

3 files changed

+219
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1010
### Added
1111

1212
- Added `GridLayout.max_column_width` https://github.com/Textualize/textual/pull/6228
13+
- Added `Content.fold`
1314

1415
### Changed
1516

src/textual/content.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,54 @@ def wrap(
966966
content_lines = [line.content for line in lines]
967967
return content_lines
968968

969+
def fold(self, width: int) -> list[Content]:
970+
"""Fold this line into a list of lines which have a cell length no greater than `width`.
971+
972+
Folded lines may be 1 less than the width if it contains double width characters (which may
973+
not be subdivided).
974+
975+
Note that this method will not do any word wrappig. For that, see [wrap()][textual.content.Content.wrap].
976+
977+
Args:
978+
width: Desired maximum width (in cells)
979+
980+
Returns:
981+
List of content instances.
982+
"""
983+
if not self:
984+
return []
985+
text = self.plain
986+
lines: list[Content] = []
987+
position = 0
988+
width = max(width, 2)
989+
while text:
990+
snip = text[position : position + width]
991+
if not snip:
992+
break
993+
snip_cell_length = cell_len(snip)
994+
if snip_cell_length < width:
995+
# last snip
996+
lines.append(self[position : position + width])
997+
break
998+
if snip_cell_length == width:
999+
# Cell length is exactly width
1000+
lines.append(self[position : position + width])
1001+
text = text[len(snip) :]
1002+
position += len(snip)
1003+
continue
1004+
# TODO: Can this be more efficient?
1005+
extra_cells = snip_cell_length - width
1006+
if start_snip := extra_cells // 2:
1007+
snip_cell_length -= cell_len(snip[-start_snip:])
1008+
snip = snip[: len(snip) - start_snip]
1009+
while snip_cell_length > width:
1010+
snip_cell_length -= cell_len(snip[-1])
1011+
snip = snip[:-1]
1012+
lines.append(self[position : position + len(snip)])
1013+
position += len(snip)
1014+
1015+
return lines
1016+
9691017
def get_style_at_offset(self, offset: int) -> Style:
9701018
"""Get the style of a character at give offset.
9711019

tests/test_content.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,173 @@ def test_wrap() -> None:
380380
assert len(wrapped) == len(expected)
381381
for line1, line2 in zip(wrapped, expected):
382382
assert line1.is_same(line2)
383+
384+
385+
@pytest.mark.parametrize(
386+
"content, width, expected",
387+
[
388+
(
389+
Content(""),
390+
10,
391+
[Content("")],
392+
),
393+
(
394+
Content("1"),
395+
10,
396+
[Content("1")],
397+
),
398+
(
399+
Content("📦"),
400+
10,
401+
[Content("📦")],
402+
),
403+
(
404+
Content("📦"),
405+
1,
406+
[Content("📦")],
407+
),
408+
(
409+
Content("Hello"),
410+
10,
411+
[Content("Hello")],
412+
),
413+
(
414+
Content("Hello"),
415+
5,
416+
[Content("Hello")],
417+
),
418+
(
419+
Content("Hello"),
420+
2,
421+
[Content("He"), Content("ll"), Content("o")],
422+
),
423+
(
424+
Content.from_markup("H[b]ell[/]o"),
425+
2,
426+
[
427+
Content.from_markup("H[b]e"),
428+
Content.from_markup("[b]ll[/]"),
429+
Content("o"),
430+
],
431+
),
432+
(
433+
Content.from_markup("💩H[b]ell[/]o"),
434+
2,
435+
[
436+
Content("💩"),
437+
Content.from_markup("H[b]e"),
438+
Content.from_markup("[b]ll[/]"),
439+
Content("o"),
440+
],
441+
),
442+
(
443+
Content.from_markup("💩H[b]ell[/]o"),
444+
3,
445+
[
446+
Content("💩H"),
447+
Content.from_markup("[b]ell"),
448+
Content.from_markup("[b]o[/]"),
449+
],
450+
),
451+
(
452+
Content.from_markup("💩H[b]ell[/]💩"),
453+
3,
454+
[
455+
Content("💩H"),
456+
Content.from_markup("[b]ell"),
457+
Content.from_markup("[b]o[/]💩"),
458+
],
459+
),
460+
(
461+
Content.from_markup("💩💩💩"),
462+
1,
463+
[
464+
Content("💩"),
465+
Content("💩"),
466+
Content("💩"),
467+
],
468+
),
469+
(
470+
Content.from_markup("💩💩💩"),
471+
3,
472+
[
473+
Content("💩"),
474+
Content("💩"),
475+
Content("💩"),
476+
],
477+
),
478+
(
479+
Content.from_markup("💩💩💩"),
480+
4,
481+
[
482+
Content("💩💩"),
483+
Content("💩"),
484+
],
485+
),
486+
(
487+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888📦999"),
488+
50,
489+
[Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888📦999")],
490+
),
491+
(
492+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888📦999"),
493+
49,
494+
[
495+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888📦99"),
496+
Content("9"),
497+
],
498+
),
499+
(
500+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888📦999"),
501+
48,
502+
[
503+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888📦9"),
504+
Content("99"),
505+
],
506+
),
507+
(
508+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888📦999"),
509+
47,
510+
[
511+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888📦"),
512+
Content("999"),
513+
],
514+
),
515+
(
516+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888📦999"),
517+
46,
518+
[
519+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888"),
520+
Content("📦999"),
521+
],
522+
),
523+
(
524+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888📦999"),
525+
45,
526+
[
527+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888"),
528+
Content("📦999"),
529+
],
530+
),
531+
(
532+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦888📦999"),
533+
44,
534+
[
535+
Content("📦000📦111📦222📦333📦444📦555📦666📦777📦88"),
536+
Content("8📦999"),
537+
],
538+
),
539+
],
540+
)
541+
def test_fold(content: Content, width: int, expected: list[Content]) -> None:
542+
"""Test content.fold method works, and correctly handles double width cells.
543+
544+
Args:
545+
content: Test content.
546+
width: Desired width.
547+
expected: Expectected result.
548+
"""
549+
result = content.fold(width)
550+
assert isinstance(result, list)
551+
for line, expected_line in zip(result, expected):
552+
assert line.is_same(expected_line)

0 commit comments

Comments
 (0)