diff --git a/fury/ui/core.py b/fury/ui/core.py index c0314fd94..d9de2f416 100644 --- a/fury/ui/core.py +++ b/fury/ui/core.py @@ -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. @@ -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() @@ -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.""" @@ -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 @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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. @@ -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): diff --git a/fury/ui/elements.py b/fury/ui/elements.py index 9e2ccca61..0a69b25a7 100644 --- a/fury/ui/elements.py +++ b/fury/ui/elements.py @@ -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 @@ -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 diff --git a/fury/ui/helpers.py b/fury/ui/helpers.py index b3cd8fd2f..db4e541b5 100644 --- a/fury/ui/helpers.py +++ b/fury/ui/helpers.py @@ -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) @@ -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 @@ -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 diff --git a/fury/ui/tests/test_containers.py b/fury/ui/tests/test_containers.py index e1b56306f..0f18d9853 100644 --- a/fury/ui/tests/test_containers.py +++ b/fury/ui/tests/test_containers.py @@ -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 diff --git a/fury/ui/tests/test_core.py b/fury/ui/tests/test_core.py index f9bcc4acb..859b0b04b 100644 --- a/fury/ui/tests/test_core.py +++ b/fury/ui/tests/test_core.py @@ -222,6 +222,7 @@ def _check_property(obj, attr, values): _check_property(text_block, 'bold', [True, False]) _check_property(text_block, 'italic', [True, False]) _check_property(text_block, 'shadow', [True, False]) + _check_property(text_block, 'auto_font_scale', [True, False]) _check_property(text_block, 'font_size', range(100)) _check_property(text_block, 'message', ['', 'Hello World', 'Line\nBreak']) _check_property(text_block, 'justification', ['left', 'center', 'right']) @@ -384,57 +385,58 @@ def test_text_block_2d_justification(): def test_text_block_2d_size(): - text_block_1 = ui.TextBlock2D(position=(50, 50), size=(100, 100)) - npt.assert_equal(text_block_1.actor.GetTextScaleMode(), 1) - npt.assert_equal(text_block_1.size, (100, 100)) + text_block_0 = ui.TextBlock2D() - text_block_1.font_size = 50 + npt.assert_equal(text_block_0.actor.GetTextScaleMode(), 0) + npt.assert_equal(text_block_0.size, (180,18)) + + text_block_0.font_size = 50 + npt.assert_equal(text_block_0.size, (500, 50)) + + text_block_0.resize((500, 200)) + npt.assert_equal(text_block_0.actor.GetTextScaleMode(), 0) + npt.assert_equal(text_block_0.size, (500, 200)) + + text_block_1 = ui.TextBlock2D(dynamic_bbox=True) npt.assert_equal(text_block_1.actor.GetTextScaleMode(), 0) - npt.assert_equal(text_block_1.font_size, 50) + npt.assert_equal(text_block_1.size, ((len("Text Block") * + text_block_1.font_size, text_block_1.font_size))) + + text_block_1.font_size = 50 + npt.assert_equal(text_block_1.size, (len("Text Block") * + text_block_1.font_size, text_block_1.font_size)) - text_block_2 = ui.TextBlock2D(position=(50, 50), font_size=50) + text_block_1.resize((500, 200)) + npt.assert_equal(text_block_1.actor.GetTextScaleMode(), 0) + npt.assert_equal(text_block_1.size, (500, 200)) - npt.assert_equal(text_block_2.actor.GetTextScaleMode(), 0) - npt.assert_equal(text_block_2.font_size, 50) - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always', RuntimeWarning) - text_block_2.size - npt.assert_equal(len(w), 1) - npt.assert_(issubclass(w[-1].category, RuntimeWarning)) + text_block_2 = ui.TextBlock2D( + text="Just Another Text Block", dynamic_bbox=True, auto_font_scale=True) - text_block_2.resize((100, 100)) + npt.assert_equal(text_block_2.actor.GetTextScaleMode(), 1) + npt.assert_equal(text_block_2.size, (len("Just Another Text Block") * + text_block_2.font_size, text_block_2.font_size)) + text_block_2.resize((500, 200)) npt.assert_equal(text_block_2.actor.GetTextScaleMode(), 1) - npt.assert_equal(text_block_2.size, (100, 100)) + npt.assert_equal(text_block_2.size, (500, 200)) text_block_2.position = (100, 100) npt.assert_equal(text_block_2.position, (100, 100)) - window_size = (700, 700) - show_manager = window.ShowManager(size=window_size) + text_block_3 = ui.TextBlock2D(size=(200, 200)) - text_block_3 = ui.TextBlock2D( - text='FURY\nFURY\nFURY\nHello', - position=(150, 100), - bg_color=(1, 0, 0), - color=(0, 1, 0), - size=(100, 100), - ) + npt.assert_equal(text_block_3.actor.GetTextScaleMode(), 0) + npt.assert_equal(text_block_3.size, (200, 200)) + + text_block_3.resize((500, 200)) + npt.assert_equal(text_block_3.actor.GetTextScaleMode(), 0) + npt.assert_equal(text_block_3.size, (500, 200)) - show_manager.scene.add(text_block_3) - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always', RuntimeWarning) - text_block_3.font_size = 100 - npt.assert_equal(len(w), 1) - npt.assert_(issubclass(w[-1].category, RuntimeWarning)) - npt.assert_equal(text_block_3.size, (100, 100)) - - text_block_3.font_size = 12 - npt.assert_equal(len(w), 1) - npt.assert_(issubclass(w[-1].category, RuntimeWarning)) - npt.assert_equal(text_block_3.font_size, 12) + text_block_3.message = "Hey Trying\nBig Text" + npt.assert_equal(text_block_3.size, (500, 200)) # test_ui_button_panel(recording=True) diff --git a/fury/ui/tests/test_helpers.py b/fury/ui/tests/test_helpers.py index 9917cac57..3584ee48e 100644 --- a/fury/ui/tests/test_helpers.py +++ b/fury/ui/tests/test_helpers.py @@ -13,89 +13,88 @@ def test_clip_overflow(): - text = ui.TextBlock2D(text='', position=(50, 50), color=(1, 0, 0)) - rectangle = ui.Rectangle2D(position=(50, 50), size=(100, 50)) + text = ui.TextBlock2D(text='', position=(50, 50), color=(1, 0, 0), size=(100, 50)) sm = window.ShowManager() - sm.scene.add(rectangle, text) + sm.scene.add(text) text.message = 'Hello' - clip_overflow(text, rectangle.size[0]) + clip_overflow(text, text.size[0]) npt.assert_equal('Hello', text.message) text.message = 'Hello wassup' - clip_overflow(text, rectangle.size[0]) - npt.assert_equal('Hello was...', text.message) + clip_overflow(text, text.size[0]) + npt.assert_equal('He...', text.message) text.message = 'A very very long message to clip text overflow' - clip_overflow(text, rectangle.size[0]) - npt.assert_equal('A very ve...', text.message) + clip_overflow(text, text.size[0]) + npt.assert_equal('A ...', text.message) text.message = 'Hello' - clip_overflow(text, rectangle.size[0], 'left') + clip_overflow(text, text.size[0], 'left') npt.assert_equal('Hello', text.message) text.message = 'Hello wassup' - clip_overflow(text, rectangle.size[0], 'left') - npt.assert_equal('...lo wassup', text.message) + clip_overflow(text, text.size[0], 'left') + npt.assert_equal('...up', text.message) text.message = 'A very very long message to clip text overflow' - clip_overflow(text, rectangle.size[0], 'left') - npt.assert_equal('... overflow', text.message) + clip_overflow(text, text.size[0], 'left') + npt.assert_equal('...ow', text.message) text.message = 'A very very long message to clip text overflow' - clip_overflow(text, rectangle.size[0], 'LeFT') - npt.assert_equal('... overflow', text.message) + clip_overflow(text, text.size[0], 'LeFT') + npt.assert_equal('...ow', text.message) text.message = 'A very very long message to clip text overflow' - clip_overflow(text, rectangle.size[0], 'RigHT') - npt.assert_equal('A very ve...', text.message) + clip_overflow(text, text.size[0], 'RigHT') + npt.assert_equal('A ...', text.message) - npt.assert_raises(ValueError, clip_overflow, text, rectangle.size[0], 'middle') + npt.assert_raises(ValueError, clip_overflow, text, text.size[0], 'middle') def test_wrap_overflow(): - text = ui.TextBlock2D(text='', position=(50, 50), color=(1, 0, 0)) - rectangle = ui.Rectangle2D(position=(50, 50), size=(100, 50)) + text = ui.TextBlock2D(text='', position=(50, 50), color=(1, 0, 0), size=(100, 50)) sm = window.ShowManager() - sm.scene.add(rectangle, text) + sm.scene.add(text) text.message = 'Hello' - wrap_overflow(text, rectangle.size[0]) + wrap_overflow(text, text.size[0]) npt.assert_equal('Hello', text.message) text.message = 'Hello wassup' - wrap_overflow(text, rectangle.size[0]) - npt.assert_equal('Hello wassu\np', text.message) + wrap_overflow(text, text.size[0]) + npt.assert_equal('Hello\n wass\nup', text.message) text.message = 'A very very long message to clip text overflow' - wrap_overflow(text, rectangle.size[0]) + wrap_overflow(text, text.size[0]) npt.assert_equal( - 'A very very\n long mess\nage to clip \ntext overflo\nw', text.message + 'A ver\ny ver\ny lon\ng mes\nsage \nto cl\nip te\nxt ov\nerflo\nw', text.message ) text.message = 'A very very long message to clip text overflow' wrap_overflow(text, 0) npt.assert_equal(text.message, '') + text.message = 'A very very long message to clip text overflow' wrap_overflow(text, -2 * text.size[0]) npt.assert_equal(text.message, '') def test_check_overflow(): - text = ui.TextBlock2D(text='', position=(50, 50), color=(1, 0, 0)) - rectangle = ui.Rectangle2D(position=(50, 50), size=(100, 50)) + text = ui.TextBlock2D(text='', position=(50, 50), + color=(1, 0, 0), size=(100, 50), bg_color=(.5, .5, .5)) sm = window.ShowManager() - sm.scene.add(rectangle, text) + sm.scene.add(text) text.message = 'A very very long message to clip text overflow' - overflow_idx = check_overflow(text, rectangle.size[0], '~') + overflow_idx = check_overflow(text, 100, '~') - npt.assert_equal(10, overflow_idx) - npt.assert_equal('A very ver~', text.message) + npt.assert_equal(4, overflow_idx) + npt.assert_equal('A ve~', text.message) def test_cal_bounding_box_2d():