"""UI components module."""
__all__ = [
    "TextBox2D",
    "LineSlider2D",
    "LineDoubleSlider2D",
    "RingSlider2D",
    "RangeSlider",
    "Checkbox",
    "Option",
    "RadioButton",
    "ComboBox2D",
    "ListBox2D",
    "ListBoxItem2D",
    "FileMenu2D",
    "DrawShape",
    "DrawPanel",
    "PlaybackPanel",
    "Card2D",
    "SpinBox",
]
from collections import OrderedDict
from numbers import Number
import os
from string import printable
from urllib.request import urlopen
from PIL import Image, UnidentifiedImageError
import numpy as np
from fury.data import read_viz_icons
from fury.decorators import warn_on_args_to_kwargs
from fury.lib import Command
from fury.ui.containers import ImageContainer2D, Panel2D
from fury.ui.core import UI, Button2D, Disk2D, Rectangle2D, TextBlock2D
from fury.ui.helpers import (
    TWO_PI,
    cal_bounding_box_2d,
    clip_overflow,
    rotate_2d,
    wrap_overflow,
)
from fury.utils import set_polydata_vertices, update_actor, vertices_from_actor
[docs]
class TextBox2D(UI):
    """An editable 2D text box that behaves as a UI component.
    Currently supports:
    - Basic text editing.
    - Cursor movements.
    - Single and multi-line text boxes.
    - Pre text formatting (text needs to be formatted beforehand).
    Attributes
    ----------
    text : str
        The current text state.
    actor : :class:`vtkActor2d`
        The text actor.
    width : int
        The number of characters in a single line of text.
    height : int
        The number of lines in the textbox.
    window_left : int
        Left limit of visible text in the textbox.
    window_right : int
        Right limit of visible text in the textbox.
    caret_pos : int
        Position of the caret in the text.
    init : bool
        Flag which says whether the textbox has just been initialized.
    """
    @warn_on_args_to_kwargs()
    def __init__(
        self,
        width,
        height,
        *,
        text="Enter Text",
        position=(100, 10),
        color=(0, 0, 0),
        font_size=18,
        font_family="Arial",
        justification="left",
        bold=False,
        italic=False,
        shadow=False,
    ):
        """Init this UI element.
        Parameters
        ----------
        width : int
            The number of characters in a single line of text.
        height : int
            The number of lines in the textbox.
        text : str
            The initial text while building the actor.
        position : (float, float)
            (x, y) in pixels.
        color : (float, float, float)
            RGB: Values must be between 0-1.
        font_size : int
            Size of the text font.
        font_family : str
            Currently only supports Arial.
        justification : str
            left, right or center.
        bold : bool
            Makes text bold.
        italic : bool
            Makes text italicised.
        shadow : bool
            Adds text shadow.
        """
        super(TextBox2D, self).__init__(position=position)
        self.message = text
        self.text.message = text
        self.text.font_size = font_size
        self.text.font_family = font_family
        self.text.justification = justification
        self.text.bold = bold
        self.text.italic = italic
        self.text.shadow = shadow
        self.text.color = color
        self.text.background_color = (1, 1, 1)
        self.width = width
        self.height = height
        self.window_left = 0
        self.window_right = 0
        self.caret_pos = 0
        self.init = True
        self.off_focus = lambda ui: None
    def _setup(self):
        """Setup this UI component.
        Create the TextBlock2D component used for the textbox.
        """
        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
        self.text.on_key_press = self.key_press
    def _get_actors(self):
        """Get the actors composing this UI component."""
        return self.text.actors
    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.
        Parameters
        ----------
        scene : scene
        """
        self.text.add_to_scene(scene)
    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.text.position = coords
    def _get_size(self):
        return self.text.size
[docs]
    def set_message(self, message):
        """Set custom text to textbox.
        Parameters
        ----------
        message: str
            The custom message to be set.
        """
        self.message = message
        self.text.message = message
        self.init = False
        self.window_right = len(self.message)
        self.window_left = 0
        self.caret_pos = self.window_right 
[docs]
    def width_set_text(self, text):
        """Add newlines to text where necessary.
        This is needed for multi-line text boxes.
        Parameters
        ----------
        text : str
            The final text to be formatted.
        Returns
        -------
        str
            A multi line formatted text.
        """
        multi_line_text = ""
        for i, t in enumerate(text):
            multi_line_text += t
            if (i + 1) % self.width == 0:
                multi_line_text += "\n"
        return multi_line_text.rstrip("\n") 
[docs]
    def handle_character(self, key, key_char):
        """Handle button events.
        # TODO: Need to handle all kinds of characters like !, +, etc.
        Parameters
        ----------
        character : str
        """
        if key.lower() == "return":
            self.render_text(show_caret=False)
            self.off_focus(self)
            return True
        elif key_char != "" and key_char in printable:
            self.add_character(key_char)
        if key.lower() == "backspace":
            self.remove_character()
        elif key.lower() == "left":
            self.move_left()
        elif key.lower() == "right":
            self.move_right()
        self.render_text()
        return False 
[docs]
    def move_caret_right(self):
        """Move the caret towards right."""
        self.caret_pos = min(self.caret_pos + 1, len(self.message)) 
[docs]
    def move_caret_left(self):
        """Move the caret towards left."""
        self.caret_pos = max(self.caret_pos - 1, 0) 
[docs]
    def right_move_right(self):
        """Move right boundary of the text window right-wards."""
        if self.window_right <= len(self.message):
            self.window_right += 1 
[docs]
    def right_move_left(self):
        """Move right boundary of the text window left-wards."""
        if self.window_right > 0:
            self.window_right -= 1 
[docs]
    def left_move_right(self):
        """Move left boundary of the text window right-wards."""
        if self.window_left <= len(self.message):
            self.window_left += 1 
[docs]
    def left_move_left(self):
        """Move left boundary of the text window left-wards."""
        if self.window_left > 0:
            self.window_left -= 1 
[docs]
    def add_character(self, character):
        """Insert a character into the text and moves window and caret.
        Parameters
        ----------
        character : str
        """
        if len(character) > 1 and character.lower() != "space":
            return
        if character.lower() == "space":
            character = " "
        self.message = (
            self.message[: self.caret_pos] + character + self.message[self.caret_pos :]
        )
        self.move_caret_right()
        if self.window_right - self.window_left == self.height * self.width - 1:
            self.left_move_right()
        self.right_move_right() 
[docs]
    def remove_character(self):
        """Remove a character and moves window and caret accordingly."""
        if self.caret_pos == 0:
            return
        self.message = (
            self.message[: self.caret_pos - 1] + self.message[self.caret_pos :]
        )
        self.move_caret_left()
        if len(self.message) < self.height * self.width - 1:
            self.right_move_left()
        if self.window_right - self.window_left == self.height * self.width - 1:
            if self.window_left > 0:
                self.left_move_left()
                self.right_move_left() 
[docs]
    def move_left(self):
        """Handle left button press."""
        self.move_caret_left()
        if self.caret_pos == self.window_left - 1:
            if self.window_right - self.window_left == self.height * self.width - 1:
                self.left_move_left()
                self.right_move_left() 
[docs]
    def move_right(self):
        """Handle right button press."""
        self.move_caret_right()
        if self.caret_pos == self.window_right + 1:
            if self.window_right - self.window_left == self.height * self.width - 1:
                self.left_move_right()
                self.right_move_right() 
[docs]
    def showable_text(self, show_caret):
        """Chop out text to be shown on the screen.
        Parameters
        ----------
        show_caret : bool
            Whether or not to show the caret.
        """
        if show_caret:
            ret_text = (
                self.message[: self.caret_pos] + "_" + self.message[self.caret_pos :]
            )
        else:
            ret_text = self.message
        ret_text = ret_text[self.window_left : self.window_right + 1]
        return ret_text 
[docs]
    @warn_on_args_to_kwargs()
    def render_text(self, *, show_caret=True):
        """Render text after processing.
        Parameters
        ----------
        show_caret : bool
            Whether or not to show the caret.
        """
        text = self.showable_text(show_caret)
        if text == "":
            text = "Enter Text"
        self.text.message = self.width_set_text(text) 
[docs]
    def edit_mode(self):
        """Turn on edit mode."""
        if self.init:
            self.message = ""
            self.init = False
            self.caret_pos = 0
        self.render_text() 
[docs]
    def left_button_press(self, i_ren, _obj, _textbox_object):
        """Handle left button press for textbox.
        Parameters
        ----------
        i_ren: :class:`CustomInteractorStyle`
        obj: :class:`vtkActor`
            The picked actor
        _textbox_object: :class:`TextBox2D`
        """
        i_ren.add_active_prop(self.text.actor)
        self.edit_mode()
        i_ren.force_render() 
[docs]
    def key_press(self, i_ren, _obj, _textbox_object):
        """Handle Key press for textboxself.
        Parameters
        ----------
        i_ren: :class:`CustomInteractorStyle`
        obj: :class:`vtkActor`
            The picked actor
        _textbox_object: :class:`TextBox2D`
        """
        key = i_ren.event.key
        key_char = i_ren.event.key_char
        is_done = self.handle_character(key, key_char)
        if is_done:
            i_ren.remove_active_prop(self.text.actor)
        i_ren.force_render() 
 
[docs]
class LineSlider2D(UI):
    """A 2D Line Slider.
    A sliding handle on a line with a percentage indicator.
    Attributes
    ----------
    line_width : int
        Width of the line on which the disk will slide.
    length : int
        Length of the slider.
    track : :class:`Rectangle2D`
        The line on which the slider's handle moves.
    handle : :class:`Disk2D`
        The moving part of the slider.
    text : :class:`TextBlock2D`
        The text that shows percentage.
    shape : string
        Describes the shape of the handle.
        Currently supports 'disk' and 'square'.
    default_color : (float, float, float)
        Color of the handle when in unpressed state.
    active_color : (float, float, float)
        Color of the handle when it is pressed.
    """
    @warn_on_args_to_kwargs()
    def __init__(
        self,
        *,
        center=(0, 0),
        initial_value=50,
        min_value=0,
        max_value=100,
        length=200,
        line_width=5,
        inner_radius=0,
        outer_radius=10,
        handle_side=20,
        font_size=16,
        orientation="horizontal",
        text_alignment="",
        text_template="{value:.1f} ({ratio:.0%})",
        shape="disk",
    ):
        """Init this UI element.
        Parameters
        ----------
        center : (float, float)
            Center of the slider's center.
        initial_value : float
            Initial value of the slider.
        min_value : float
            Minimum value of the slider.
        max_value : float
            Maximum value of the slider.
        length : int
            Length of the slider.
        line_width : int
            Width of the line on which the disk will slide.
        inner_radius : int
            Inner radius of the handles (if disk).
        outer_radius : int
            Outer radius of the handles (if disk).
        handle_side : int
            Side length of the handles (if square).
        font_size : int
            Size of the text to display alongside the slider (pt).
        orientation : str
            horizontal or vertical
        text_alignment : str
            define text alignment on a slider. Left (default)/ right for the
            vertical slider or top/bottom (default) for an horizontal slider.
        text_template : str, callable
            If str, text template can contain one or multiple of the
            replacement fields: `{value:}`, `{ratio:}`.
            If callable, this instance of `:class:LineSlider2D` will be
            passed as argument to the text template function.
        shape : string
            Describes the shape of the handle.
            Currently supports 'disk' and 'square'.
        """
        self.shape = shape
        self.orientation = orientation.lower().strip()
        self.align_dict = {
            "horizontal": ["top", "bottom"],
            "vertical": ["left", "right"],
        }
        self.default_color = (1, 1, 1)
        self.active_color = (0, 0, 1)
        self.alignment = text_alignment.lower()
        super(LineSlider2D, self).__init__()
        if self.orientation == "horizontal":
            self.alignment = "bottom" if not self.alignment else self.alignment
            self.track.width = length
            self.track.height = line_width
        elif self.orientation == "vertical":
            self.alignment = "left" if not self.alignment else self.alignment
            self.track.width = line_width
            self.track.height = length
        else:
            raise ValueError("Unknown orientation")
        if self.alignment not in self.align_dict[self.orientation]:
            raise ValueError(
                "Unknown alignment: choose from '{}' or '{}'".format(
                    *self.align_dict[self.orientation]
                )
            )
        if shape == "disk":
            self.handle.inner_radius = inner_radius
            self.handle.outer_radius = outer_radius
        elif shape == "square":
            self.handle.width = handle_side
            self.handle.height = handle_side
        self.center = center
        self.min_value = min_value
        self.max_value = max_value
        self.text.font_size = font_size
        self.text_template = text_template
        # Offer some standard hooks to the user.
        self.on_change = lambda ui: None
        self.on_value_changed = lambda ui: None
        self.on_moving_slider = lambda ui: None
        self.value = initial_value
        self.update()
    def _setup(self):
        """Setup this UI component.
        Create the slider's track (Rectangle2D), the handle (Disk2D) and
        the text (TextBlock2D).
        """
        # Slider's track
        self.track = Rectangle2D()
        self.track.color = (1, 0, 0)
        # Slider's handle
        if self.shape == "disk":
            self.handle = Disk2D(outer_radius=1)
        elif self.shape == "square":
            self.handle = Rectangle2D(size=(1, 1))
        self.handle.color = self.default_color
        # Slider Text
        self.text = TextBlock2D(justification="center", vertical_justification="top")
        # Add default events listener for this UI component.
        self.track.on_left_mouse_button_pressed = self.track_click_callback
        self.track.on_left_mouse_button_dragged = self.handle_move_callback
        self.track.on_left_mouse_button_released = self.handle_release_callback
        self.handle.on_left_mouse_button_dragged = self.handle_move_callback
        self.handle.on_left_mouse_button_released = self.handle_release_callback
    def _get_actors(self):
        """Get the actors composing this UI component."""
        return self.track.actors + self.handle.actors + self.text.actors
    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.
        Parameters
        ----------
        scene : scene
        """
        self.track.add_to_scene(scene)
        self.handle.add_to_scene(scene)
        self.text.add_to_scene(scene)
    def _get_size(self):
        # Consider the handle's size when computing the slider's size.
        width = None
        height = None
        if self.orientation == "horizontal":
            width = self.track.width + self.handle.size[0]
            height = max(self.track.height, self.handle.size[1])
        else:
            width = max(self.track.width, self.handle.size[0])
            height = self.track.height + self.handle.size[1]
        return np.array([width, height])
    def _set_position(self, coords):
        """Set the lower-left corner position of this UI component.
        Parameters
        ----------
        coords: (float, float)
            Absolute pixel coordinates (x, y).
        """
        # Offset the slider line by the handle's radius.
        track_position = coords + self.handle.size / 2.0
        if self.orientation == "horizontal":
            # Offset the slider line height by half the slider line width.
            track_position[1] -= self.track.size[1] / 2.0
        else:
            # Offset the slider line width by half the slider line height.
            track_position[0] += self.track.size[0] / 2.0
        self.track.position = track_position
        self.handle.position = self.handle.position.astype(float)
        self.handle.position += coords - self.position
        # Position the text below the handle.
        if self.orientation == "horizontal":
            align = 35 if self.alignment == "top" else -10
            self.text.position = (
                self.handle.center[0],
                self.handle.position[1] + align,
            )
        else:
            align = 70 if self.alignment == "right" else -35
            self.text.position = (
                self.handle.position[0] + align,
                self.handle.center[1] + 2,
            )
    @property
    def bottom_y_position(self):
        return self.track.position[1]
    @property
    def top_y_position(self):
        return self.track.position[1] + self.track.size[1]
    @property
    def left_x_position(self):
        return self.track.position[0]
    @property
    def right_x_position(self):
        return self.track.position[0] + self.track.size[0]
[docs]
    def set_position(self, position):
        """Set the disk's position.
        Parameters
        ----------
        position : (float, float)
            The absolute position of the disk (x, y).
        """
        # Move slider disk.
        if self.orientation == "horizontal":
            x_position = position[0]
            x_position = max(x_position, self.left_x_position)
            x_position = min(x_position, self.right_x_position)
            self.handle.center = (x_position, self.track.center[1])
        else:
            y_position = position[1]
            y_position = max(y_position, self.bottom_y_position)
            y_position = min(y_position, self.top_y_position)
            self.handle.center = (self.track.center[0], y_position)
        self.update()  # Update information. 
    @property
    def value(self):
        return self._value
    @value.setter
    def value(self, value):
        value_range = self.max_value - self.min_value
        self.ratio = (value - self.min_value) / value_range if value_range else 0
        self.on_value_changed(self)
    @property
    def ratio(self):
        return self._ratio
    @ratio.setter
    def ratio(self, ratio):
        position_x = self.left_x_position + ratio * self.track.width
        position_y = self.bottom_y_position + ratio * self.track.height
        self.set_position((position_x, position_y))
[docs]
    def format_text(self):
        """Return formatted text to display along the slider."""
        if callable(self.text_template):
            return self.text_template(self)
        return self.text_template.format(ratio=self.ratio, value=self.value) 
[docs]
    def update(self):
        """Update the slider."""
        # Compute the ratio determined by the position of the slider disk.
        disk_position_x = None
        disk_position_y = None
        if self.orientation == "horizontal":
            length = float(self.right_x_position - self.left_x_position)
            length = np.round(length, decimals=6)
            if length != self.track.width:
                raise ValueError("Disk position outside the slider line")
            disk_position_x = self.handle.center[0]
            self._ratio = (disk_position_x - self.left_x_position) / length
        else:
            length = float(self.top_y_position - self.bottom_y_position)
            if length != self.track.height:
                raise ValueError("Disk position outside the slider line")
            disk_position_y = self.handle.center[1]
            self._ratio = (disk_position_y - self.bottom_y_position) / length
        # Compute the selected value considering min_value and max_value.
        value_range = self.max_value - self.min_value
        self._value = self.min_value + self.ratio * value_range
        # Update text.
        text = self.format_text()
        self.text.message = text
        # Move the text below the slider's handle.
        if self.orientation == "horizontal":
            self.text.position = (disk_position_x, self.text.position[1])
        else:
            self.text.position = (self.text.position[0], disk_position_y)
        self.on_change(self) 
[docs]
    def track_click_callback(self, i_ren, _vtkactor, _slider):
        """Update disk position and grab the focus.
        Parameters
        ----------
        i_ren : :class:`CustomInteractorStyle`
        vtkactor : :class:`vtkActor`
            The picked actor
        _slider : :class:`LineSlider2D`
        """
        position = i_ren.event.position
        self.set_position(position)
        self.on_moving_slider(self)
        i_ren.force_render()
        i_ren.event.abort()  # Stop propagating the event. 
[docs]
    def handle_move_callback(self, i_ren, _vtkactor, _slider):
        """Handle movement.
        Parameters
        ----------
        i_ren : :class:`CustomInteractorStyle`
        vtkactor : :class:`vtkActor`
            The picked actor
        slider : :class:`LineSlider2D`
        """
        self.handle.color = self.active_color
        position = i_ren.event.position
        self.set_position(position)
        self.on_moving_slider(self)
        i_ren.force_render()
        i_ren.event.abort()  # Stop propagating the event. 
[docs]
    def handle_release_callback(self, i_ren, _vtkactor, _slider):
        """Change color when handle is released.
        Parameters
        ----------
        i_ren : :class:`CustomInteractorStyle`
        vtkactor : :class:`vtkActor`
            The picked actor
        slider : :class:`LineSlider2D`
        """
        self.handle.color = self.default_color
        i_ren.force_render() 
 
[docs]
class LineDoubleSlider2D(UI):
    """A 2D Line Slider with two sliding rings.
    Useful for setting min and max values for something.
    Currently supports:
    - Setting positions of both disks.
    Attributes
    ----------
    line_width : int
        Width of the line on which the disk will slide.
    length : int
        Length of the slider.
    track : :class:`vtkActor`
        The line on which the handles move.
    handles : [:class:`vtkActor`, :class:`vtkActor`]
        The moving slider disks.
    text : [:class:`TextBlock2D`, :class:`TextBlock2D`]
        The texts that show the values of the disks.
    shape : string
        Describes the shape of the handle.
        Currently supports 'disk' and 'square'.
    default_color : (float, float, float)
        Color of the handles when in unpressed state.
    active_color : (float, float, float)
        Color of the handles when they are pressed.
    """
    @warn_on_args_to_kwargs()
    def __init__(
        self,
        *,
        line_width=5,
        inner_radius=0,
        outer_radius=10,
        handle_side=20,
        center=(450, 300),
        length=200,
        initial_values=(0, 100),
        min_value=0,
        max_value=100,
        font_size=16,
        text_template="{value:.1f}",
        orientation="horizontal",
        shape="disk",
    ):
        """Init this UI element.
        Parameters
        ----------
        line_width : int
            Width of the line on which the disk will slide.
        inner_radius : int
            Inner radius of the handles (if disk).
        outer_radius : int
            Outer radius of the handles (if disk).
        handle_side : int
            Side length of the handles (if square).
        center : (float, float)
            Center of the slider.
        length : int
            Length of the slider.
        initial_values : (float, float)
            Initial values of the two handles.
        min_value : float
            Minimum value of the slider.
        max_value : float
            Maximum value of the slider.
        font_size : int
            Size of the text to display alongside the slider (pt).
        text_template : str, callable
            If str, text template can contain one or multiple of the
            replacement fields: `{value:}`, `{ratio:}`.
            If callable, this instance of `:class:LineDoubleSlider2D` will be
            passed as argument to the text template function.
        orientation : str
            horizontal or vertical
        shape : string
            Describes the shape of the handle.
            Currently supports 'disk' and 'square'.
        """
        self.shape = shape
        self.default_color = (1, 1, 1)
        self.active_color = (0, 0, 1)
        self.orientation = orientation.lower()
        super(LineDoubleSlider2D, self).__init__()
        if self.orientation == "horizontal":
            self.track.width = length
            self.track.height = line_width
        elif self.orientation == "vertical":
            self.track.width = line_width
            self.track.height = length
        else:
            raise ValueError("Unknown orientation")
        self.center = center
        if shape == "disk":
            self.handles[0].inner_radius = inner_radius
            self.handles[0].outer_radius = outer_radius
            self.handles[1].inner_radius = inner_radius
            self.handles[1].outer_radius = outer_radius
        elif shape == "square":
            self.handles[0].width = handle_side
            self.handles[0].height = handle_side
            self.handles[1].width = handle_side
            self.handles[1].height = handle_side
        self.min_value = min_value
        self.max_value = max_value
        self.text[0].font_size = font_size
        self.text[1].font_size = font_size
        self.text_template = text_template
        # Offer some standard hooks to the user.
        self.on_change = lambda ui: None
        self.on_value_changed = lambda ui: None
        self.on_moving_slider = lambda ui: None
        # Setting the handle positions will also update everything.
        self._values = [initial_values[0], initial_values[1]]
        self._ratio = [None, None]
        self.left_disk_value = initial_values[0]
        self.right_disk_value = initial_values[1]
        self.bottom_disk_value = initial_values[0]
        self.top_disk_value = initial_values[1]
    def _setup(self):
        """Setup this UI component.
        Create the slider's track (Rectangle2D), the handles (Disk2D) and
        the text (TextBlock2D).
        """
        # Slider's track
        self.track = Rectangle2D()
        self.track.color = (1, 0, 0)
        # Handles
        self.handles = []
        if self.shape == "disk":
            self.handles.append(Disk2D(outer_radius=1))
            self.handles.append(Disk2D(outer_radius=1))
        elif self.shape == "square":
            self.handles.append(Rectangle2D(size=(1, 1)))
            self.handles.append(Rectangle2D(size=(1, 1)))
        self.handles[0].color = self.default_color
        self.handles[1].color = self.default_color
        # Slider Text
        self.text = [
            TextBlock2D(justification="center", vertical_justification="top"),
            TextBlock2D(justification="center", vertical_justification="top"),
        ]
        # Add default events listener for this UI component.
        self.track.on_left_mouse_button_dragged = self.handle_move_callback
        self.handles[0].on_left_mouse_button_dragged = self.handle_move_callback
        self.handles[1].on_left_mouse_button_dragged = self.handle_move_callback
        self.handles[0].on_left_mouse_button_released = self.handle_release_callback
        self.handles[1].on_left_mouse_button_released = self.handle_release_callback
    def _get_actors(self):
        """Get the actors composing this UI component."""
        return (
            self.track.actors
            + self.handles[0].actors
            + self.handles[1].actors
            + self.text[0].actors
            + self.text[1].actors
        )
    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.
        Parameters
        ----------
        scene : scene
        """
        self.track.add_to_scene(scene)
        self.handles[0].add_to_scene(scene)
        self.handles[1].add_to_scene(scene)
        self.text[0].add_to_scene(scene)
        self.text[1].add_to_scene(scene)
    def _get_size(self):
        # Consider the handle's size when computing the slider's size.
        width = None
        height = None
        if self.orientation == "horizontal":
            width = self.track.width + self.handles[0].size[0]
            height = max(self.track.height, self.handles[0].size[1])
        else:
            width = max(self.track.width, self.handles[0].size[0])
            height = self.track.height + self.handles[0].size[1]
        return np.array([width, height])
    def _set_position(self, coords):
        """Set the lower-left corner position of this UI component.
        Parameters
        ----------
        coords: (float, float)
            Absolute pixel coordinates (x, y).
        """
        # Offset the slider line by the handle's radius.
        track_position = coords
        if self.orientation == "horizontal":
            # Offset the slider line height by half the slider line width.
            track_position[1] -= self.track.size[1] / 2.0
        else:
            # Offset the slider line width by half the slider line height.
            track_position[0] -= self.track.size[0] / 2.0
        self.track.position = track_position
        self.handles[0].position = self.handles[0].position.astype(float)
        self.handles[1].position = self.handles[1].position.astype(float)
        self.handles[0].position += coords - self.position
        self.handles[1].position += coords - self.position
        if self.orientation == "horizontal":
            # Position the text below the handles.
            self.text[0].position = (
                self.handles[0].center[0],
                self.handles[0].position[1] - 10,
            )
            self.text[1].position = (
                self.handles[1].center[0],
                self.handles[1].position[1] - 10,
            )
        else:
            # Position the text to the left of the handles.
            self.text[0].position = (
                self.handles[0].center[0] - 35,
                self.handles[0].position[1],
            )
            self.text[1].position = (
                self.handles[1].center[0] - 35,
                self.handles[1].position[1],
            )
    @property
    def bottom_y_position(self):
        return self.track.position[1]
    @property
    def top_y_position(self):
        return self.track.position[1] + self.track.size[1]
    @property
    def left_x_position(self):
        return self.track.position[0]
    @property
    def right_x_position(self):
        return self.track.position[0] + self.track.size[0]
[docs]
    def value_to_ratio(self, value):
        """Convert the value of a disk to the ratio.
        Parameters
        ----------
        value : float
        """
        value_range = self.max_value - self.min_value
        return (value - self.min_value) / value_range if value_range else 0 
[docs]
    def ratio_to_coord(self, ratio):
        """Convert the ratio to the absolute coordinate.
        Parameters
        ----------
        ratio : float
        """
        if self.orientation == "horizontal":
            return self.left_x_position + ratio * self.track.width
        return self.bottom_y_position + ratio * self.track.height 
[docs]
    def coord_to_ratio(self, coord):
        """Convert the x coordinate of a disk to the ratio.
        Parameters
        ----------
        coord : float
        """
        if self.orientation == "horizontal":
            return (coord - self.left_x_position) / float(self.track.width)
        return (coord - self.bottom_y_position) / float(self.track.height) 
[docs]
    def ratio_to_value(self, ratio):
        """Convert the ratio to the value of the disk.
        Parameters
        ----------
        ratio : float
        """
        value_range = self.max_value - self.min_value
        return self.min_value + ratio * value_range 
[docs]
    def set_position(self, position, disk_number):
        """Set the disk's position.
        Parameters
        ----------
        position : (float, float)
            The absolute position of the disk (x, y).
        disk_number : int
            The index of disk being moved.
        """
        if self.orientation == "horizontal":
            x_position = position[0]
            if disk_number == 0 and x_position >= self.handles[1].center[0]:
                x_position = self.ratio_to_coord(
                    self.value_to_ratio(self._values[1] - 1)
                )
            if disk_number == 1 and x_position <= self.handles[0].center[0]:
                x_position = self.ratio_to_coord(
                    self.value_to_ratio(self._values[0] + 1)
                )
            x_position = max(x_position, self.left_x_position)
            x_position = min(x_position, self.right_x_position)
            self.handles[disk_number].center = (x_position, self.track.center[1])
        else:
            y_position = position[1]
            if disk_number == 0 and y_position >= self.handles[1].center[1]:
                y_position = self.ratio_to_coord(
                    self.value_to_ratio(self._values[1] - 1)
                )
            if disk_number == 1 and y_position <= self.handles[0].center[1]:
                y_position = self.ratio_to_coord(
                    self.value_to_ratio(self._values[0] + 1)
                )
            y_position = max(y_position, self.bottom_y_position)
            y_position = min(y_position, self.top_y_position)
            self.handles[disk_number].center = (self.track.center[0], y_position)
        self.update(disk_number) 
    @property
    def bottom_disk_value(self):
        """Return the value of the bottom disk."""
        return self._values[0]
    @bottom_disk_value.setter
    def bottom_disk_value(self, bottom_disk_value):
        """Set the value of the bottom disk.
        Parameters
        ----------
        bottom_disk_value : float
            New value for the bottom disk.
        """
        self.bottom_disk_ratio = self.value_to_ratio(bottom_disk_value)
    @property
    def top_disk_value(self):
        """Return the value of the top disk."""
        return self._values[1]
    @top_disk_value.setter
    def top_disk_value(self, top_disk_value):
        """Set the value of the top disk.
        Parameters
        ----------
        top_disk_value : float
            New value for the top disk.
        """
        self.top_disk_ratio = self.value_to_ratio(top_disk_value)
    @property
    def left_disk_value(self):
        """Return the value of the left disk."""
        return self._values[0]
    @left_disk_value.setter
    def left_disk_value(self, left_disk_value):
        """Set the value of the left disk.
        Parameters
        ----------
        left_disk_value : float
            New value for the left disk.
        """
        self.left_disk_ratio = self.value_to_ratio(left_disk_value)
        self.on_value_changed(self)
    @property
    def right_disk_value(self):
        """Return the value of the right disk."""
        return self._values[1]
    @right_disk_value.setter
    def right_disk_value(self, right_disk_value):
        """Set the value of the right disk.
        Parameters
        ----------
        right_disk_value : float
            New value for the right disk.
        """
        self.right_disk_ratio = self.value_to_ratio(right_disk_value)
        self.on_value_changed(self)
    @property
    def bottom_disk_ratio(self):
        """Return the ratio of the bottom disk."""
        return self._ratio[0]
    @bottom_disk_ratio.setter
    def bottom_disk_ratio(self, bottom_disk_ratio):
        """Set the ratio of the bottom disk.
        Parameters
        ----------
        bottom_disk_ratio : float
            New ratio for the bottom disk.
        """
        position_x = self.ratio_to_coord(bottom_disk_ratio)
        position_y = self.ratio_to_coord(bottom_disk_ratio)
        self.set_position((position_x, position_y), 0)
    @property
    def top_disk_ratio(self):
        """Return the ratio of the top disk."""
        return self._ratio[1]
    @top_disk_ratio.setter
    def top_disk_ratio(self, top_disk_ratio):
        """Set the ratio of the top disk.
        Parameters
        ----------
        top_disk_ratio : float
            New ratio for the top disk.
        """
        position_x = self.ratio_to_coord(top_disk_ratio)
        position_y = self.ratio_to_coord(top_disk_ratio)
        self.set_position((position_x, position_y), 1)
    @property
    def left_disk_ratio(self):
        """Return the ratio of the left disk."""
        return self._ratio[0]
    @left_disk_ratio.setter
    def left_disk_ratio(self, left_disk_ratio):
        """Set the ratio of the left disk.
        Parameters
        ----------
        left_disk_ratio : float
            New ratio for the left disk.
        """
        position_x = self.ratio_to_coord(left_disk_ratio)
        position_y = self.ratio_to_coord(left_disk_ratio)
        self.set_position((position_x, position_y), 0)
    @property
    def right_disk_ratio(self):
        """Return the ratio of the right disk."""
        return self._ratio[1]
    @right_disk_ratio.setter
    def right_disk_ratio(self, right_disk_ratio):
        """Set the ratio of the right disk.
        Parameters
        ----------
        right_disk_ratio : float
            New ratio for the right disk.
        """
        position_x = self.ratio_to_coord(right_disk_ratio)
        position_y = self.ratio_to_coord(right_disk_ratio)
        self.set_position((position_x, position_y), 1)
[docs]
    def format_text(self, disk_number):
        """Return formatted text to display along the slider.
        Parameters
        ----------
        disk_number : int
            Index of the disk.
        """
        if callable(self.text_template):
            return self.text_template(self)
        return self.text_template.format(value=self._values[disk_number]) 
[docs]
    def update(self, disk_number):
        """Update the slider.
        Parameters
        ----------
        disk_number : int
            Index of the disk to be updated.
        """
        # Compute the ratio determined by the position of the slider disk.
        if self.orientation == "horizontal":
            self._ratio[disk_number] = self.coord_to_ratio(
                self.handles[disk_number].center[0]
            )
        else:
            self._ratio[disk_number] = self.coord_to_ratio(
                self.handles[disk_number].center[1]
            )
        # Compute the selected value considering min_value and max_value.
        self._values[disk_number] = self.ratio_to_value(self._ratio[disk_number])
        # Update text.
        text = self.format_text(disk_number)
        self.text[disk_number].message = text
        if self.orientation == "horizontal":
            self.text[disk_number].position = (
                self.handles[disk_number].center[0],
                self.text[disk_number].position[1],
            )
        else:
            self.text[disk_number].position = (
                self.text[disk_number].position[0],
                self.handles[disk_number].center[1],
            )
        self.on_change(self) 
[docs]
    def handle_move_callback(self, i_ren, vtkactor, _slider):
        """Handle movement.
        Parameters
        ----------
        i_ren : :class:`CustomInteractorStyle`
        vtkactor : :class:`vtkActor`
            The picked actor
        _slider : :class:`LineDoubleSlider2D`
        """
        position = i_ren.event.position
        if vtkactor == self.handles[0].actors[0]:
            self.set_position(position, 0)
            self.handles[0].color = self.active_color
        elif vtkactor == self.handles[1].actors[0]:
            self.set_position(position, 1)
            self.handles[1].color = self.active_color
        self.on_moving_slider(self)
        i_ren.force_render()
        i_ren.event.abort()  # Stop propagating the event. 
[docs]
    def handle_release_callback(self, i_ren, vtkactor, _slider):
        """Change color when handle is released.
        Parameters
        ----------
        i_ren : :class:`CustomInteractorStyle`
        vtkactor : :class:`vtkActor`
            The picked actor
        _slider : :class:`LineDoubleSlider2D`
        """
        if vtkactor == self.handles[0].actors[0]:
            self.handles[0].color = self.default_color
        elif vtkactor == self.handles[1].actors[0]:
            self.handles[1].color = self.default_color
        i_ren.force_render() 
 
[docs]
class RingSlider2D(UI):
    """A disk slider.
    A disk moves along the boundary of a ring.
    Goes from 0-360 degrees.
    Attributes
    ----------
    mid_track_radius: float
        Distance from the center of the slider to the middle of the track.
    previous_value: float
        Value of Rotation of the actor before the current value.
    track : :class:`Disk2D`
        The circle on which the slider's handle moves.
    handle : :class:`Disk2D`
        The moving part of the slider.
    text : :class:`TextBlock2D`
        The text that shows percentage.
    default_color : (float, float, float)
        Color of the handle when in unpressed state.
    active_color : (float, float, float)
        Color of the handle when it is pressed.
    """
    @warn_on_args_to_kwargs()
    def __init__(
        self,
        *,
        center=(0, 0),
        initial_value=180,
        min_value=0,
        max_value=360,
        slider_inner_radius=40,
        slider_outer_radius=44,
        handle_inner_radius=0,
        handle_outer_radius=10,
        font_size=16,
        text_template="{ratio:.0%}",
    ):
        """Init this UI element.
        Parameters
        ----------
        center : (float, float)
            Position (x, y) of the slider's center.
        initial_value : float
            Initial value of the slider.
        min_value : float
            Minimum value of the slider.
        max_value : float
            Maximum value of the slider.
        slider_inner_radius : int
            Inner radius of the base disk.
        slider_outer_radius : int
            Outer radius of the base disk.
        handle_outer_radius : int
            Outer radius of the slider's handle.
        handle_inner_radius : int
            Inner radius of the slider's handle.
        font_size : int
            Size of the text to display alongside the slider (pt).
        text_template : str, callable
            If str, text template can contain one or multiple of the
            replacement fields: `{value:}`, `{ratio:}`, `{angle:}`.
            If callable, this instance of `:class:RingSlider2D` will be
            passed as argument to the text template function.
        """
        self.default_color = (1, 1, 1)
        self.active_color = (0, 0, 1)
        super(RingSlider2D, self).__init__()
        self.track.inner_radius = slider_inner_radius
        self.track.outer_radius = slider_outer_radius
        self.handle.inner_radius = handle_inner_radius
        self.handle.outer_radius = handle_outer_radius
        self.center = center
        self.min_value = min_value
        self.max_value = max_value
        self.text.font_size = font_size
        self.text_template = text_template
        # Offer some standard hooks to the user.
        self.on_change = lambda ui: None
        self.on_value_changed = lambda ui: None
        self.on_moving_slider = lambda ui: None
        self._value = initial_value
        self.value = initial_value
        self._previous_value = initial_value
        self._angle = 0
        self._ratio = self.angle / TWO_PI
    def _setup(self):
        """Setup this UI component.
        Create the slider's circle (Disk2D), the handle (Disk2D) and
        the text (TextBlock2D).
        """
        # Slider's track.
        self.track = Disk2D(outer_radius=1)
        self.track.color = (1, 0, 0)
        # Slider's handle.
        self.handle = Disk2D(outer_radius=1)
        self.handle.color = self.default_color
        # Slider Text
        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
        self.track.on_left_mouse_button_dragged = self.handle_move_callback
        self.track.on_left_mouse_button_released = self.handle_release_callback
        self.handle.on_left_mouse_button_dragged = self.handle_move_callback
        self.handle.on_left_mouse_button_released = self.handle_release_callback
    def _get_actors(self):
        """Get the actors composing this UI component."""
        return self.track.actors + self.handle.actors + self.text.actors
    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.
        Parameters
        ----------
        scene : scene
        """
        self.track.add_to_scene(scene)
        self.handle.add_to_scene(scene)
        self.text.add_to_scene(scene)
    def _get_size(self):
        return self.track.size + self.handle.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.track.position = coords + self.handle.size / 2.0
        self.handle.position += coords - self.position
        # Position the text in the center of the slider's track.
        self.text.position = coords + self.size / 2.0
    @property
    def mid_track_radius(self):
        return (self.track.inner_radius + self.track.outer_radius) / 2.0
    @property
    def value(self):
        return self._value
    @value.setter
    def value(self, value):
        value_range = self.max_value - self.min_value
        self.ratio = (value - self.min_value) / value_range if value_range else 0
        self.on_value_changed(self)
    @property
    def previous_value(self):
        return self._previous_value
    @property
    def ratio(self):
        return self._ratio
    @ratio.setter
    def ratio(self, ratio):
        self.angle = ratio * TWO_PI
    @property
    def angle(self):
        """Return Angle (in rad) the handle makes with x-axis."""
        return self._angle
    @angle.setter
    def angle(self, angle):
        self._angle = angle % TWO_PI  # Wraparound
        self.update()
[docs]
    def format_text(self):
        """Return formatted text to display along the slider."""
        if callable(self.text_template):
            return self.text_template(self)
        return self.text_template.format(
            ratio=self.ratio, value=self.value, angle=np.rad2deg(self.angle)
        ) 
[docs]
    def update(self):
        """Update the slider."""
        # Compute the ratio determined by the position of the slider disk.
        self._ratio = self.angle / TWO_PI
        # Compute the selected value considering min_value and max_value.
        value_range = self.max_value - self.min_value
        self._previous_value = self.value
        self._value = self.min_value + self.ratio * value_range
        # Update text disk actor.
        x = self.mid_track_radius * np.cos(self.angle) + self.center[0]
        y = self.mid_track_radius * np.sin(self.angle) + self.center[1]
        self.handle.center = (x, y)
        # Update text.
        text = self.format_text()
        self.text.message = text
        self.on_change(self)  # Call hook. 
[docs]
    def move_handle(self, click_position):
        """Move the slider's handle.
        Parameters
        ----------
        click_position: (float, float)
            Position of the mouse click.
        """
        x, y = np.array(click_position) - self.center
        angle = np.arctan2(y, x)
        if angle < 0:
            angle += TWO_PI
        self.angle = angle 
[docs]
    def track_click_callback(self, i_ren, _obj, _slider):
        """Update disk position and grab the focus.
        Parameters
        ----------
        i_ren : :class:`CustomInteractorStyle`
        obj : :class:`vtkActor`
            The picked actor
        _slider : :class:`RingSlider2D`
        """
        click_position = i_ren.event.position
        self.move_handle(click_position=click_position)
        self.on_moving_slider(self)
        i_ren.force_render()
        i_ren.event.abort()  # Stop propagating the event. 
[docs]
    def handle_move_callback(self, i_ren, _obj, _slider):
        """Move the slider's handle.
        Parameters
        ----------
        i_ren : :class:`CustomInteractorStyle`
        obj : :class:`vtkActor`
            The picked actor
        _slider : :class:`RingSlider2D`
        """
        click_position = i_ren.event.position
        self.handle.color = self.active_color
        self.move_handle(click_position=click_position)
        self.on_moving_slider(self)
        i_ren.force_render()
        i_ren.event.abort()  # Stop propagating the event. 
[docs]
    def handle_release_callback(self, i_ren, _obj, _slider):
        """Change color when handle is released.
        Parameters
        ----------
        i_ren : :class:`CustomInteractorStyle`
        vtkactor : :class:`vtkActor`
            The picked actor
        _slider : :class:`RingSlider2D`
        """
        self.handle.color = self.default_color
        i_ren.force_render() 
 
[docs]
class RangeSlider(UI):
    """A set of a LineSlider2D and a LineDoubleSlider2D.
    The double slider is used to set the min and max value
    for the LineSlider2D
    Attributes
    ----------
    range_slider_center : (float, float)
        Center of the LineDoubleSlider2D object.
    value_slider_center : (float, float)
        Center of the LineSlider2D object.
    range_slider : :class:`LineDoubleSlider2D`
        The line slider which sets the min and max values
    value_slider : :class:`LineSlider2D`
        The line slider which sets the value
    """
    @warn_on_args_to_kwargs()
    def __init__(
        self,
        *,
        line_width=5,
        inner_radius=0,
        outer_radius=10,
        handle_side=20,
        range_slider_center=(450, 400),
        value_slider_center=(450, 300),
        length=200,
        min_value=0,
        max_value=100,
        font_size=16,
        range_precision=1,
        orientation="horizontal",
        value_precision=2,
        shape="disk",
    ):
        """Init this class instance.
        Parameters
        ----------
        line_width : int
            Width of the slider tracks
        inner_radius : int
            Inner radius of the handles.
        outer_radius : int
            Outer radius of the handles.
        handle_side : int
            Side length of the handles (if square).
        range_slider_center : (float, float)
            Center of the LineDoubleSlider2D object.
        value_slider_center : (float, float)
            Center of the LineSlider2D object.
        length : int
            Length of the sliders.
        min_value : float
            Minimum value of the double slider.
        max_value : float
            Maximum value of the double slider.
        font_size : int
            Size of the text to display alongside the sliders (pt).
        range_precision : int
            Number of decimal places to show the min and max values set.
        orientation : str
            horizontal or vertical
        value_precision : int
            Number of decimal places to show the value set on slider.
        shape : string
            Describes the shape of the handle.
            Currently supports 'disk' and 'square'.
        """
        self.min_value = min_value
        self.max_value = max_value
        self.inner_radius = inner_radius
        self.outer_radius = outer_radius
        self.handle_side = handle_side
        self.length = length
        self.line_width = line_width
        self.font_size = font_size
        self.shape = shape
        self.orientation = orientation.lower()
        self.range_slider_text_template = "{value:." + str(range_precision) + "f}"
        self.value_slider_text_template = "{value:." + str(value_precision) + "f}"
        self.range_slider_center = range_slider_center
        self.value_slider_center = value_slider_center
        super(RangeSlider, self).__init__()
    def _setup(self):
        """Setup this UI component."""
        self.range_slider = LineDoubleSlider2D(
            line_width=self.line_width,
            inner_radius=self.inner_radius,
            outer_radius=self.outer_radius,
            handle_side=self.handle_side,
            center=self.range_slider_center,
            length=self.length,
            min_value=self.min_value,
            max_value=self.max_value,
            initial_values=(self.min_value, self.max_value),
            font_size=self.font_size,
            shape=self.shape,
            orientation=self.orientation,
            text_template=self.range_slider_text_template,
        )
        self.value_slider = LineSlider2D(
            line_width=self.line_width,
            length=self.length,
            inner_radius=self.inner_radius,
            outer_radius=self.outer_radius,
            handle_side=self.handle_side,
            center=self.value_slider_center,
            min_value=self.min_value,
            max_value=self.max_value,
            initial_value=(self.min_value + self.max_value) / 2,
            font_size=self.font_size,
            shape=self.shape,
            orientation=self.orientation,
            text_template=self.value_slider_text_template,
        )
        # Add default events listener for this UI component.
        self.range_slider.handles[
            0
        ].on_left_mouse_button_dragged = self.range_slider_handle_move_callback
        self.range_slider.handles[
            1
        ].on_left_mouse_button_dragged = self.range_slider_handle_move_callback
    def _get_actors(self):
        """Get the actors composing this UI component."""
        return self.range_slider.actors + self.value_slider.actors
    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.
        Parameters
        ----------
        scene : scene
        """
        self.range_slider.add_to_scene(scene)
        self.value_slider.add_to_scene(scene)
    def _get_size(self):
        return self.range_slider.size + self.value_slider.size
    def _set_position(self, coords):
        pass
[docs]
    def range_slider_handle_move_callback(self, i_ren, obj, _slider):
        """Update range_slider's handles.
        Parameters
        ----------
        i_ren : :class:`CustomInteractorStyle`
        obj : :class:`vtkActor`
            The picked actor
        _slider : :class:`RangeSlider`
        """
        position = i_ren.event.position
        if obj == self.range_slider.handles[0].actors[0]:
            self.range_slider.handles[0].color = self.range_slider.active_color
            self.range_slider.set_position(position, 0)
            self.value_slider.min_value = self.range_slider.left_disk_value
            self.value_slider.update()
        elif obj == self.range_slider.handles[1].actors[0]:
            self.range_slider.handles[1].color = self.range_slider.active_color
            self.range_slider.set_position(position, 1)
            self.value_slider.max_value = self.range_slider.right_disk_value
            self.value_slider.update()
        i_ren.force_render()
        i_ren.event.abort()  # Stop propagating the event. 
 
[docs]
class Option(UI):
    """A set of a Button2D and a TextBlock2D to act as a single option
    for checkboxes and radio buttons.
    Clicking the button toggles its checked/unchecked status.
    Attributes
    ----------
    label : str
        The label for the option.
    font_size : int
            Font Size of the label.
    """
    @warn_on_args_to_kwargs()
    def __init__(self, label, *, position=(0, 0), font_size=18, checked=False):
        """Init this class instance.
        Parameters
        ----------
        label : str
            Text to be displayed next to the option's button.
        position : (float, float)
            Absolute coordinates (x, y) of the lower-left corner of
            the button of the option.
        font_size : int
            Font size of the label.
        checked : bool, optional
            Boolean value indicates the initial state of the option
        """
        self.label = label
        self.font_size = font_size
        self.checked = checked
        self.button_size = (font_size * 1.2, font_size * 1.2)
        self.button_label_gap = 10
        super(Option, self).__init__(position=position)
        # Offer some standard hooks to the user.
        self.on_change = lambda obj: None
    def _setup(self):
        """Setup this UI component."""
        # Option's button
        self.button_icons = []
        self.button_icons.append(("unchecked", read_viz_icons(fname="stop2.png")))
        self.button_icons.append(("checked", read_viz_icons(fname="checkmark.png")))
        self.button = Button2D(icon_fnames=self.button_icons, size=self.button_size)
        self.text = TextBlock2D(text=self.label, font_size=self.font_size)
        # Display initial state
        if self.checked:
            self.button.set_icon_by_name("checked")
        # Add callbacks
        self.button.on_left_mouse_button_clicked = self.toggle
        self.text.on_left_mouse_button_clicked = self.toggle
    def _get_actors(self):
        """Get the actors composing this UI component."""
        return self.button.actors + self.text.actors
    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.
        Parameters
        ----------
        scene : scene
        """
        self.button.add_to_scene(scene)
        self.text.add_to_scene(scene)
    def _get_size(self):
        width = self.button.size[0] + self.button_label_gap + self.text.size[0]
        height = max(self.button.size[1], self.text.size[1])
        return np.array([width, height])
    def _set_position(self, coords):
        """Set the lower-left corner position of this UI component.
        Parameters
        ----------
        coords: (float, float)
            Absolute pixel coordinates (x, y).
        """
        num_newlines = self.label.count("\n")
        self.button.position = coords + (0, num_newlines * self.font_size * 0.5)
        offset = (self.button.size[0] + self.button_label_gap, 0)
        self.text.position = coords + offset
[docs]
    def toggle(self, i_ren, _obj, _element):
        if self.checked:
            self.deselect()
        else:
            self.select()
        self.on_change(self)
        i_ren.force_render() 
[docs]
    def select(self):
        self.checked = True
        self.button.set_icon_by_name("checked") 
[docs]
    def deselect(self):
        self.checked = False
        self.button.set_icon_by_name("unchecked") 
 
[docs]
class Checkbox(UI):
    """A 2D set of :class:'Option' objects.
    Multiple options can be selected.
    Attributes
    ----------
    labels : list(string)
        List of labels of each option.
    options : dict(Option)
        Dictionary of all the options in the checkbox set.
    padding : float
        Distance between two adjacent options
    """
    @warn_on_args_to_kwargs()
    def __init__(
        self,
        labels,
        *,
        checked_labels=(),
        padding=1,
        font_size=18,
        font_family="Arial",
        position=(0, 0),
    ):
        """Init this class instance.
        Parameters
        ----------
        labels : list(str)
            List of labels of each option.
        checked_labels: list(str), optional
            List of labels that are checked on setting up.
        padding : float, optional
            The distance between two adjacent options
        font_size : int, optional
            Size of the text font.
        font_family : str, optional
            Currently only supports Arial.
        position : (float, float), optional
            Absolute coordinates (x, y) of the lower-left corner of
            the button of the first option.
        """
        self.labels = list(reversed(list(labels)))
        self._padding = padding
        self._font_size = font_size
        self.font_family = font_family
        self.checked_labels = list(checked_labels)
        super(Checkbox, self).__init__(position=position)
        self.on_change = lambda checkbox: None
    def _setup(self):
        """Setup this UI component."""
        self.options = OrderedDict()
        button_y = self.position[1]
        for label in self.labels:
            option = Option(
                label=label,
                font_size=self.font_size,
                position=(self.position[0], button_y),
                checked=(label in self.checked_labels),
            )
            line_spacing = option.text.actor.GetTextProperty().GetLineSpacing()
            button_y = (
                button_y
                + self.font_size * (label.count("\n") + 1) * (line_spacing + 0.1)
                + self.padding
            )
            self.options[label] = option
            # Set callback
            option.on_change = self._handle_option_change
    def _get_actors(self):
        """Get the actors composing this UI component."""
        actors = []
        for option in self.options.values():
            actors = actors + option.actors
        return actors
    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.
        Parameters
        ----------
        scene : scene
        """
        for option in self.options.values():
            option.add_to_scene(scene)
    def _get_size(self):
        option_width, option_height = self.options.values()[0].get_size()
        height = len(self.labels) * (option_height + self.padding) - self.padding
        return np.asarray([option_width, height])
    def _handle_option_change(self, option):
        """Update whenever an option changes.
        Parameters
        ----------
        option : :class:`Option`
        """
        if option.checked:
            self.checked_labels.append(option.label)
        else:
            self.checked_labels.remove(option.label)
        self.on_change(self)
    def _set_position(self, coords):
        """Set the lower-left corner position of this UI component.
        Parameters
        ----------
        coords: (float, float)
            Absolute pixel coordinates (x, y).
        """
        button_y = coords[1]
        for option_no, option in enumerate(self.options.values()):
            option.position = (coords[0], button_y)
            line_spacing = option.text.actor.GetTextProperty().GetLineSpacing()
            button_y = (
                button_y
                + self.font_size
                * (self.labels[option_no].count("\n") + 1)
                * (line_spacing + 0.1)
                + self.padding
            )
    @property
    def font_size(self):
        """Gets the font size of text."""
        return self._font_size
    @property
    def padding(self):
        """Get the padding between options."""
        return self._padding 
[docs]
class ComboBox2D(UI):
    """UI element to create drop-down menus.
    Attributes
    ----------
    selection_box: :class: 'TextBox2D'
        Display selection and placeholder text.
    drop_down_button: :class: 'Button2D'
        Button to show or hide menu.
    drop_down_menu: :class: 'ListBox2D'
        Container for item list.
    """
    @warn_on_args_to_kwargs()
    def __init__(
        self,
        *,
        items=None,
        position=(0, 0),
        size=(300, 200),
        placeholder="Choose selection...",
        draggable=True,
        selection_text_color=(0, 0, 0),
        selection_bg_color=(1, 1, 1),
        menu_text_color=(0.2, 0.2, 0.2),
        selected_color=(0.9, 0.6, 0.6),
        unselected_color=(0.6, 0.6, 0.6),
        scroll_bar_active_color=(0.6, 0.2, 0.2),
        scroll_bar_inactive_color=(0.9, 0.0, 0.0),
        menu_opacity=1.0,
        reverse_scrolling=False,
        font_size=20,
        line_spacing=1.4,
    ):
        """Init class Instance.
        Parameters
        ----------
        items: list(string)
            List of items to be displayed as choices.
        position : (float, float)
            Absolute coordinates (x, y) of the lower-left corner of this
            UI component.
        size : (int, int)
            Width and height in pixels of this UI component.
        placeholder : str
            Holds the default text to be displayed.
        draggable: {True, False}
            Whether the UI element is draggable or not.
        selection_text_color : tuple of 3 floats
            Color of the selected text to be displayed.
        selection_bg_color : tuple of 3 floats
            Background color of the selection text.
        menu_text_color : tuple of 3 floats.
            Color of the options displayed in drop down menu.
        selected_color : tuple of 3 floats.
            Background color of the selected option in drop down menu.
        unselected_color : tuple of 3 floats.
            Background color of the unselected option in drop down menu.
        scroll_bar_active_color : tuple of 3 floats.
            Color of the scrollbar when in active use.
        scroll_bar_inactive_color : tuple of 3 floats.
            Color of the scrollbar when inactive.
        reverse_scrolling: {True, False}
            If True, scrolling up will move the list of files down.
        font_size: int
            The font size of selected text in pixels.
        line_spacing: float
            Distance between drop down menu's items in pixels.
        """
        if items is None:
            items = []
        self.items = items.copy()
        self.font_size = font_size
        self.reverse_scrolling = reverse_scrolling
        self.line_spacing = line_spacing
        self.panel_size = size
        self._selection = placeholder
        self._menu_visibility = False
        self._selection_ID = None
        self.draggable = draggable
        self.sel_text_color = selection_text_color
        self.sel_bg_color = selection_bg_color
        self.menu_txt_color = menu_text_color
        self.selected_color = selected_color
        self.unselected_color = unselected_color
        self.scroll_active_color = scroll_bar_active_color
        self.scroll_inactive_color = scroll_bar_inactive_color
        self.menu_opacity = menu_opacity
        # Define subcomponent sizes.
        self.text_block_size = (int(0.9 * size[0]), int(0.1 * size[1]))
        self.drop_menu_size = (int(0.9 * size[0]), int(0.7 * size[1]))
        self.drop_button_size = (int(0.1 * size[0]), int(0.1 * size[1]))
        self._icon_files = [
            ("left", read_viz_icons(fname="circle-left.png")),
            ("down", read_viz_icons(fname="circle-down.png")),
        ]
        super(ComboBox2D, self).__init__()
        self.position = position
    def _setup(self):
        """Setup this UI component.
        Create the ListBox filled with empty slots (ListBoxItem2D).
        Create TextBox with placeholder text.
        Create Button for toggling drop down menu.
        """
        self.selection_box = TextBlock2D(
            size=self.text_block_size,
            color=self.sel_text_color,
            bg_color=self.sel_bg_color,
            text=self._selection,
        )
        self.drop_down_button = Button2D(
            icon_fnames=self._icon_files, size=self.drop_button_size
        )
        self.drop_down_menu = ListBox2D(
            values=self.items,
            multiselection=False,
            font_size=self.font_size,
            line_spacing=self.line_spacing,
            text_color=self.menu_txt_color,
            selected_color=self.selected_color,
            unselected_color=self.unselected_color,
            scroll_bar_active_color=self.scroll_active_color,
            scroll_bar_inactive_color=self.scroll_inactive_color,
            background_opacity=self.menu_opacity,
            reverse_scrolling=self.reverse_scrolling,
            size=self.drop_menu_size,
        )
        self.drop_down_menu.set_visibility(False)
        self.panel = Panel2D(self.panel_size, opacity=0.0)
        self.panel.add_element(self.selection_box, (0.001, 0.7))
        self.panel.add_element(self.drop_down_button, (0.8, 0.7))
        self.panel.add_element(self.drop_down_menu, (0, 0))
        if self.draggable:
            self.drop_down_button.on_left_mouse_button_dragged = (
                self.left_button_dragged
            )
            self.drop_down_menu.panel.background.on_left_mouse_button_dragged = (
                self.left_button_dragged
            )
            self.selection_box.on_left_mouse_button_dragged = self.left_button_dragged
            self.selection_box.background.on_left_mouse_button_dragged = (
                self.left_button_dragged
            )
            self.drop_down_button.on_left_mouse_button_pressed = (
                self.left_button_pressed
            )
            self.drop_down_menu.panel.background.on_left_mouse_button_pressed = (
                self.left_button_pressed
            )
            self.selection_box.on_left_mouse_button_pressed = self.left_button_pressed
            self.selection_box.background.on_left_mouse_button_pressed = (
                self.left_button_pressed
            )
        else:
            self.panel.background.on_left_mouse_button_dragged = (
                lambda i_ren, _obj, _comp: i_ren.force_render
            )
            self.drop_down_menu.panel.background.on_left_mouse_button_dragged = (
                lambda i_ren, _obj, _comp: i_ren.force_render
            )
        # Handle mouse wheel events on the slots.
        for slot in self.drop_down_menu.slots:
            slot.add_callback(
                slot.textblock.actor,
                "LeftButtonPressEvent",
                self.select_option_callback,
            )
            slot.add_callback(
                slot.background.actor,
                "LeftButtonPressEvent",
                self.select_option_callback,
            )
        self.drop_down_button.on_left_mouse_button_clicked = self.menu_toggle_callback
        # Offer some standard hooks to the user.
        self.on_change = lambda ui: None
    def _get_actors(self):
        """Get the actors composing this UI component."""
        return self.panel.actors
[docs]
    def resize(self, size):
        """Resize ComboBox2D.
        Parameters
        ----------
        size : (int, int)
            ComboBox size(width, height) in pixels.
        """
        self.panel.resize(size)
        self.text_block_size = (int(0.9 * size[0]), int(0.1 * size[1]))
        self.drop_menu_size = (int(0.9 * size[0]), int(0.7 * size[1]))
        self.drop_button_size = (int(0.1 * size[0]), int(0.1 * size[1]))
        self.panel.update_element(self.selection_box, (0.001, 0.7))
        self.panel.update_element(self.drop_down_button, (0.8, 0.7))
        self.panel.update_element(self.drop_down_menu, (0, 0))
        self.drop_down_button.resize(self.drop_button_size)
        self.drop_down_menu.resize(self.drop_menu_size)
        self.selection_box.resize(self.text_block_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.position = coords
        self.panel.position = (
            self.panel.position[0],
            self.panel.position[1] - self.drop_menu_size[1],
        )
    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)
        self.selection_box.font_size = self.font_size
    def _get_size(self):
        return self.panel.size
    @property
    def selected_text(self):
        return self._selection
    @property
    def selected_text_index(self):
        return self._selection_ID
[docs]
    def set_visibility(self, visibility):
        super().set_visibility(visibility)
        if not self._menu_visibility:
            self.drop_down_menu.set_visibility(False) 
[docs]
    def append_item(self, *items):
        """Append additional options to the menu.
        Parameters
        ----------
        items : n-d list, n-d tuple, Number or str
            Additional options.
        """
        for item in items:
            if isinstance(item, (list, tuple)):
                # Useful when n-d lists/tuples are used.
                self.append_item(*item)
            elif isinstance(item, (str, Number)):
                self.items.append(str(item))
            else:
                raise TypeError("Invalid item instance {}".format(type(item)))
        self.drop_down_menu.update_scrollbar()
        if not self._menu_visibility:
            self.drop_down_menu.scroll_bar.set_visibility(False) 
[docs]
    def select_option_callback(self, i_ren, _obj, listboxitem):
        """Select the appropriate option
        Parameters
        ----------
        i_ren: :class:`CustomInteractorStyle`
        obj: :class:`vtkActor`
            The picked actor
        listboxitem: :class:`ListBoxItem2D`
        """
        # Set the Text of TextBlock2D to the text of listboxitem
        self._selection = listboxitem.element
        self._selection_ID = self.items.index(self._selection)
        self.selection_box.message = self._selection
        clip_overflow(self.selection_box, self.selection_box.background.size[0])
        self.drop_down_menu.set_visibility(False)
        self._menu_visibility = False
        self.drop_down_button.next_icon()
        self.on_change(self)
        i_ren.force_render()
        i_ren.event.abort() 
 
[docs]
class ListBox2D(UI):
    """UI component that allows the user to select items from a list.
    Attributes
    ----------
    on_change: function
        Callback function for when the selected items have changed.
    """
    @warn_on_args_to_kwargs()
    def __init__(
        self,
        values,
        *,
        position=(0, 0),
        size=(100, 300),
        multiselection=True,
        reverse_scrolling=False,
        font_size=20,
        line_spacing=1.4,
        text_color=(0.2, 0.2, 0.2),
        selected_color=(0.9, 0.6, 0.6),
        unselected_color=(0.6, 0.6, 0.6),
        scroll_bar_active_color=(0.6, 0.2, 0.2),
        scroll_bar_inactive_color=(0.9, 0.0, 0.0),
        background_opacity=1.0,
    ):
        """Init class instance.
        Parameters
        ----------
        values: list of objects
            Values used to populate this listbox. Objects must be castable
            to string.
        position : (float, float)
            Absolute coordinates (x, y) of the lower-left corner of this
            UI component.
        size : (int, int)
            Width and height in pixels of this UI component.
        multiselection: {True, False}
            Whether multiple values can be selected at once.
        reverse_scrolling: {True, False}
            If True, scrolling up will move the list of files down.
        font_size: int
            The font size in pixels.
        line_spacing: float
            Distance between listbox's items in pixels.
        text_color : tuple of 3 floats
        selected_color : tuple of 3 floats
        unselected_color : tuple of 3 floats
        scroll_bar_active_color : tuple of 3 floats
        scroll_bar_inactive_color : tuple of 3 floats
        background_opacity : float
        """
        self.view_offset = 0
        self.slots = []
        self.selected = []
        self.panel_size = size
        self.font_size = font_size
        self.line_spacing = line_spacing
        self.slot_height = int(self.font_size * self.line_spacing)
        self.text_color = text_color
        self.selected_color = selected_color
        self.unselected_color = unselected_color
        self.background_opacity = background_opacity
        # self.panel.resize(size)
        self.values = values
        self.multiselection = multiselection
        self.last_selection_idx = 0
        self.reverse_scrolling = reverse_scrolling
        super(ListBox2D, self).__init__()
        denom = len(self.values) - self.nb_slots
        if not denom:
            denom += 1
        self.scroll_step_size = (
            self.slot_height * self.nb_slots - self.scroll_bar.height
        ) / denom
        self.scroll_bar_active_color = scroll_bar_active_color
        self.scroll_bar_inactive_color = scroll_bar_inactive_color
        self.scroll_bar.color = self.scroll_bar_inactive_color
        self.scroll_bar.opacity = self.background_opacity
        self.position = position
        self.scroll_init_position = 0
        self.update()
        # Offer some standard hooks to the user.
        self.on_change = lambda: None
    def _setup(self):
        """Setup this UI component.
        Create the ListBox (Panel2D) filled with empty slots (ListBoxItem2D).
        """
        self.margin = 10
        size = self.panel_size
        font_size = self.font_size
        # Calculating the number of slots.
        self.nb_slots = int((size[1] - 2 * self.margin) // self.slot_height)
        # This panel facilitates adding slots at the right position.
        self.panel = Panel2D(size=size, color=(1, 1, 1))
        # Add a scroll bar
        scroll_bar_height = (
            self.nb_slots * (size[1] - 2 * self.margin) / len(self.values)
        )
        self.scroll_bar = Rectangle2D(size=(int(size[0] / 20), scroll_bar_height))
        if len(self.values) <= self.nb_slots:
            self.scroll_bar.set_visibility(False)
            self.scroll_bar.height = 0
        self.panel.add_element(
            self.scroll_bar, size - self.scroll_bar.size - self.margin
        )
        # Initialisation of empty text actors
        self.slot_width = (
            size[0] - self.scroll_bar.size[0] - 2 * self.margin - self.margin
        )
        x = self.margin
        y = size[1] - self.margin
        for _ in range(self.nb_slots):
            y -= self.slot_height
            item = ListBoxItem2D(
                list_box=self,
                size=(self.slot_width, self.slot_height),
                text_color=self.text_color,
                selected_color=self.selected_color,
                unselected_color=self.unselected_color,
                background_opacity=self.background_opacity,
            )
            item.textblock.font_size = font_size
            self.slots.append(item)
            self.panel.add_element(item, (x, y + self.margin))
        # Add default events listener for this UI component.
        self.scroll_bar.on_left_mouse_button_pressed = self.scroll_click_callback
        self.scroll_bar.on_left_mouse_button_released = self.scroll_release_callback
        self.scroll_bar.on_left_mouse_button_dragged = self.scroll_drag_callback
        # Handle mouse wheel events on the panel.
        up_event = "MouseWheelForwardEvent"
        down_event = "MouseWheelBackwardEvent"
        if self.reverse_scrolling:
            up_event, down_event = down_event, up_event  # Swap events
        self.add_callback(
            self.panel.background.actor, up_event, self.up_button_callback
        )
        self.add_callback(
            self.panel.background.actor, down_event, self.down_button_callback
        )
        # Handle mouse wheel events on the slots.
        for slot in self.slots:
            self.add_callback(slot.background.actor, up_event, self.up_button_callback)
            self.add_callback(
                slot.background.actor, down_event, self.down_button_callback
            )
            self.add_callback(slot.textblock.actor, up_event, self.up_button_callback)
            self.add_callback(
                slot.textblock.actor, down_event, self.down_button_callback
            )
[docs]
    def resize(self, size):
        pass 
    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)
        for slot in self.slots:
            clip_overflow(slot.textblock, self.slot_width)
    def _get_size(self):
        return self.panel.size
    def _set_position(self, coords):
        """Position the lower-left corner of this UI component.
        Parameters
        ----------
        coords: (float, float)
            Absolute pixel coordinates (x, y).
        """
        self.panel.position = coords
[docs]
    def update(self):
        """Refresh listbox's content."""
        view_start = self.view_offset
        view_end = view_start + self.nb_slots
        values_to_show = self.values[view_start:view_end]
        # Populate slots according to the view.
        for i, choice in enumerate(values_to_show):
            slot = self.slots[i]
            slot.element = choice
            if slot.textblock.scene is not None:
                clip_overflow(slot.textblock, self.slot_width)
            slot.set_visibility(True)
            if slot.size[1] != self.slot_height:
                slot.resize((self.slot_width, self.slot_height))
            if slot.element in self.selected:
                slot.select()
            else:
                slot.deselect()
        # Flush remaining slots.
        for slot in self.slots[len(values_to_show) :]:
            slot.element = None
            slot.set_visibility(False)
            slot.resize((self.slot_width, 0))
            slot.deselect() 
[docs]
    def clear_selection(self):
        del self.selected[:] 
[docs]
    @warn_on_args_to_kwargs()
    def select(self, item, *, multiselect=False, range_select=False):
        """Select the item.
        Parameters
        ----------
        item: ListBoxItem2D's object
            Item to select.
        multiselect: {True, False}
            If True and multiselection is allowed, the item is added to the
            selection.
            Otherwise, the selection will only contain the provided item unless
            range_select is True.
        range_select: {True, False}
            If True and multiselection is allowed, all items between the last
            selected item and the current one will be added to the selection.
            Otherwise, the selection will only contain the provided item unless
            multi_select is True.
        """
        selection_idx = self.values.index(item.element)
        if self.multiselection and range_select:
            self.clear_selection()
            step = 1 if selection_idx >= self.last_selection_idx else -1
            for i in range(self.last_selection_idx, selection_idx + step, step):
                self.selected.append(self.values[i])
        elif self.multiselection and multiselect:
            if item.element in self.selected:
                self.selected.remove(item.element)
            else:
                self.selected.append(item.element)
            self.last_selection_idx = selection_idx
        else:
            self.clear_selection()
            self.selected.append(item.element)
            self.last_selection_idx = selection_idx
        self.on_change()  # Call hook.
        self.update() 
 
[docs]
class ListBoxItem2D(UI):
    """The text displayed in a listbox."""
    @warn_on_args_to_kwargs()
    def __init__(
        self,
        list_box,
        size,
        *,
        text_color=(1.0, 0.0, 0.0),
        selected_color=(0.4, 0.4, 0.4),
        unselected_color=(0.9, 0.9, 0.9),
        background_opacity=1.0,
    ):
        """Init ListBox Item instance.
        Parameters
        ----------
        list_box : :class:`ListBox`
            The ListBox reference this text belongs to.
        size : tuple of 2 ints
            The size of the listbox item.
        text_color : tuple of 3 floats
        unselected_color : tuple of 3 floats
        selected_color : tuple of 3 floats
        background_opacity : float
        """
        super(ListBoxItem2D, self).__init__()
        self._element = None
        self.list_box = list_box
        self.background.resize(size)
        self.background_opacity = background_opacity
        self.selected = False
        self.text_color = text_color
        self.textblock.color = self.text_color
        self.selected_color = selected_color
        self.unselected_color = unselected_color
        self.background.opacity = self.background_opacity
        self.deselect()
    def _setup(self):
        """Setup this UI component.
        Create the ListBoxItem2D with its background (Rectangle2D) and its
        label (TextBlock2D).
        """
        self.background = Rectangle2D()
        self.textblock = TextBlock2D(
            justification="left", vertical_justification="middle"
        )
        # Add default events listener for this UI component.
        self.add_callback(
            self.textblock.actor, "LeftButtonPressEvent", self.left_button_clicked
        )
        self.add_callback(
            self.background.actor, "LeftButtonPressEvent", self.left_button_clicked
        )
    def _get_actors(self):
        """Get the actors composing this UI component."""
        return self.background.actors + self.textblock.actors
    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.
        Parameters
        ----------
        scene : scene
        """
        self.background.add_to_scene(scene)
        self.textblock.add_to_scene(scene)
    def _get_size(self):
        return self.background.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.textblock.position = coords
        # Center background underneath the text.
        position = coords
        self.background.position = (
            position[0],
            position[1] - self.background.size[1] / 2.0,
        )
[docs]
    def deselect(self):
        self.background.color = self.unselected_color
        self.textblock.bold = False
        self.selected = False 
[docs]
    def select(self):
        self.textblock.bold = True
        self.background.color = self.selected_color
        self.selected = True 
    @property
    def element(self):
        return self._element
    @element.setter
    def element(self, element):
        self._element = element
        self.textblock.message = "" if self._element is None else str(element)
[docs]
    def resize(self, size):
        self.background.resize(size) 
 
[docs]
class DrawShape(UI):
    """Create and Manage 2D Shapes."""
    @warn_on_args_to_kwargs()
    def __init__(self, shape_type, *, drawpanel=None, position=(0, 0)):
        """Init this UI element.
        Parameters
        ----------
        shape_type : string
            Type of shape to be created.
        drawpanel : DrawPanel, optional
            Reference to the main canvas on which it is drawn.
        position : (float, float), optional
            (x, y) in pixels.
        """
        self.shape = None
        self.shape_type = shape_type.lower()
        self.drawpanel = drawpanel
        self.max_size = None
        self.rotation = 0
        super(DrawShape, self).__init__(position=position)
        self.shape.color = np.random.random(3)
    def _setup(self):
        """Setup this UI component.
        Create a Shape.
        """
        if self.shape_type == "line":
            self.shape = Rectangle2D(size=(3, 3))
        elif self.shape_type == "quad":
            self.shape = Rectangle2D(size=(3, 3))
        elif self.shape_type == "circle":
            self.shape = Disk2D(outer_radius=2)
        else:
            raise IOError("Unknown shape type: {}.".format(self.shape_type))
        self.shape.on_left_mouse_button_pressed = self.left_button_pressed
        self.shape.on_left_mouse_button_dragged = self.left_button_dragged
        self.shape.on_left_mouse_button_released = self.left_button_released
    def _get_actors(self):
        """Get the actors composing this UI component."""
        return self.shape
    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.
        Parameters
        ----------
        scene : scene
        """
        self._scene = scene
        self.shape.add_to_scene(scene)
    def _get_size(self):
        return self.shape.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).
        """
        if self.shape_type == "circle":
            self.shape.center = coords
        else:
            self.shape.position = coords
[docs]
    def update_shape_position(self, center_position):
        """Update the center position on the canvas.
        Parameters
        ----------
        center_position: (float, float)
            Absolute pixel coordinates (x, y).
        """
        new_center = self.clamp_position(center=center_position)
        self.drawpanel.canvas.update_element(self, new_center, anchor="center")
        self.cal_bounding_box() 
    @property
    def center(self):
        return self._bounding_box_min + self._bounding_box_size // 2
    @center.setter
    def center(self, coords):
        """Position the center of this UI component.
        Parameters
        ----------
        coords: (float, float)
            Absolute pixel coordinates (x, y).
        """
        new_center = np.array(coords)
        new_lower_left_corner = new_center - self._bounding_box_size // 2
        self.position = new_lower_left_corner + self._bounding_box_offset
        self.cal_bounding_box()
    @property
    def is_selected(self):
        return self._is_selected
    @is_selected.setter
    def is_selected(self, value):
        if self.drawpanel and value:
            self.drawpanel.current_shape = self
        self._is_selected = value
        self.selection_change()
[docs]
    def selection_change(self):
        if self.is_selected:
            self.drawpanel.rotation_slider.value = self.rotation
        else:
            self.drawpanel.rotation_slider.set_visibility(False) 
[docs]
    def rotate(self, angle):
        """Rotate the vertices of the UI component using specific angle.
        Parameters
        ----------
        angle: float
            Value by which the vertices are rotated in radian.
        """
        if self.shape_type == "circle":
            return
        points_arr = vertices_from_actor(self.shape.actor)
        new_points_arr = rotate_2d(points_arr, angle)
        set_polydata_vertices(self.shape._polygonPolyData, new_points_arr)
        update_actor(self.shape.actor)
        self.cal_bounding_box() 
[docs]
    def cal_bounding_box(self):
        """Calculate the min, max position and the size of the bounding box."""
        vertices = self.position + vertices_from_actor(self.shape.actor)[:, :-1]
        (
            self._bounding_box_min,
            self._bounding_box_max,
            self._bounding_box_size,
        ) = cal_bounding_box_2d(vertices)
        self._bounding_box_offset = self.position - self._bounding_box_min 
[docs]
    @warn_on_args_to_kwargs()
    def clamp_position(self, *, center=None):
        """Clamp the given center according to the DrawPanel canvas.
        Parameters
        ----------
        center : (float, float)
            (x, y) in pixels.
        Returns
        -------
        new_center: ndarray(int)
            New center for the shape.
        """
        center = self.center if center is None else center
        new_center = np.clip(
            center,
            self._bounding_box_size // 2,
            self.drawpanel.canvas.size - self._bounding_box_size // 2,
        )
        return new_center.astype(int) 
[docs]
    def resize(self, size):
        """Resize the UI."""
        if self.shape_type == "line":
            hyp = np.hypot(size[0], size[1])
            self.shape.resize((hyp, 3))
            self.rotate(angle=np.arctan2(size[1], size[0]))
        elif self.shape_type == "quad":
            self.shape.resize(size)
        elif self.shape_type == "circle":
            hyp = np.hypot(size[0], size[1])
            if self.max_size and hyp > self.max_size:
                hyp = self.max_size
            self.shape.outer_radius = hyp
        self.cal_bounding_box() 
[docs]
    def remove(self):
        """Remove the Shape and all related actors."""
        self._scene.rm(self.shape.actor)
        self.drawpanel.rotation_slider.set_visibility(False) 
 
[docs]
class DrawPanel(UI):
    """The main Canvas(Panel2D) on which everything would be drawn."""
    @warn_on_args_to_kwargs()
    def __init__(self, *, size=(400, 400), position=(0, 0), is_draggable=False):
        """Init this UI element.
        Parameters
        ----------
        size : (int, int), optional
            Width and height in pixels of this UI component.
        position : (float, float), optional
            (x, y) in pixels.
        is_draggable : bool, optional
            Whether the background canvas will be draggble or not.
        """
        self.panel_size = size
        super(DrawPanel, self).__init__(position=position)
        self.is_draggable = is_draggable
        self.current_mode = None
        if is_draggable:
            self.current_mode = "selection"
        self.shape_list = []
        self.current_shape = None
    def _setup(self):
        """Setup this UI component.
        Create a Canvas(Panel2D).
        """
        self.canvas = Panel2D(size=self.panel_size)
        self.canvas.background.on_left_mouse_button_pressed = self.left_button_pressed
        self.canvas.background.on_left_mouse_button_dragged = self.left_button_dragged
        # Todo
        # Convert mode_data into a private variable and make it read-only
        # Then add the ability to insert user-defined mode
        mode_data = {
            "selection": ["selection.png", "selection-pressed.png"],
            "line": ["line.png", "line-pressed.png"],
            "quad": ["quad.png", "quad-pressed.png"],
            "circle": ["circle.png", "circle-pressed.png"],
            "delete": ["delete.png", "delete-pressed.png"],
        }
        padding = 5
        # Todo
        # Add this size to __init__
        mode_panel_size = (len(mode_data) * 35 + 2 * padding, 40)
        self.mode_panel = Panel2D(size=mode_panel_size, color=(0.5, 0.5, 0.5))
        btn_pos = np.array([0, 0])
        for mode, fname in mode_data.items():
            icon_files = []
            icon_files.append((mode, read_viz_icons(style="new_icons", fname=fname[0])))
            icon_files.append(
                (mode + "-pressed", read_viz_icons(style="new_icons", fname=fname[1]))
            )
            btn = Button2D(icon_fnames=icon_files)
            def mode_selector(i_ren, _obj, btn):
                self.current_mode = btn.icon_names[0]
                i_ren.force_render()
            btn.on_left_mouse_button_pressed = mode_selector
            self.mode_panel.add_element(btn, btn_pos + padding)
            btn_pos[0] += btn.size[0] + padding
        self.canvas.add_element(self.mode_panel, (0, -mode_panel_size[1]))
        self.mode_text = TextBlock2D(
            text="Select appropriate drawing mode using below icon"
        )
        self.canvas.add_element(self.mode_text, (0.0, 1.0))
        self.rotation_slider = RingSlider2D(
            initial_value=0, text_template="{angle:5.1f}°"
        )
        self.rotation_slider.set_visibility(False)
        def rotate_shape(slider):
            angle = slider.value
            previous_angle = slider.previous_value
            rotation_angle = angle - previous_angle
            current_center = self.current_shape.center
            self.current_shape.rotate(np.deg2rad(rotation_angle))
            self.current_shape.rotation = slider.value
            self.current_shape.update_shape_position(
                current_center - self.canvas.position
            )
        self.rotation_slider.on_moving_slider = rotate_shape
    def _get_actors(self):
        """Get the actors composing this UI component."""
        return self.canvas.actors
    def _add_to_scene(self, scene):
        """Add all subcomponents or VTK props that compose this UI component.
        Parameters
        ----------
        scene : scene
        """
        self._scene = scene
        self.canvas.add_to_scene(scene)
    def _get_size(self):
        return self.canvas.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.canvas.position = coords + [0, self.mode_panel.size[1]]
        slider_position = self.canvas.position + [
            self.canvas.size[0] - self.rotation_slider.size[0] / 2,
            self.rotation_slider.size[1] / 2,
        ]
        self.rotation_slider.center = slider_position
[docs]
    def resize(self, size):
        """Resize the UI."""
        pass 
    @property
    def current_mode(self):
        return self._current_mode
    @current_mode.setter
    def current_mode(self, mode):
        self.update_button_icons(mode)
        self._current_mode = mode
        if mode is not None:
            self.mode_text.message = f"Mode: {mode}"
[docs]
    def cal_min_boundary_distance(self, position):
        """Calculate minimum distance between the current position and canvas boundary.
        Parameters
        ----------
        position: (float,float)
            current position of the shape.
        Returns
        -------
        float
            Minimum distance from the boundary.
        """
        distance_list = []
        # calculate distance from element to left and lower boundary
        distance_list.extend(position - self.canvas.position)
        # calculate distance from element to upper and right boundary
        distance_list.extend(self.canvas.position + self.canvas.size - position)
        return min(distance_list) 
[docs]
    def draw_shape(self, shape_type, current_position):
        """Draw the required shape at the given position.
        Parameters
        ----------
        shape_type: string
            Type of shape - line, quad, circle.
        current_position: (float,float)
            Lower left corner position for the shape.
        """
        shape = DrawShape(
            shape_type=shape_type, drawpanel=self, position=current_position
        )
        if shape_type == "circle":
            shape.max_size = self.cal_min_boundary_distance(current_position)
        self.shape_list.append(shape)
        self._scene.add(shape)
        self.canvas.add_element(shape, current_position - self.canvas.position)
        self.update_shape_selection(shape) 
[docs]
    def resize_shape(self, current_position):
        """Resize the shape.
        Parameters
        ----------
        current_position: (float,float)
            Lower left corner position for the shape.
        """
        self.current_shape = self.shape_list[-1]
        size = current_position - self.current_shape.position
        self.current_shape.resize(size) 
[docs]
    def update_shape_selection(self, selected_shape):
        for shape in self.shape_list:
            if selected_shape == shape:
                shape.is_selected = True
            else:
                shape.is_selected = False 
[docs]
    def show_rotation_slider(self):
        """Display the  RingSlider2D to allow rotation of shape from the center."""
        self._scene.rm(*self.rotation_slider.actors)
        self.rotation_slider.add_to_scene(self._scene)
        self.rotation_slider.set_visibility(True) 
[docs]
    def clamp_mouse_position(self, mouse_position):
        """Restrict the mouse position to the canvas boundary.
        Parameters
        ----------
        mouse_position: (float,float)
            Current mouse position.
        Returns
        -------
        list(float)
            New clipped position.
        """
        return np.clip(
            mouse_position,
            self.canvas.position,
            self.canvas.position + self.canvas.size,
        ) 
[docs]
    def handle_mouse_click(self, position):
        if self.current_mode == "selection":
            if self.is_draggable:
                self._drag_offset = position - self.position
            self.current_shape.is_selected = False
        if self.current_mode in ["line", "quad", "circle"]:
            self.draw_shape(self.current_mode, position) 
[docs]
    def handle_mouse_drag(self, position):
        if self.is_draggable and self.current_mode == "selection":
            if self._drag_offset is not None:
                new_position = position - self._drag_offset
                self.position = new_position
        if self.current_mode in ["line", "quad", "circle"]:
            self.resize_shape(position) 
 
[docs]
class PlaybackPanel(UI):
    """A playback controller that can do essential functionalities.
    such as play, pause, stop, and seek.
    """
    @warn_on_args_to_kwargs()
    def __init__(self, *, loop=False, position=(0, 0), width=None):
        self._width = width if width is not None else 900
        self._auto_width = width is None
        self._position = position
        super(PlaybackPanel, self).__init__(position=position)
        self._playing = False
        self._loop = None
        self.loop() if loop else self.play_once()
        self._speed = 1
        # callback functions
        self.on_play_pause_toggle = lambda state: None
        self.on_play = lambda: None
        self.on_pause = lambda: None
        self.on_stop = lambda: None
        self.on_loop_toggle = lambda is_looping: None
        self.on_progress_bar_changed = lambda x: None
        self.on_speed_up = lambda x: None
        self.on_slow_down = lambda x: None
        self.on_speed_changed = lambda x: None
        self._set_position(position)
    def _setup(self):
        """Setup this Panel component."""
        self.time_text = TextBlock2D()
        self.speed_text = TextBlock2D(
            text="1",
            font_size=21,
            color=(0.2, 0.2, 0.2),
            bold=True,
            justification="center",
            vertical_justification="middle",
        )
        self.panel = Panel2D(
            size=(190, 30),
            color=(1, 1, 1),
            align="right",
            has_border=True,
            border_color=(0, 0.3, 0),
            border_width=2,
        )
        play_pause_icons = [
            ("play", read_viz_icons(fname="play3.png")),
            ("pause", read_viz_icons(fname="pause2.png")),
        ]
        loop_icons = [
            ("once", read_viz_icons(fname="checkmark.png")),
            ("loop", read_viz_icons(fname="infinite.png")),
        ]
        self._play_pause_btn = Button2D(icon_fnames=play_pause_icons)
        self._loop_btn = Button2D(icon_fnames=loop_icons)
        self._stop_btn = Button2D(
            icon_fnames=[("stop", read_viz_icons(fname="stop2.png"))]
        )
        self._speed_up_btn = Button2D(
            icon_fnames=[("plus", read_viz_icons(fname="plus.png"))], size=(15, 15)
        )
        self._slow_down_btn = Button2D(
            icon_fnames=[("minus", read_viz_icons(fname="minus.png"))], size=(15, 15)
        )
        self._progress_bar = LineSlider2D(
            initial_value=0,
            orientation="horizontal",
            min_value=0,
            max_value=100,
            text_alignment="top",
            length=590,
            text_template="",
            line_width=9,
        )
        start = 0.04
        w = 0.2
        self.panel.add_element(self._play_pause_btn, (start, 0.04))
        self.panel.add_element(self._stop_btn, (start + w, 0.04))
        self.panel.add_element(self._loop_btn, (start + 2 * w, 0.04))
        self.panel.add_element(self._slow_down_btn, (start + 0.63, 0.3))
        self.panel.add_element(self.speed_text, (start + 0.78, 0.45))
        self.panel.add_element(self._speed_up_btn, (start + 0.86, 0.3))
        def play_pause_toggle(i_ren, _obj, _button):
            self._playing = not self._playing
            if self._playing:
                self.play()
            else:
                self.pause()
            self.on_play_pause_toggle(self._playing)
            i_ren.force_render()
        def stop(i_ren, _obj, _button):
            self.stop()
            i_ren.force_render()
        def speed_up(i_ren, _obj, _button):
            inc = 10 ** np.floor(np.log10(self.speed))
            self.speed = round(self.speed + inc, 13)
            self.on_speed_up(self._speed)
            self.on_speed_changed(self._speed)
            i_ren.force_render()
        def slow_down(i_ren, _obj, _button):
            dec = 10 ** np.floor(np.log10(self.speed - self.speed / 10))
            self.speed = round(self.speed - dec, 13)
            self.on_slow_down(self._speed)
            self.on_speed_changed(self._speed)
            i_ren.force_render()
        def loop_toggle(i_ren, _obj, _button):
            self._loop = not self._loop
            if self._loop:
                self.loop()
            else:
                self.play_once()
            self.on_loop_toggle(self._loop)
            i_ren.force_render()
        # using the adapters created above
        self._play_pause_btn.on_left_mouse_button_pressed = play_pause_toggle
        self._stop_btn.on_left_mouse_button_pressed = stop
        self._loop_btn.on_left_mouse_button_pressed = loop_toggle
        self._speed_up_btn.on_left_mouse_button_pressed = speed_up
        self._slow_down_btn.on_left_mouse_button_pressed = slow_down
        def on_progress_change(slider):
            t = slider.value
            self.on_progress_bar_changed(t)
            self.current_time = t
        self._progress_bar.on_moving_slider = on_progress_change
        self.current_time = 0
[docs]
    def play(self):
        """Play the playback"""
        self._playing = True
        self._play_pause_btn.set_icon_by_name("pause")
        self.on_play() 
[docs]
    def stop(self):
        """Stop the playback"""
        self._playing = False
        self._play_pause_btn.set_icon_by_name("play")
        self.on_stop() 
[docs]
    def pause(self):
        """Pause the playback"""
        self._playing = False
        self._play_pause_btn.set_icon_by_name("play")
        self.on_pause() 
[docs]
    def loop(self):
        """Set repeating mode to loop."""
        self._loop = True
        self._loop_btn.set_icon_by_name("loop") 
[docs]
    def play_once(self):
        """Set repeating mode to repeat once."""
        self._loop = False
        self._loop_btn.set_icon_by_name("once") 
    @property
    def final_time(self):
        """Set final progress slider time value.
        Returns
        -------
        float
            Final time for the progress slider.
        """
        return self._progress_bar.max_value
    @final_time.setter
    def final_time(self, t):
        """Set final progress slider time value.
        Parameters
        ----------
        t: float
            Final time for the progress slider.
        """
        self._progress_bar.max_value = t
    @property
    def current_time(self):
        """Get current time of the progress slider.
        Returns
        -------
        float
            Progress slider current value.
        """
        return self._progress_bar.value
    @current_time.setter
    def current_time(self, t):
        """Set progress slider value.
        Parameters
        ----------
        t: float
            Current time to be set.
        """
        self._progress_bar.value = t
        self.current_time_str = t
    @property
    def current_time_str(self):
        """Returns current time as a string.
        Returns
        -------
        str
            Current time formatted as a string in the form:`HH:MM:SS`.
        """
        return self.time_text.message
    @current_time_str.setter
    def current_time_str(self, t):
        """Set time counter.
        Parameters
        ----------
        t: float
            Time to be set in the time_text counter.
        Notes
        -----
        This should only be used when the `current_value` is not being set
        since setting`current_value` automatically sets this property as well.
        """
        t = np.clip(t, 0, self.final_time)
        if self.final_time < 3600:
            m, s = divmod(t, 60)
            t_str = r"%02d:%05.2f" % (m, s)
        else:
            m, s = divmod(t, 60)
            h, m = divmod(m, 60)
            t_str = r"%02d:%02d:%02d" % (h, m, s)
        self.time_text.message = t_str
    @property
    def speed(self):
        """Returns current speed.
        Returns
        -------
        str
            Current time formatted as a string in the form:`HH:MM:SS`.
        """
        return self._speed
    @speed.setter
    def speed(self, speed):
        """Set time counter.
        Parameters
        ----------
        speed: float
            Speed value to be set in the speed_text counter.
        """
        if speed <= 0:
            speed = 0.01
        self._speed = speed
        speed_str = f"{speed}".strip("0").rstrip(".")
        self.speed_text.font_size = 21 if 0.01 <= speed < 100 else 14
        self.speed_text.message = speed_str
[docs]
    def show(self):
        [act.SetVisibility(1) for act in self._get_actors()] 
[docs]
    def hide(self):
        [act.SetVisibility(0) for act in self._get_actors()] 
    def _get_actors(self):
        """Get the actors composing this UI component."""
        return self.panel.actors + self._progress_bar.actors + self.time_text.actors
    def _add_to_scene(self, _scene):
        """Add all subcomponents or VTK props that compose this UI component.
        Parameters
        ----------
        _scene : scene
        """
        def resize_cbk(caller, ev):
            if self._auto_width:
                width = _scene.GetSize()[0]
                if width == self.width:
                    return
                self._width = width
                self._set_position(self.position)
                self._progress_bar.value = self._progress_bar.value
        _scene.AddObserver(Command.StartEvent, resize_cbk)
        self.panel.add_to_scene(_scene)
        self._progress_bar.add_to_scene(_scene)
        self.time_text.add_to_scene(_scene)
    @property
    def width(self):
        """Return the width of the PlaybackPanel
        Returns
        -------
        float
            The width of the PlaybackPanel.
        """
        return self._width
    @width.setter
    def width(self, width):
        """Set width of the PlaybackPanel.
        Parameters
        ----------
        width: float
            The width of the whole panel.
            If set to None, The width will be the same as the window's width.
        """
        self._width = width if width is not None else 900
        self._auto_width = width is None
        self._set_position(self.position)
    def _set_position(self, _coords):
        x, y = self.position
        width = self.width
        self.panel.position = (x + 5, y + 5)
        progress_length = max(width - 310 - x, 1.0)
        self._progress_bar.track.width = progress_length
        self._progress_bar.center = (x + 215 + progress_length / 2, y + 20)
        self.time_text.position = (x + 225 + progress_length, y + 10)
    def _get_size(self):
        return self.panel.size + self._progress_bar.size + self.time_text.size 
[docs]
class Card2D(UI):
    """Card element to show image and related text
    Attributes
    ----------
    image: :class: 'ImageContainer2D'
        Renders the image on the card.
    title_box: :class: 'TextBlock2D'
        Displays the title on card.
    body_box: :class: 'TextBLock2D'
        Displays the body text.
    """
    @warn_on_args_to_kwargs()
    def __init__(
        self,
        image_path,
        *,
        body_text="",
        draggable=True,
        title_text="",
        padding=10,
        position=(0, 0),
        size=(400, 400),
        image_scale=0.5,
        bg_color=(0.5, 0.5, 0.5),
        bg_opacity=1,
        title_color=(0.0, 0.0, 0.0),
        body_color=(0.0, 0.0, 0.0),
        border_color=(1.0, 1.0, 1.0),
        border_width=0,
        maintain_aspect=False,
    ):
        """Parameters
        ----------
        image_path: str
            Path of the image, supports png and jpg/jpeg images
        body_text: str, optional
            Card body text
        draggable: Bool, optional
            If the card should be draggable
        title_text: str, optional
            Card title text
        padding: int, optional
            Padding between image, title, body
        position : (float, float), optional
            Absolute coordinates (x, y) of the lower-left corner of the
            UI component
        size : (int, int), optional
            Width and height of the pixels of this UI component.
        image_scale: float, optional
            fraction of size taken by the image (between 0 , 1)
        bg_color: (float, float, float), optional
            Background color of card
        bg_opacity: float, optional
            Background opacity
        title_color: (float, float, float), optional
            Title text color
        body_color: (float, float, float), optional
            Body text color
        border_color: (float, float, float), optional
            Border color
        border_width: int, optional
            Width of the border
        maintain_aspect: bool, optional
            If the image should be scaled to maintain aspect ratio
        """
        self.image_path = image_path
        self._basename = os.path.basename(self.image_path)
        self._extension = self._basename.split(".")[-1]
        if self._extension not in ["jpg", "jpeg", "png"]:
            raise UnidentifiedImageError(
                f"Image extension {self._extension} not supported"
            )
        self.body_text = body_text
        self.title_text = title_text
        self.draggable = draggable
        self.card_size = size
        self.padding = padding
        self.title_color = [np.clip(value, 0, 1) for value in title_color]
        self.body_color = [np.clip(value, 0, 1) for value in body_color]
        self.bg_color = [np.clip(value, 0, 1) for value in bg_color]
        self.border_color = [np.clip(value, 0, 1) for value in border_color]
        self.bg_opacity = bg_opacity
        self.text_scale = np.clip(1 - image_scale, 0, 1)
        self.image_scale = np.clip(image_scale, 0, 1)
        self.maintain_aspect = maintain_aspect
        if self.maintain_aspect:
            self._true_image_size = Image.open(urlopen(self.image_path)).size
        self._image_size = (self.card_size[0], self.card_size[1] * self.image_scale)
        self.border_width = border_width
        self.has_border = bool(border_width)
        super(Card2D, self).__init__()
        self.position = position
        if self.maintain_aspect:
            self._new_size = (
                self._true_image_size[0],
                self._true_image_size[1] // self.image_scale,
            )
            self.resize(self._new_size)
        else:
            self.resize(size)
    def _setup(self):
        """Setup this UI component
        Create the image.
        Create the title and body.
        Create a Panel2D widget to hold image, title, body.
        """
        self.image = ImageContainer2D(img_path=self.image_path, size=self._image_size)
        self.body_box = TextBlock2D(text=self.body_text, color=self.body_color)
        self.title_box = TextBlock2D(
            text=self.title_text, bold=True, color=self.title_color
        )
        self.panel = Panel2D(
            self.card_size,
            color=self.bg_color,
            opacity=self.bg_opacity,
            border_color=self.border_color,
            border_width=self.border_width,
            has_border=self.has_border,
        )
        self.panel.add_element(self.image, (0.0, 0.0))
        self.panel.add_element(self.title_box, (0.0, 0.0))
        self.panel.add_element(self.body_box, (0.0, 0.0))
        if self.draggable:
            self.panel.background.on_left_mouse_button_dragged = (
                self.left_button_dragged
            )
            self.panel.background.on_left_mouse_button_pressed = (
                self.left_button_pressed
            )
            self.image.on_left_mouse_button_dragged = self.left_button_dragged
            self.image.on_left_mouse_button_pressed = self.left_button_pressed
        else:
            self.panel.background.on_left_mouse_button_dragged = (
                lambda i_ren, _obj, _comp: i_ren.force_render
            )
    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)
        if self.size[0] <= 200:
            clip_overflow(self.body_box, self.size[0] - 2 * self.padding)
        else:
            wrap_overflow(self.body_box, self.size[0] - 2 * self.padding)
        wrap_overflow(self.title_box, self.size[0] - 2 * self.padding)
    def _get_size(self):
        return self.panel.size
[docs]
    def resize(self, size):
        """Resize Card2D.
        Parameters
        ----------
        size : (int, int)
            Card2D size(width, height) in pixels.
        """
        _width, _height = size
        self.panel.resize(size)
        self._image_size = (
            size[0] - int(self.border_width),
            int(self.image_scale * size[1]),
        )
        _title_box_size = (
            _width - 2 * self.padding,
            _height * 0.34 * self.text_scale / 2,
        )
        _body_box_size = (_width - 2 * self.padding, _height * self.text_scale / 2)
        _img_coords = (int(self.border_width), int(size[1] - self._image_size[1]))
        _title_coords = (
            self.padding,
            int(_img_coords[1] - _title_box_size[1] - self.padding + self.border_width),
        )
        _text_coords = (
            self.padding,
            int(
                _title_coords[1] - _body_box_size[1] - self.padding + self.border_width
            ),
        )
        self.panel.update_element(self.image, _img_coords)
        self.panel.update_element(self.body_box, _text_coords)
        self.panel.update_element(self.title_box, _title_coords)
        self.image.resize(self._image_size)
        self.title_box.resize(_title_box_size) 
    def _set_position(self, _coords):
        """Position the lower-left corner of this UI component.
        Parameters
        ----------
        coords: (float, float)
            Absolute pixel coordinates (x, y).
        """
        self.panel.position = _coords
    @property
    def color(self):
        """Returns the background color of card."""
        return self.panel.color
    @color.setter
    def color(self, color):
        """Sets background color of card.
        Parameters
        ----------
        color : list of 3 floats.
        """
        self.panel.color = color
    @property
    def body(self):
        """Returns the body text of the card."""
        return self.body_box.message
    @body.setter
    def body(self, text):
        self.body_box.message = text
    @property
    def title(self):
        """Returns the title text of the card"""
        return self.title_box.message
    @title.setter
    def title(self, text):
        self.title_box.message = text
 
[docs]
class SpinBox(UI):
    """SpinBox UI."""
    @warn_on_args_to_kwargs()
    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=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
[docs]
    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
[docs]
    def increment_callback(self, i_ren, _obj, _button):
        self.increment()
        i_ren.force_render()
        i_ren.event.abort() 
[docs]
    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))
[docs]
    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 
[docs]
    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) 
[docs]
    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) 
[docs]
    def textbox_update_value(self, textbox):
        self.value = self.validate_value(textbox.message)
        self.on_change(self)