diff --git a/docs/examples/_valid_examples.toml b/docs/examples/_valid_examples.toml index 2eb4d2a5a..616564885 100644 --- a/docs/examples/_valid_examples.toml +++ b/docs/examples/_valid_examples.toml @@ -78,7 +78,8 @@ files = [ "viz_ui.py", "viz_ui_slider.py", "viz_card.py", - "viz_card_sprite_sheet.py" + "viz_card_sprite_sheet.py", + "viz_spinbox.py" ] [animation] diff --git a/docs/examples/viz_spinbox.py b/docs/examples/viz_spinbox.py new file mode 100644 index 000000000..1c8c8d7e8 --- /dev/null +++ b/docs/examples/viz_spinbox.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +""" +=========== +SpinBox UI +=========== + +This example shows how to use the UI API. We will demonstrate how to create +a SpinBox UI. + +First, some imports. +""" +from fury import actor, ui, utils, window +from fury.data import fetch_viz_icons +import numpy as np + +############################################################################## +# First we need to fetch some icons that are included in FURY. + +fetch_viz_icons() + +############################################################################### +# Let's create a Cone. + +cone = actor.cone(centers=np.random.rand(1, 3), + directions=np.random.rand(1, 3), + colors=(1, 1, 1), heights=np.random.rand(1)) + +############################################################################### +# Creating the SpinBox UI. + +spinbox = ui.SpinBox(position=(200, 100), size=(300, 100), min_val=0, + max_val=360, initial_val=180, step=10) + +############################################################################### +# Now that all the elements have been initialised, we add them to the show +# manager. + +current_size = (800, 800) +show_manager = window.ShowManager(size=current_size, + title="FURY SpinBox Example") + +show_manager.scene.add(cone) +show_manager.scene.add(spinbox) + +############################################################################### +# Using the on_change hook to rotate the cone. + +# Tracking previous value to check in/decrement. +previous_value = spinbox.value + + +def rotate_cone(spinbox): + global previous_value + change_in_value = spinbox.value - previous_value + utils.rotate(cone, (change_in_value, 1, 0, 0)) + previous_value = spinbox.value + + +spinbox.on_change = rotate_cone + +############################################################################### +# Starting the ShowManager. + +interactive = False + +if interactive: + show_manager.start() + +window.record(show_manager.scene, size=current_size, + out_path="viz_spinbox.png") diff --git a/fury/data/files/test_ui_spinbox.json b/fury/data/files/test_ui_spinbox.json new file mode 100644 index 000000000..e3526f17f --- /dev/null +++ b/fury/data/files/test_ui_spinbox.json @@ -0,0 +1 @@ +{"CharEvent": 14, "MouseMoveEvent": 209, "KeyPressEvent": 16, "KeyReleaseEvent": 14, "LeftButtonPressEvent": 41, "LeftButtonReleaseEvent": 41, "RightButtonPressEvent": 0, "RightButtonReleaseEvent": 0, "MiddleButtonPressEvent": 0, "MiddleButtonReleaseEvent": 0} \ No newline at end of file diff --git a/fury/data/files/test_ui_spinbox.log.gz b/fury/data/files/test_ui_spinbox.log.gz new file mode 100644 index 000000000..ccffaaa98 Binary files /dev/null and b/fury/data/files/test_ui_spinbox.log.gz differ diff --git a/fury/ui/elements.py b/fury/ui/elements.py index 0a69b25a7..1bcefd54d 100644 --- a/fury/ui/elements.py +++ b/fury/ui/elements.py @@ -16,6 +16,8 @@ 'DrawShape', 'DrawPanel', 'PlaybackPanel', + 'Card2D', + 'SpinBox' ] import os @@ -4368,3 +4370,194 @@ def left_button_dragged(self, i_ren, _obj, _sub_component): self.panel.position += change self._click_position = click_position i_ren.force_render() + + +class SpinBox(UI): + """SpinBox UI. + """ + + def __init__(self, position=(350, 400), size=(300, 100), padding=10, + panel_color=(1, 1, 1), min_val=0, max_val=100, + initial_val=50, step=1, max_column=10, max_line=2): + """Init this UI element. + + Parameters + ---------- + position : (int, int), optional + Absolute coordinates (x, y) of the lower-left corner of this + UI component. + size : (int, int), optional + Width and height in pixels of this UI component. + padding : int, optional + Distance between TextBox and Buttons. + panel_color : (float, float, float), optional + Panel color of SpinBoxUI. + min_val: int, optional + Minimum value of SpinBoxUI. + max_val: int, optional + Maximum value of SpinBoxUI. + initial_val: int, optional + Initial value of SpinBoxUI. + step: int, optional + Step value of SpinBoxUI. + max_column: int, optional + Max number of characters in a line. + max_line: int, optional + Max number of lines in the textbox. + """ + self.panel_size = size + self.padding = padding + self.panel_color = panel_color + self.min_val = min_val + self.max_val = max_val + self.step = step + self.max_column = max_column + self.max_line = max_line + + super(SpinBox, self).__init__(position) + self.value = initial_val + self.resize(size) + + self.on_change = lambda ui: None + + def _setup(self): + """Setup this UI component. + + Create the SpinBoxUI with Background (Panel2D) and InputBox (TextBox2D) + and Increment,Decrement Button (Button2D). + """ + self.panel = Panel2D(size=self.panel_size, color=self.panel_color) + + self.textbox = TextBox2D(width=self.max_column, + height=self.max_line) + self.textbox.text.dynamic_bbox = False + self.textbox.text.auto_font_scale = True + self.increment_button = Button2D( + icon_fnames=[("up", read_viz_icons(fname="circle-up.png"))]) + self.decrement_button = Button2D( + icon_fnames=[("down", read_viz_icons(fname="circle-down.png"))]) + + self.panel.add_element(self.textbox, (0, 0)) + self.panel.add_element(self.increment_button, (0, 0)) + self.panel.add_element(self.decrement_button, (0, 0)) + + # Adding button click callbacks + self.increment_button.on_left_mouse_button_pressed = \ + self.increment_callback + self.decrement_button.on_left_mouse_button_pressed = \ + self.decrement_callback + self.textbox.off_focus = self.textbox_update_value + + def resize(self, size): + """Resize SpinBox. + + Parameters + ---------- + size : (float, float) + SpinBox size(width, height) in pixels. + """ + self.panel_size = size + self.textbox_size = (int(0.7 * size[0]), int(0.8 * size[1])) + self.button_size = (int(0.2 * size[0]), int(0.3 * size[1])) + self.padding = int(0.03 * self.panel_size[0]) + + self.panel.resize(size) + self.textbox.text.resize(self.textbox_size) + self.increment_button.resize(self.button_size) + self.decrement_button.resize(self.button_size) + + textbox_pos = (self.padding, int((size[1] - self.textbox_size[1])/2)) + inc_btn_pos = (size[0] - self.padding - self.button_size[0], + int((1.5*size[1] - self.button_size[1])/2)) + dec_btn_pos = (size[0] - self.padding - self.button_size[0], + int((0.5*size[1] - self.button_size[1])/2)) + + self.panel.update_element(self.textbox, textbox_pos) + self.panel.update_element(self.increment_button, inc_btn_pos) + self.panel.update_element(self.decrement_button, dec_btn_pos) + + def _get_actors(self): + """Get the actors composing this UI component.""" + return self.panel.actors + + def _add_to_scene(self, scene): + """Add all subcomponents or VTK props that compose this UI component. + + Parameters + ---------- + scene : Scene + + """ + self.panel.add_to_scene(scene) + + def _get_size(self): + return self.panel.size + + def _set_position(self, coords): + """Set the lower-left corner position of this UI component. + + Parameters + ---------- + coords: (float, float) + Absolute pixel coordinates (x, y). + """ + self.panel.center = coords + + def increment_callback(self, i_ren, _obj, _button): + self.increment() + i_ren.force_render() + i_ren.event.abort() + + def decrement_callback(self, i_ren, _obj, _button): + self.decrement() + i_ren.force_render() + i_ren.event.abort() + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + if value >= self.max_val: + self._value = self.max_val + elif value <= self.min_val: + self._value = self.min_val + else: + self._value = value + + self.textbox.set_message(str(self._value)) + + def validate_value(self, value): + """Validate and convert the given value into integer. + + Parameters + ---------- + value : str + Input value received from the textbox. + + Returns + ------- + int + If valid return converted integer else the previous value. + """ + if value.isnumeric(): + return int(value) + + return self.value + + def increment(self): + """Increment the current value by the step.""" + current_val = self.validate_value(self.textbox.message) + self.value = current_val + self.step + self.on_change(self) + + def decrement(self): + """Decrement the current value by the step.""" + current_val = self.validate_value(self.textbox.message) + self.value = current_val - self.step + self.on_change(self) + + def textbox_update_value(self, textbox): + self.value = self.validate_value(textbox.message) + self.on_change(self) diff --git a/fury/ui/tests/test_elements.py b/fury/ui/tests/test_elements.py index 53b8234a8..de82aa168 100644 --- a/fury/ui/tests/test_elements.py +++ b/fury/ui/tests/test_elements.py @@ -1098,13 +1098,14 @@ def test_ui_combobox_2d(interactive=False): npt.assert_equal((90, 90), combobox.drop_button_size) npt.assert_equal((450, 210), combobox.drop_menu_size) + def test_ui_combobox_2d_dropdown_visibility(interactive=False): values = ['An Item' + str(i) for i in range(0, 5)] tab_ui = ui.TabUI(position=(49, 94), size=(400, 400), nb_tabs=1 , draggable=True) combobox = ui.ComboBox2D(items=values, position=(400, 400), size=(300, 200)) - + tab_ui.add_element(0, combobox, (0.1, 0.3)) # Assign the counter callback to every possible event. @@ -1139,6 +1140,7 @@ def test_ui_combobox_2d_dropdown_visibility(interactive=False): npt.assert_equal(True, combobox.drop_down_button.actors[0].GetVisibility()) npt.assert_equal(True, combobox.selection_box.actors[0].GetVisibility()) + @pytest.mark.skipif( skip_osx, reason='This test does not work on macOS.' @@ -1377,3 +1379,50 @@ def test_card_ui(interactive=False): show_manager.play_events_from_file(recording_filename) expected = EventCounter.load(expected_events_counts_filename) event_counter.check_counts(expected) + + +def test_ui_spinbox(interactive=False): + filename = "test_ui_spinbox" + recording_filename = pjoin(DATA_DIR, filename + ".log.gz") + expected_events_counts_filename = pjoin(DATA_DIR, filename + ".json") + + spinbox = ui.SpinBox(size=(300, 200), min_val=-20, max_val=10, step=2) + npt.assert_equal(spinbox.value, 10) + + spinbox.value = 5 + npt.assert_equal(spinbox.value, 5) + spinbox.value = 50 + npt.assert_equal(spinbox.value, 10) + spinbox.value = -50 + npt.assert_equal(spinbox.value, -20) + + spinbox.min_val = -100 + spinbox.max_val = 100 + + spinbox.value = 5 + npt.assert_equal(spinbox.value, 5) + spinbox.value = 50 + npt.assert_equal(spinbox.value, 50) + spinbox.value = -50 + npt.assert_equal(spinbox.value, -50) + + # Assign the counter callback to every possible event. + event_counter = EventCounter() + event_counter.monitor(spinbox) + + current_size = (800, 800) + show_manager = window.ShowManager(size=current_size, title="SpinBox UI Example") + show_manager.scene.add(spinbox) + + if interactive: + show_manager.record_events_to_file(recording_filename) + print(list(event_counter.events_counts.items())) + event_counter.save(expected_events_counts_filename) + else: + show_manager.play_events_from_file(recording_filename) + expected = EventCounter.load(expected_events_counts_filename) + event_counter.check_counts(expected) + + spinbox.resize((450, 200)) + npt.assert_equal((315, 160), spinbox.textbox_size) + npt.assert_equal((90, 60), spinbox.button_size)