Skip to content

Commit

Permalink
Merge pull request #803 from ganimtron-10/textBoundingBox
Browse files Browse the repository at this point in the history
UI: Adding Bounding Box & Fixing Alignment issue in TextBlock2D
  • Loading branch information
skoudoro authored Jul 29, 2023
2 parents 0baaf03 + 0ecdc84 commit 924d024
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 163 deletions.
185 changes: 104 additions & 81 deletions fury/ui/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,8 @@ def __init__(
color=(1, 1, 1),
bg_color=None,
position=(0, 0),
auto_font_scale=False,
dynamic_bbox=False
):
"""Init class instance.
Expand Down Expand Up @@ -730,23 +732,34 @@ def __init__(
Adds text shadow.
size : (int, int)
Size (width, height) in pixels of the text bounding box.
auto_font_scale : bool, optional
Automatically scale font according to the text bounding box.
dynamic_bbox : bool, optional
Automatically resize the bounding box according to the content.
"""
self.boundingbox = [0, 0, 0, 0]
super(TextBlock2D, self).__init__(position=position)
self.scene = None
self.have_bg = bool(bg_color)
if size is not None:
self.resize(size)
else:
self.font_size = font_size
self.color = color
self.background_color = bg_color
self.font_family = font_family
self.justification = justification
self._justification = justification
self.bold = bold
self.italic = italic
self.shadow = shadow
self.vertical_justification = vertical_justification
self._vertical_justification = vertical_justification
self.auto_font_scale = auto_font_scale
if self.auto_font_scale:
self.actor.SetTextScaleModeToProp()
self.dynamic_bbox = dynamic_bbox
self.message = text
self.font_size = font_size
if size is not None:
self.resize(size)
elif not self.dynamic_bbox:
# raise ValueError("TextBlock size is required as it is not dynamic.")
self.resize((0, 0))

def _setup(self):
self.actor = TextActor()
Expand All @@ -762,10 +775,7 @@ def resize(self, size):
size : (int, int)
Text bounding box size(width, height) in pixels.
"""
if self.have_bg:
self.background.resize(size)
self.actor.SetTextScaleModeToProp()
self.actor.SetPosition2(*size)
self.update_bounding_box(size)

def _get_actors(self):
"""Get the actors composing this UI component."""
Expand All @@ -778,11 +788,6 @@ def _add_to_scene(self, scene):
----------
scene : scene
"""
self.scene = scene
if self.have_bg and not self.actor.GetTextScaleMode():
size = np.zeros(2)
self.actor.GetSize(scene, size)
self.background.resize(size)
scene.add(self.background, self.actor)

@property
Expand All @@ -806,6 +811,8 @@ def message(self, text):
The message to be set.
"""
self.actor.SetInput(text)
if self.dynamic_bbox:
self.update_bounding_box()

@property
def font_size(self):
Expand All @@ -827,21 +834,12 @@ def font_size(self, size):
size : int
Text font size.
"""
self.actor.SetTextScaleModeToNone()
self.actor.GetTextProperty().SetFontSize(size)

if self.scene is not None and self.have_bg:
bb_size = np.zeros(2)
self.actor.GetSize(self.scene, bb_size)
bg_size = self.background.size
if bb_size[0] > bg_size[0] or bb_size[1] > bg_size[1]:
warn(
'Font size exceeds background bounding box.'
' Font Size will not be updated.',
RuntimeWarning,
)
self.actor.SetTextScaleModeToProp()
self.actor.SetPosition2(*bg_size)
if not self.auto_font_scale:
self.actor.SetTextScaleModeToNone()
self.actor.GetTextProperty().SetFontSize(size)

if self.dynamic_bbox:
self.update_bounding_box()

@property
def font_family(self):
Expand Down Expand Up @@ -881,13 +879,7 @@ def justification(self):
str
Text justification.
"""
justification = self.actor.GetTextProperty().GetJustificationAsString()
if justification == 'Left':
return 'left'
elif justification == 'Centered':
return 'center'
elif justification == 'Right':
return 'right'
return self._justification

@justification.setter
def justification(self, justification):
Expand All @@ -899,16 +891,8 @@ def justification(self, justification):
Possible values are left, right, center.
"""
text_property = self.actor.GetTextProperty()
if justification == 'left':
text_property.SetJustificationToLeft()
elif justification == 'center':
text_property.SetJustificationToCentered()
elif justification == 'right':
text_property.SetJustificationToRight()
else:
msg = 'Text can only be justified left, right and center.'
raise ValueError(msg)
self._justification = justification
self.update_alignment()

@property
def vertical_justification(self):
Expand All @@ -920,14 +904,7 @@ def vertical_justification(self):
Text vertical justification.
"""
text_property = self.actor.GetTextProperty()
vjustification = text_property.GetVerticalJustificationAsString()
if vjustification == 'Bottom':
return 'bottom'
elif vjustification == 'Centered':
return 'middle'
elif vjustification == 'Top':
return 'top'
return self._vertical_justification

@vertical_justification.setter
def vertical_justification(self, vertical_justification):
Expand All @@ -939,16 +916,8 @@ def vertical_justification(self, vertical_justification):
Possible values are bottom, middle, top.
"""
text_property = self.actor.GetTextProperty()
if vertical_justification == 'bottom':
text_property.SetVerticalJustificationToBottom()
elif vertical_justification == 'middle':
text_property.SetVerticalJustificationToCentered()
elif vertical_justification == 'top':
text_property.SetVerticalJustificationToTop()
else:
msg = 'Vertical justification must be: bottom, middle or top.'
raise ValueError(msg)
self._vertical_justification = vertical_justification
self.update_alignment()

@property
def bold(self):
Expand Down Expand Up @@ -1078,6 +1047,71 @@ def background_color(self, color):
self.background.set_visibility(True)
self.background.color = color

def update_alignment(self):
"""Update Text Alignment.
"""
text_property = self.actor.GetTextProperty()
updated_text_position = [0, 0]

if self.justification.lower() == 'left':
text_property.SetJustificationToLeft()
updated_text_position[0] = self.boundingbox[0]
elif self.justification.lower() == 'center':
text_property.SetJustificationToCentered()
updated_text_position[0] = self.boundingbox[0] + \
(self.boundingbox[2]-self.boundingbox[0])//2
elif self.justification.lower() == 'right':
text_property.SetJustificationToRight()
updated_text_position[0] = self.boundingbox[2]
else:
msg = 'Text can only be justified left, right and center.'
raise ValueError(msg)

if self.vertical_justification.lower() == 'bottom':
text_property.SetVerticalJustificationToBottom()
updated_text_position[1] = self.boundingbox[1]
elif self.vertical_justification.lower() == 'middle':
text_property.SetVerticalJustificationToCentered()
updated_text_position[1] = self.boundingbox[1] + \
(self.boundingbox[3]-self.boundingbox[1])//2
elif self.vertical_justification.lower() == 'top':
text_property.SetVerticalJustificationToTop()
updated_text_position[1] = self.boundingbox[3]
else:
msg = 'Vertical justification must be: bottom, middle or top.'
raise ValueError(msg)

self.actor.SetPosition(updated_text_position)

def cal_size_from_message(self):
"Calculate size of background according to the message it contains."
lines = self.message.split("\n")
max_length = max(len(line) for line in lines)
return [max_length*self.font_size, len(lines)*self.font_size]

def update_bounding_box(self, size=None):
"""Update Text Bounding Box.
Parameters
----------
size : (int, int) or None
If None, calculates bounding box.
Otherwise, uses the given size.
"""
if size is None:
size = self.cal_size_from_message()

self.boundingbox = [self.position[0], self.position[1],
self.position[0]+size[0], self.position[1]+size[1]]
self.background.resize(size)

if self.auto_font_scale:
self.actor.SetPosition2(
self.boundingbox[2]-self.boundingbox[0], self.boundingbox[3]-self.boundingbox[1])
else:
self.update_alignment()

def _set_position(self, position):
"""Set text actor position.
Expand All @@ -1091,22 +1125,11 @@ def _set_position(self, position):
self.background.position = position

def _get_size(self):
if self.have_bg:
return self.background.size

if not self.actor.GetTextScaleMode():
if self.scene is not None:
size = np.zeros(2)
self.actor.GetSize(self.scene, size)
return size
else:
warn(
'TextBlock2D must be added to the scene before '
'querying its size while TextScaleMode is set to None.',
RuntimeWarning,
)

return self.actor.GetPosition2()
bb_size = (self.boundingbox[2]-self.boundingbox[0],
self.boundingbox[3]-self.boundingbox[1])
if self.dynamic_bbox or self.auto_font_scale or sum(bb_size):
return bb_size
return self.cal_size_from_message()


class Button2D(UI):
Expand Down
5 changes: 3 additions & 2 deletions fury/ui/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def _setup(self):
Create the TextBlock2D component used for the textbox.
"""
self.text = TextBlock2D()
self.text = TextBlock2D(dynamic_bbox=True)

# Add default events listener for this UI component.
self.text.on_left_mouse_button_pressed = self.left_button_press
Expand Down Expand Up @@ -1447,7 +1447,8 @@ def _setup(self):
self.handle.color = self.default_color

# Slider Text
self.text = TextBlock2D(justification='center', vertical_justification='middle')
self.text = TextBlock2D(justification='center',
vertical_justification='middle')

# Add default events listener for this UI component.
self.track.on_left_mouse_button_pressed = self.track_click_callback
Expand Down
13 changes: 4 additions & 9 deletions fury/ui/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ def wrap_overflow(textblock, wrap_width, side='right'):
"""
original_str = textblock.message
str_copy = textblock.message
prev_bg = textblock.have_bg
wrap_idxs = []

wrap_idx = check_overflow(textblock, wrap_width, '', side)
Expand All @@ -72,7 +71,6 @@ def wrap_overflow(textblock, wrap_width, side='right'):
original_str = original_str[:idx] + '\n' + original_str[idx:]

textblock.message = original_str
textblock.have_bg = prev_bg
return textblock.message


Expand All @@ -99,26 +97,23 @@ def check_overflow(textblock, width, overflow_postfix='', side='right'):
start_ptr = 0
mid_ptr = 0
end_ptr = len(original_str)
prev_bg = textblock.have_bg
textblock.have_bg = False

if side == 'left':
original_str = original_str[::-1]

if textblock.size[0] <= width:
textblock.have_bg = prev_bg
if textblock.cal_size_from_message()[0] <= width:
return 0

while start_ptr < end_ptr:
mid_ptr = (start_ptr + end_ptr) // 2
textblock.message = original_str[:mid_ptr] + overflow_postfix

if textblock.size[0] < width:
if textblock.cal_size_from_message()[0] < width:
start_ptr = mid_ptr
elif textblock.size[0] > width:
elif textblock.cal_size_from_message()[0] > width:
end_ptr = mid_ptr

if mid_ptr == (start_ptr + end_ptr) // 2 or textblock.size[0] == width:
if mid_ptr == (start_ptr + end_ptr) // 2 or textblock.cal_size_from_message()[0] == width:
if side == 'left':
textblock.message = textblock.message[::-1]
return mid_ptr
Expand Down
6 changes: 3 additions & 3 deletions fury/ui/tests/test_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,9 @@ def test_ui_tab_ui(interactive=False):
npt.assert_equal(tab_ui.tabs[1].title_color, (.0, 1., .0))
npt.assert_equal(tab_ui.tabs[2].title_color, (.0, .0, 1.))

npt.assert_equal(tab_ui.tabs[0].title_font_size, 12)
npt.assert_equal(tab_ui.tabs[1].title_font_size, 12)
npt.assert_equal(tab_ui.tabs[2].title_font_size, 12)
npt.assert_equal(tab_ui.tabs[0].title_font_size, 18)
npt.assert_equal(tab_ui.tabs[1].title_font_size, 18)
npt.assert_equal(tab_ui.tabs[2].title_font_size, 18)

tab_ui.tabs[0].title_font_size = 10
tab_ui.tabs[1].title_font_size = 20
Expand Down
Loading

0 comments on commit 924d024

Please sign in to comment.