"""Module containing the py_cui renderer. It is used to draw all of the onscreen ui_elements and items. """ # Author: Jakub Wlodek # Created: 12-Aug-2019 import curses import py_cui import py_cui.colors class Renderer: """Main renderer class used for drawing ui_elements to the terminal. Has helper functions for drawing the borders, cursor, and text required for the cui. All of the functions supplied by the renderer class should only be used internally. Attributes ---------- root : py_cui.PyCUI The parent window stdscr : standard cursor The cursor with which renderer draws text color_rules : list of py_cui.colors.ColorRule List of currently loaded rules to apply during drawing """ def __init__(self, root, stdscr, logger): """Constructor for renderer object """ self._root = root self._stdscr = stdscr self._color_rules = [] self._logger = logger # Define ui_element border characters self._border_characters = { 'UP_LEFT' : '+', 'UP_RIGHT' : '+', 'DOWN_LEFT' : '+', 'DOWN_RIGHT' : '+', 'HORIZONTAL' : '-', 'VERTICAL' : '|' } def _set_border_renderer_chars(self, border_char_set): """Function that sets the border characters for ui_elements Parameters ---------- border_characters : Dict of str to str The border characters as specified by user """ self._border_characters['UP_LEFT' ] = border_char_set['UP_LEFT' ] self._border_characters['UP_RIGHT' ] = border_char_set['UP_RIGHT' ] self._border_characters['DOWN_LEFT' ] = border_char_set['DOWN_LEFT' ] self._border_characters['DOWN_RIGHT'] = border_char_set['DOWN_RIGHT'] self._border_characters['HORIZONTAL'] = border_char_set['HORIZONTAL'] self._border_characters['VERTICAL' ] = border_char_set['VERTICAL' ] def _set_bold(self): """Sets bold draw mode """ self._stdscr.attron(curses.A_BOLD) def _unset_bold(self): """Unsets bold draw mode """ self._stdscr.attroff(curses.A_BOLD) def set_color_rules(self, color_rules): """Sets current color rules Parameters ---------- color_rules : List[py_cui.colors.ColorRule] List of currently loaded rules to apply during drawing """ self._color_rules = color_rules def set_color_mode(self, color_mode): """Sets the output color mode Parameters ---------- color_mode : int Color code to apply during drawing """ self._stdscr.attron(curses.color_pair(color_mode)) def unset_color_mode(self, color_mode): """Unsets the output color mode Parameters ---------- color_mode : int Color code to unapply during drawing """ self._stdscr.attroff(curses.color_pair(color_mode)) def reset_cursor(self, ui_element, fill=True): """Positions the cursor at the bottom right of the selected element Parameters ---------- ui_element : py_cui.ui.UIElement ui element for which to reset cursor fill : bool a flag that tells the renderer if the element is filling its grid space, or not (ex. Textbox vs textblock) """ padx, pady = ui_element.get_padding() start_x, start_y = ui_element.get_start_position() height, width = ui_element.get_absolute_dimensions() if fill: cursor_y = start_y + height - pady - 1 cursor_x = start_x + width - 2 * padx + 1 else: cursor_y = start_y + int(height / 2) + 2 cursor_x = start_x + width - 2 * padx + 1 try: self._stdscr.move(cursor_y, cursor_x) except: self._stdscr.move(0,0) def draw_cursor(self, cursor_y, cursor_x): """Draws the cursor at a particular location Parameters ---------- cursor_x, cursor_y : int x, y coordinates where to draw the cursor """ self._stdscr.move(cursor_y, cursor_x) def draw_border(self, ui_element, fill=True, with_title=True): """Draws ascii border around ui element Parameters ---------- ui_element : py_cui.ui.UIElement The ui_element being drawn fill : bool a flag that tells the renderer if the ui_element is filling its grid space, or not (ex. Textbox vs textblock) with_title : bool flag that tells whether or not to draw ui_element title """ _, pady = ui_element.get_padding() _, start_y = ui_element.get_start_position() height, _ = ui_element.get_absolute_dimensions() if ui_element.is_selected(): self._set_bold() if fill: border_y_start = start_y + pady border_y_stop = start_y + height - pady - 1 else: border_y_start = start_y + int(height / 2) border_y_stop = border_y_start + 2 self._draw_border_top(ui_element, border_y_start, with_title) for i in range(border_y_start + 1, border_y_stop): self._draw_blank_row(ui_element, i) self._draw_border_bottom(ui_element, border_y_stop) if ui_element.is_selected(): self._unset_bold() def _draw_border_top(self, ui_element, y, with_title): """Internal function for drawing top of border Parameters ---------- ui_element : py_cui.ui.UIElement The ui_element being drawn y : int the terminal row (top down) on which to draw the text with_title : bool Flag that tells renderer if title should be superimposed into border. """ padx, _ = ui_element.get_padding() start_x, _ = ui_element.get_start_position() _, width = ui_element.get_absolute_dimensions() title = ui_element.get_title() if not with_title or (len(title) + 4 >= width - 2 * padx): render_text = '{}{}{}'.format( self._border_characters['UP_LEFT'], self._border_characters['HORIZONTAL'] * (width - 2 - 2 * padx), self._border_characters['UP_RIGHT']) self._stdscr.addstr(y, start_x + padx, render_text) else: render_text = '{}{} {} {}{}'.format(self._border_characters['UP_LEFT'], 2 * self._border_characters['HORIZONTAL'], title, self._border_characters['HORIZONTAL'] * (width - 6 - 2 * padx - len(title)), self._border_characters['UP_RIGHT']) self._stdscr.addstr(y, start_x + padx, render_text) def _draw_border_bottom(self, ui_element, y): """Internal function for drawing bottom of border Parameters ---------- ui_element : py_cui.ui.UIElement The ui_element being drawn y : int the terminal row (top down) on which to draw the text """ padx, _ = ui_element.get_padding() start_x, _ = ui_element.get_start_position() _, width = ui_element.get_absolute_dimensions() render_text = '{}{}{}'.format( self._border_characters['DOWN_LEFT'], self._border_characters['HORIZONTAL'] * (width - 2 - 2 * padx), self._border_characters['DOWN_RIGHT']) self._stdscr.addstr(y, start_x + padx, render_text) def _draw_blank_row(self, ui_element, y): """Internal function for drawing a blank row Parameters ---------- ui_element : py_cui.ui.UIElement The ui_element being drawn y : int the terminal row (top down) on which to draw the text """ padx, _ = ui_element.get_padding() start_x, _ = ui_element.get_start_position() _, width = ui_element.get_absolute_dimensions() render_text = '{}{}{}'.format( self._border_characters['VERTICAL'], ' ' * (width - 2 - 2 * padx), self._border_characters['VERTICAL']) self._stdscr.addstr(y, start_x + padx, render_text) def _get_render_text(self, ui_element, line, centered, bordered, start_pos): """Internal function that computes the scope of the text that should be drawn Parameters ---------- ui_element : py_cui.ui.UIElement The ui_element being drawn line : str the line of text being drawn centered : bool flag to set if the text should be centered bordered : bool a flag to set if the text should be bordered start_pos : int position to start rendering the text from. Returns ------- render_text : str The text shortened to fit within given space """ padx, _ = ui_element.get_padding() _, width = ui_element.get_absolute_dimensions() render_text_length = width - (2 * padx) if bordered: render_text_length = render_text_length - 4 if len(line) - start_pos < render_text_length: if centered: render_text = '{}'.format( line[start_pos:].center(render_text_length, ' ')) else: render_text = '{}{}'.format(line[start_pos:], ' ' * (render_text_length - len(line[start_pos:]))) else: render_text = line[start_pos:start_pos + render_text_length] render_text_fragments = self._generate_text_color_fragments(ui_element, line, render_text) return render_text_fragments def _generate_text_color_fragments(self, ui_element, line, render_text): """Function that applies color rules to text, dividing them if match is found Parameters ---------- ui_element : py_cui.ui.UIElement The ui_element being drawn line : str the line of text being drawn render_text : str The text shortened to fit within given space Returns ------- fragments : list of [int, str] list of text - color code combinations to write """ fragments = [[render_text, ui_element.get_color()]] for color_rule in self._color_rules: fragments, match = color_rule.generate_fragments(ui_element, line, render_text) if match: return fragments return fragments def draw_text(self, ui_element, line, y, centered = False, bordered = True, selected = False, start_pos = 0): """Function that draws ui_element text. Parameters ---------- ui_element : py_cui.ui.UIElement The ui_element being drawn line : str the line of text being drawn y : int the terminal row (top down) on which to draw the text centered : bool flag to set if the text should be centered bordered : bool a flag to set if the text should be bordered selected : bool Flag that tells renderer if ui_element is selected. start_pos : int position to start rendering the text from. """ padx, _ = ui_element.get_padding() _, width = ui_element.get_absolute_dimensions() start_x, _ = ui_element.get_start_position() render_text = self._get_render_text(ui_element, line, centered, bordered, start_pos) current_start_x = start_x + padx if ui_element.is_selected(): self._set_bold() if bordered: self._stdscr.addstr(y, start_x + padx, self._border_characters['VERTICAL']) current_start_x = current_start_x + 2 if ui_element.is_selected(): self._unset_bold() # Each text elem is a list with [text, color] for text_elem in render_text: if text_elem[1] != ui_element.get_color(): self.set_color_mode(text_elem[1]) if selected: self._set_bold() self._stdscr.addstr(y, current_start_x, text_elem[0]) current_start_x = current_start_x + len(text_elem[0]) if selected: self._unset_bold() if text_elem[1] != ui_element.get_color(): self.unset_color_mode(text_elem[1]) if ui_element.is_selected(): self._set_bold() if bordered: self._stdscr.addstr(y, start_x + width - 2 * padx, self._border_characters['VERTICAL']) if ui_element.is_selected(): self._unset_bold()