class EL_TEXT_RECTANGLE
Container for wrapping text into a rectangular area before rendering it with a drawing command
Supports
* Multiple simultaneous font sizes * Word wrapping * Squeezing of text into available space by adjusting the font size * Rotation of text area
note
description: "[
Container for wrapping text into a rectangular area before rendering it with a drawing command
**Supports**
* Multiple simultaneous font sizes
* Word wrapping
* Squeezing of text into available space by adjusting the font size
* Rotation of text area
]"
author: "Finnian Reilly"
copyright: "Copyright (c) 2001-2022 Finnian Reilly"
contact: "finnian at eiffel hyphen loop dot com"
license: "MIT license (See: en.wikipedia.org/wiki/MIT_License)"
date: "2024-08-27 10:02:08 GMT (Tuesday 27th August 2024)"
revision: "22"
class
EL_TEXT_RECTANGLE
inherit
EL_RECTANGLE
redefine
make
end
EL_HYPENATEABLE
undefine
out
end
EL_TEXT_ALIGNMENT
export
{ANY} Left_alignment, Center_alignment, Right_alignment
{EL_ALIGNED_TEXT} alignment_code
undefine
out
end
EL_STRING_GENERAL_ROUTINES
EL_ZSTRING_CONSTANTS
EL_MODULE_LIO
EL_MODULE_ACTION
create
make_cms, make, make_from_rectangle
feature {NONE} -- Initialization
make (a_x, a_y, a_width, a_height: INTEGER)
do
Precursor (a_x, a_y, a_width, a_height)
create font
create internal_lines.make (2)
align_text_left; align_text_top
end
make_from_rectangle (r: EL_RECTANGLE)
do
make (r.x, r.y, r.width, r.height)
end
feature -- Access
font: EV_FONT
lines: like word_wrapped_lines
do
create Result.make (internal_lines.count)
across internal_lines as line loop
Result.extend (line.item.text)
end
end
feature -- Measurement
available_height: INTEGER
do
Result := height
across internal_lines as line loop
Result := Result - line.item.height
end
Result := Result.max (0)
end
bottom_most_y: INTEGER
-- y coordinate of bottom of bottom-most line
do
if internal_lines.count > 0 then
Result := internal_lines.last.bottom
end
end
line_count: INTEGER
do
Result := internal_lines.count
end
feature -- Status query
is_text_squeezable: BOOLEAN
-- if true allows squeezing of text into available space by reducing font size
line_fits (line: READABLE_STRING_GENERAL): BOOLEAN
do
Result := Text_.string_width (line, font) <= width
end
feature -- Status setting
disable_squeezing
-- disable text squeezing
do
is_text_squeezable := False
end
enable_squeezing
-- enable squeezing of text into available space by reducing font size
do
is_text_squeezable := True
end
feature -- Element change
add_separation (a_separation_cms: REAL)
local
separator: EL_ALIGNED_TEXT; l_bottom: INTEGER
do
separator := new_aligned_text ("")
separator.set_height (Screen.vertical_pixels (a_separation_cms))
l_bottom := bottom_most_y
if l_bottom > 0 then
separator.set_y (l_bottom + 1)
end
internal_lines.extend (separator)
end
append_line (a_line: READABLE_STRING_GENERAL)
-- append line without wrapping
do
if is_text_squeezable then
squeeze_line (as_zstring (a_line))
else
extend_lines (as_zstring (a_line))
end
end
append_paragraphs (list: ITERABLE [READABLE_STRING_GENERAL]; separation_cms: REAL)
local
i: INTEGER
do
across list as l loop
if i > 0 then
add_separation (separation_cms)
end
append_words (l.item)
i := i + 1
end
end
append_words (line: READABLE_STRING_GENERAL)
-- append words wrapping them if they do not fit in one line
do
if is_text_squeezable then
squeeze_flow_text (line)
else
flow_text (line)
end
end
set_font (a_font: like font)
do
font := a_font.twin
end
feature -- Basic operations
draw (canvas: EL_DRAWABLE)
do
across internal_lines as line loop
if not line.item.text.is_empty then
line.item.align (Current)
line.item.draw (canvas)
end
end
end
draw_border (canvas: EL_DRAWABLE)
do
canvas.draw_rectangle (x, y, width, height)
end
draw_rotated_border (canvas: EL_DRAWABLE; a_angle: DOUBLE)
local
rect: EL_MODEL_ROTATED_RECTANGLE
do
create rect.make_rotated (width, height, a_angle)
rect.move (x, y)
rect.draw (canvas)
end
draw_rotated_top_left (canvas: EL_DRAWABLE; a_angle: DOUBLE)
local
text_group: like line_text_group
line: EL_ALIGNED_TEXT
do
text_group := line_text_group
text_group.rotate_around (a_angle, x, y)
across text_group as text_point loop
line := internal_lines [text_point.cursor_index]
if attached {EV_MODEL_DOT} text_point.item as point then
canvas.set_font (line.font)
canvas.draw_rotated_text (point.x, point.y, a_angle.truncated_to_real.opposite, line.text)
end
end
end
feature -- Removal
wipe_out
do
internal_lines.wipe_out
end
feature {NONE} -- Implementation
extend_lines (a_line: ZSTRING)
do
internal_lines.extend (new_aligned_text (a_line))
end
flow_text (line: READABLE_STRING_GENERAL)
do
across word_wrapped_lines (line) as l_line loop
extend_lines (l_line.item)
end
end
hypenate_word (words: EL_INTERVAL_LIST; line, line_out: ZSTRING)
local
old_count, i: INTEGER; outside_bounds: BOOLEAN
do
if words.item_count >= 4 then
old_count := line_out.count
-- check if part of word will fit
if not line_out.is_empty then
line_out.append_character (' ')
end
line_out.append_substring (line, words.item_lower, words.item_lower + 1)
line_out.append_character ('-')
if line_fits (line_out) then
from i := words.item_lower + 2 until i > words.item_upper or outside_bounds loop
line_out.insert_character (line [i], line_out.count)
if line_fits (line_out) then
i := i + 1
else
outside_bounds := True
line_out.remove_substring (line_out.count - 1, line_out.count - 1) -- Undo insertion
end
end
words.replace (i, words.item_upper)
if words.item_count = 1
or else words.item_count = 2
and then line.is_alpha_item (i)
and then is_comma_or_dot (line [i + 1])
then
line_out.remove_tail (1)
line_out.append_substring (line, i, words.item_upper)
words.replace (words.item_upper + 1, words.item_upper) -- set to zero
end
else
line_out.keep_head (old_count)
end
end
end
line_text_group: EV_MODEL_GROUP
local
line: EL_ALIGNED_TEXT
do
create Result.make_with_position (x, y)
across internal_lines as list loop
line := list.item
line.align (Current)
Result.extend (create {EV_MODEL_DOT}.make_with_position (line.x, line.y))
end
end
is_comma_or_dot (c: CHARACTER_32): BOOLEAN
do
inspect c
when ',', '.' then
Result := True
else
end
end
new_aligned_text (a_text: ZSTRING): EL_ALIGNED_TEXT
do
create Result.make (a_text, Current)
end
squeeze_flow_text (line: READABLE_STRING_GENERAL)
-- append words, decreasing font size until text fits
local
appended: BOOLEAN; old_font: like font
wrapped_lines: EL_ZSTRING_LIST
do
old_font := font.twin
from until font.height < 4 or appended loop
wrapped_lines := word_wrapped_lines (line)
if Text_.widest_width (wrapped_lines, font) <= width
and then wrapped_lines.count * font.line_height <= available_height
then
flow_text (line)
appended := True
else
font.set_height (font.height - 1)
end
end
font := old_font
end
squeeze_line (a_line: ZSTRING)
-- append line, reducing font size so the line fits in available space
local
appended: BOOLEAN
old_font: like font
do
old_font := font.twin
from until font.height < 4 or appended loop
if font.line_height <= available_height and then line_fits (a_line) then
extend_lines (a_line)
appended := True
else
font.set_height (font.height - 1)
end
end
font := old_font
end
word_wrapped_lines (a_line: READABLE_STRING_GENERAL): EL_ZSTRING_LIST
local
line_out: ZSTRING; old_count: INTEGER; words: EL_SPLIT_ZSTRING_LIST
line: ZSTRING
do
create Result.make (0); create line_out.make_empty; line := as_zstring (a_line)
create words.make (line, ' ')
if is_lio_enabled then
lio.put_line (a_line)
lio.put_line ("WRAPPED")
lio.put_new_line
end
from words.start until words.after loop
old_count := line_out.count
if not line_out.is_empty then
line_out.append_character (' ')
end
words.append_item_to (line_out)
if line_fits (line_out) then
words.forth
else
if is_hyphenated then
line_out.keep_head (old_count)
hypenate_word (words, line, line_out)
if words.item_count = 0 then
-- word might be empty if it ended with a comma and had one alpha character
words.forth
end
else
if words.item_same_as (line_out) then
-- Allow a line consisting of a single word even though it's too wide
words.forth
else
line_out.keep_head (old_count)
end
end
Result.extend (line_out.twin)
line_out.wipe_out
end
end
if not line_out.is_empty then
Result.extend (line_out)
end
if is_lio_enabled then
across Result as l loop
lio.put_line (l.item)
end
lio.put_new_line
end
end
feature {EV_ANY_HANDLER} -- Internal attributes
internal_lines: EL_ARRAYED_LIST [EL_ALIGNED_TEXT]
end