def draw_text(offset_x, offset_y, width, height): canvas.paint.text_align = canvas.paint.TextAlign.CENTER for row in range(3): for col in range(3): text_string = "" if settings["user.grids_put_one_bottom_left"]: text_string = f"{(2 - row)*3+col+1}" else: text_string = f"{row*3+col+1}" text_rect = canvas.paint.measure_text(text_string)[1] background_rect = text_rect.copy() background_rect.center = Point2d( offset_x + width / 6 + col * width / 3, offset_y + height / 6 + row * height / 3, ) background_rect = background_rect.inset(-4) paint.color = "9999995f" paint.style = Paint.Style.FILL canvas.draw_rect(background_rect) paint.color = "00ff00ff" canvas.draw_text( text_string, offset_x + width / 6 + col * width / 3, offset_y + height / 6 + row * height / 3 + text_rect.height / 2, )
def draw_header_buttons(self, canvas, paint, dimensions): header_height = dimensions["header_height"] dimensions = dimensions["rect"] # Header button tray x = dimensions.x for index, icon in enumerate(self.icons): # Minimize / maximize icon icon_position = Point2d(x + dimensions.width - (self.icon_radius * 1.5 + ( index * self.icon_radius * 2.2 )), dimensions.y + self.icon_radius + self.padding[0] / 2) self.icons[index].pos = icon_position paint.style = paint.Style.FILL if icon.id == "minimize": hover_colour = self.theme.get_colour("button_hover_background", "999999") if self.icon_hovered == index \ else self.theme.get_colour("button_background", "CCCCCC") paint.shader = linear_gradient(self.x, self.y, self.x, self.y + header_height, (hover_colour, hover_colour)) canvas.draw_circle(icon_position.x, icon_position.y, self.icon_radius, paint) text_colour = self.theme.get_colour("icon_colour", "000000") paint.shader = linear_gradient(self.x, self.y, self.x, self.y + header_height, (text_colour, text_colour)) if not self.minimized: canvas.draw_rect(ui.Rect(icon_position.x - self.icon_radius / 2, icon_position.y - 1, self.icon_radius, 2)) else: paint.style = paint.Style.STROKE canvas.draw_rect(ui.Rect( 1 + icon_position.x - self.icon_radius / 2, icon_position.y - self.icon_radius / 2, self.icon_radius - 2, self.icon_radius - 2)) elif icon.id == "close": close_colour = self.theme.get_colour("close_icon_hover_colour") if self.icon_hovered == index else self.theme.get_colour("close_icon_accent_colour") paint.shader = linear_gradient(self.x, self.y, self.x, self.y + header_height, (self.theme.get_colour("close_icon_colour"), close_colour)) canvas.draw_circle(icon_position.x, icon_position.y, self.icon_radius, paint)
def draw_text(offset_x, offset_y, width, height): canvas.paint.text_align = canvas.paint.TextAlign.CENTER for row in range(3): for col in range(3): text_string = "" text_string = f"{row*3+col+1}" text_rect = canvas.paint.measure_text(text_string)[1] center = Point2d( offset_x + width / 6 + col * width / 3, offset_y + height / 6 + row * height / 3, ) background_rect = text_rect.copy() background_rect.center = center background_rect = background_rect.inset(-4) paint.color = "9999995f" paint.style = Paint.Style.FILL canvas.draw_rect(background_rect) paint.color = "00ff00ff" canvas.draw_text( text_string, center.x, center.y + text_rect.height / 2, )
def setup_draw_cycle(self, canvas): """Drawing cycle that mimics a screen region set up""" region = HudScreenRegion("setup", "Setup mode text", "command_icon", "DD4500", canvas.rect, \ Point2d(canvas.rect.x, canvas.rect.y), vertical_centered = True) region.text_colour = "FFFFFF" canvas_rect = self.align_region_canvas_rect(region) self.draw_region(canvas, region, True) if self.canvas: self.canvas.pause()
def draw_text(): canvas.paint.text_align = canvas.paint.TextAlign.CENTER canvas.paint.textsize = 17 for row in range(0, self.rows + 1): skip_it = row % 2 == 0 for col in range(0, self.columns + 1): skip_it = not skip_it center = Point2d( col * self.field_size + self.field_size / 2, row * self.field_size + self.field_size / 2, ) if not skip_it or not self.checkers: text_string = f"{letters[row % len(letters)]}{letters[col % len(letters)]}" text_rect = canvas.paint.measure_text(text_string)[1] background_rect = text_rect.copy() background_rect.center = Point2d( col * self.field_size + self.field_size / 2, row * self.field_size + self.field_size / 2, ) background_rect = background_rect.inset(-4) if (self.input_so_far.startswith( letters[row % len(letters)]) or len(self.input_so_far) > 1 and self.input_so_far.endswith( letters[col % len(letters)])): canvas.paint.color = setting_row_highlighter.get( ) + hx(self.label_transparency) else: canvas.paint.color = setting_letters_background_color.get( ) + hx(self.label_transparency) canvas.paint.style = Paint.Style.FILL canvas.draw_rect(background_rect) canvas.paint.color = setting_small_letters_color.get( ) + hx(self.label_transparency) #paint.style = Paint.Style.STROKE canvas.draw_text( text_string, col * self.field_size + self.field_size / 2, row * self.field_size + self.field_size / 2 + text_rect.height / 2)
def draw_header_buttons(self, canvas, paint, dimensions): # Header button tray x = dimensions.x for index, icon in enumerate(self.icons): icon_position = Point2d(x + dimensions.width - (icon_radius * 1.5 + ( index * icon_radius * 2.2 )), dimensions.y + icon_radius + self.padding[0] / 2) self.icons[index].pos = icon_position paint.style = paint.Style.FILL if icon.id == "close": close_colour = self.theme.get_colour("close_icon_hover_colour") if self.icon_hovered == index else self.theme.get_colour("close_icon_accent_colour") paint.shader = linear_gradient(icon_position.x, dimensions.y, icon_position.x, icon_position.y + icon_radius, (self.theme.get_colour("close_icon_colour"), close_colour)) canvas.draw_circle(icon_position.x, icon_position.y, icon_radius, paint)
def determine_active_icon(self, pos): pos = Point2d(pos[0], pos[1]) active_icon = None size = None for icon in self.cursor_icons: if icon.rect is None and active_icon is None: active_icon = icon if icon.rect is not None and hit_test_rect(icon.rect, pos): if size is None or size > icon.rect.width * icon.rect.height: size = icon.rect.width * icon.rect.height active_icon = icon self.active_icon = active_icon
def hud_create_screen_region(topic: str, colour: str = None, icon: str = None, title: str = None, hover_visibility: Union[bool, int] = False, x: int = 0, y: int = 0, width: int = 0, height: int = 0, relative_x: int = 0, relative_y: int = 0): """Create a HUD screen region, where by default it is active all over the available space and it is visible only on a hover""" rect = ui.Rect(x, y, width, height) if width * height > 0 else None point = Point2d(x + relative_x, y + relative_y) return HudScreenRegion(topic, title, icon, colour, rect, point, hover_visibility)
def draw_footer_buttons(self, canvas, paint, dimensions): footer_height = dimensions["header_height"] dimensions = dimensions["rect"] # Small divider between the content and the footer x = dimensions.x + self.padding[3] start_y = dimensions.y + dimensions.height - self.padding[0] - self.padding[2] / 2 for index, icon in enumerate(self.footer_icons): icon_position = Point2d(x + dimensions.width - self.padding[3] - (self.icon_radius * 1.5 + ( index * self.icon_radius * 2.2 )), start_y - self.padding[0] * 2) self.footer_icons[index].pos = icon_position paint.style = paint.Style.FILL hover_colour = self.theme.get_colour("button_hover_background", "999999") if self.footer_icon_hovered == index \ else self.theme.get_colour("button_background", "CCCCCC") paint.shader = linear_gradient(self.x, self.y, self.x, self.y + footer_height, ("AAAAAA", hover_colour)) canvas.draw_circle(icon_position.x, icon_position.y, self.icon_radius, paint) image = self.theme.get_image(icon.image) if image: canvas.draw_image(image, icon_position.x - image.width / 2, icon_position.y - image.height / 2)
def determine_active_regions(self, pos): pos = Point2d(pos[0], pos[1]) active_regions = [] size = None for region in self.regions: if not region.hover_visibility: active_regions.append(region) else: if region.rect is None: active_regions.append(region) elif region.rect is not None and region.hover_visibility == 1 and hit_test_rect( region.rect, pos): active_regions.append(region) # For hover visibility -1 , we want the region canvas to be translucent when hovered # So it doesn"t occlude content - But otherwise it should be visible elif region.hover_visibility == -1: rect = self.align_region_canvas_rect(region) if not hit_test_rect(rect, pos): active_regions.append(region) if self.active_regions != active_regions: self.active_regions = active_regions for canvas_reference in self.canvases: canvas_reference["canvas"].freeze()
def jump(self, spoken_letters, number=-1, compasspoint=None): if number == -1: number = self.default_superblock + 1 base_rect = self.superblocks[number - 1].copy() base_rect.x += self.rect.x base_rect.y += self.rect.y spoken_letters = spoken_letters.upper() x_idx = letters.index(spoken_letters[1]) y_idx = letters.index(spoken_letters[0]) if compasspoint != None: index = direction_name_step.index(compasspoint) point = direction_vectors[index] else: point = Point2d(0, 0) ctrl.mouse_move( point.x + base_rect.x + x_idx * self.field_size + self.field_size / 2, point.y + base_rect.y + y_idx * self.field_size + self.field_size / 2) self.input_so_far = ""
def start_setup(self, setup_type, mouse_position=None): """Starts a setup mode that is used for moving, resizing and other various changes that the user might setup""" if (mouse_position is not None): self.drag_position = [ mouse_position[0] - self.limit_x, mouse_position[1] - self.limit_y ] if (setup_type not in self.allowed_setup_options and setup_type not in ["", "cancel", "reload"]): return pos = ctrl.mouse_pos() # Persist the user preferences when we end our setup if (self.setup_type != "" and not setup_type): self.drag_position = [] rect = self.canvas.rect if (self.setup_type == "dimension"): self.x = 0 self.y = 0 self.width = int(rect.width) self.height = int(rect.height) self.limit_width = int(rect.width) self.limit_height = int(rect.height) self.preferences.width = self.limit_width self.preferences.height = self.limit_height self.preferences.limit_width = self.limit_width self.preferences.limit_height = self.limit_height elif (self.setup_type == "font_size"): self.preferences.font_size = self.font_size self.setup_type = setup_type self.preferences.mark_changed = True self.canvas.pause() self.canvas.unregister("draw", self.setup_draw_cycle) self.canvas = None self.event_dispatch.request_persist_preferences() # Cancel every change elif setup_type == "cancel": self.drag_position = [] if (self.setup_type != ""): self.load({}, False) self.setup_type = "" if self.canvas: self.canvas.unregister("draw", self.setup_draw_cycle) self.canvas = None for canvas_reference in self.canvases: canvas_rect = self.align_region_canvas_rect( canvas_reference["region"]) canvas_reference["canvas"].rect = canvas_rect canvas_reference["canvas"].freeze() elif setup_type == "reload": self.drag_position = [] self.setup_type = "" for canvas_reference in self.canvases: canvas_reference["canvas"].freeze() # Start the setup by mocking a full screen screen region to place the canvas in else: main_screen = ui.main_screen() region = HudScreenRegion("setup", "Setup mode text", "command_icon", "DD4500", main_screen.rect, \ Point2d(main_screen.rect.x, main_screen.rect.y)) region.vertical_centered = True canvas_rect = self.align_region_canvas_rect(region) self.x = canvas_rect.x self.y = canvas_rect.y if not self.canvas: self.canvas = canvas.Canvas(self.x, self.y, self.limit_width, self.limit_height) self.canvas.register("draw", self.setup_draw_cycle) self.canvas.move(self.x, self.y) self.canvas.resume() super().start_setup(setup_type, mouse_position)
ctx = Context() ctx.matches = r""" tag: user.full_mouse_grid_enabled """ # stolen from the race car, this should probably go in a central spot somewhere direction_name_steps = [ "east", "east south east", "south east", "south south east", "south", "south south west", "south west", "west south west", "west", "west north west", "north west", "north north west", "north", "north north east", "north east", "east north east" ] direction_vectors = [Point2d(0, 0) for i in range(len(direction_name_steps))] direction_vectors[0] = Point2d(1, 0) direction_vectors[4] = Point2d(0, 1) direction_vectors[8] = Point2d(-1, 0) direction_vectors[12] = Point2d(0, -1) for i in [2, 6, 10, 14]: direction_vectors[i] = direction_vectors[ (i - 2) % len(direction_vectors)] + direction_vectors[ (i + 2) % len(direction_vectors)] for i in [1, 3, 5, 7, 9, 11, 13, 15]: direction_vectors[i] = ( direction_vectors[(i - 1) % len(direction_vectors)] + direction_vectors[(i + 1) % len(direction_vectors)]) / 2
def hud_show_context_menu(widget_id: str, pos_x: int, pos_y: int, buttons: list[HudButton]): """Show the context menu for a specific widget id""" hud.move_context_menu(widget_id, Point2d(pos_x, pos_y), buttons)
class HeadUpTextPanel(LayoutWidget): preferences = HeadUpDisplayUserWidgetPreferences(type="text_box", x=1680, y=50, width=200, height=200, limit_x=1580, limit_y=50, limit_width=300, limit_height=400, enabled=False, alignment="left", expand_direction="down", font_size=18) mouse_enabled = True # New content topic types topic_types = ["text"] current_topics = [] subscriptions = ["*"] # Top, right, bottom, left, same order as CSS padding padding = [3, 20, 10, 8] line_padding = 6 # Options given to the context menu default_buttons = [ HudButton("copy_icon", "Copy contents", ui.Rect(0,0,0,0), lambda widget: widget.copy_contents()) ] buttons = [] # All the header icons in a right to left order icon_radius = 10 icon_hovered = -1 icons = [HudIcon("close", "", Point2d(0,0), icon_radius, close_widget), HudIcon("minimize", "", Point2d(0,0), icon_radius, minimize_toggle_widget), ] # All the footer icons in a right to left order footer_icon_hovered = -1 footer_icons = [HudIcon("next", "next_icon", Point2d(0,0), icon_radius, lambda widget: widget.set_page_index(widget.page_index + 1)), HudIcon("previous", "previous_icon", Point2d(0,0), icon_radius, lambda widget: widget.set_page_index(widget.page_index - 1)) ] panel_content = HudPanelContent("", "", [""], [], 0, False) animation_max_duration = 60 def copy_contents(self): clip.set_text(remove_tokens_from_rich_text(self.panel_content.content[0])) actions.user.hud_add_log("event", "Copied contents to clipboard!") def update_panel(self, panel_content) -> bool: # Update the content buttons self.buttons = list(panel_content.buttons) self.buttons.extend(self.default_buttons) return super().update_panel(panel_content) def set_preference(self, preference, value, persisted=False): self.mark_layout_invalid = True super().set_preference(preference, value, persisted) def load_theme_values(self): self.intro_animation_start_colour = self.theme.get_colour_as_ints("intro_animation_start_colour") self.intro_animation_end_colour = self.theme.get_colour_as_ints("intro_animation_end_colour") self.blink_difference = [ self.intro_animation_end_colour[0] - self.intro_animation_start_colour[0], self.intro_animation_end_colour[1] - self.intro_animation_start_colour[1], self.intro_animation_end_colour[2] - self.intro_animation_start_colour[2] ] def on_mouse(self, event): icon_hovered = -1 for index, icon in enumerate(self.icons): if hit_test_icon(icon, event.gpos): icon_hovered = index footer_icon_hovered = -1 if icon_hovered == -1: for index, icon in enumerate(self.footer_icons): if hit_test_icon(icon, event.gpos): footer_icon_hovered = index if icon_hovered != self.icon_hovered or footer_icon_hovered != self.footer_icon_hovered: self.icon_hovered = icon_hovered self.footer_icon_hovered = footer_icon_hovered self.canvas.resume() if event.event == "mouseup" and event.button == 0: clicked_icon = None if icon_hovered != -1: clicked_icon = self.icons[icon_hovered] elif footer_icon_hovered != -1: clicked_icon = self.footer_icons[footer_icon_hovered] if clicked_icon != None: self.icon_hovered = -1 self.footer_icon_hovered = -1 clicked_icon.callback(self) if event.button == 1 and event.event == "mouseup": self.event_dispatch.show_context_menu(self.id, event.gpos, self.buttons) if event.button == 0 and event.event == "mouseup": self.event_dispatch.hide_context_menu() # Allow dragging and dropping with the mouse if icon_hovered == -1 and footer_icon_hovered == -1: super().on_mouse(event) def layout_content(self, canvas, paint): paint.textsize = self.font_size # Line padding needs to accumulate to at least 1.5 times the font size # In order to make it readable according to WCAG specifications # https://www.w3.org/TR/WCAG21/#visual-presentation self.line_padding = int(self.font_size / 2) + 1 if self.font_size <= 17 else 5 horizontal_alignment = "right" if self.limit_x < self.x else "left" vertical_alignment = "bottom" if self.limit_y < self.y else "top" layout_width = max(self.width - self.padding[1] - self.padding[3] * 2, self.limit_width - self.padding[1] * 2 - self.padding[3] * 2) icon_size = len(self.icons) * 2 * self.icon_radius """Calculates the width and the height of the content""" header_title = self.panel_content.title if self.panel_content.title != "" else self.id header_text = layout_rich_text(paint, header_title, self.limit_width - icon_size, self.limit_height) content_text = [] if self.minimized else layout_rich_text(paint, self.panel_content.content[0], layout_width, self.limit_height) layout_pages = [] line_count = 0 total_text_width = 0 total_text_height = 0 current_line_length = 0 header_height = 0 # The header can only be one line long, so only take the first line for index, text in enumerate(header_text): line_count = line_count + 1 if text.x == 0 else line_count if line_count <= 1: header_height = text.height + self.padding[0] * 2 current_line_length = current_line_length + text.width + self.icon_radius * 1.5 total_text_width = max( total_text_width, current_line_length ) header_height = max(header_height, self.padding[0] * 2 + self.icon_radius) page_height_limit = self.limit_height - header_height * 2 # We do not render content if the text box is minimized current_content_height = self.font_size + self.line_padding current_page_text = [] current_line_height = 0 if not self.minimized: line_count = 0 for index, text in enumerate(content_text): line_count = line_count + 1 if text.x == 0 else line_count current_line_length = current_line_length + text.width if text.x != 0 else text.width total_text_width = max( total_text_width, current_line_length ) total_text_height = total_text_height + current_line_height if text.x == 0 else total_text_height current_content_height = total_text_height + self.padding[0] + self.padding[2] + header_height # Recalculate current line height if we are starting a new line if text.x == 0: current_line_height = max(text.height, self.font_size) + self.line_padding else: current_line_height = max(current_line_height, max(text.height, self.font_size) + self.line_padding) if page_height_limit > current_content_height: current_page_text.append(text) # We have exceeded the page height limit, append the layout and try again else: width = min( self.limit_width, max(self.width, total_text_width + self.padding[1] + self.padding[3])) height = self.limit_height x = self.x if horizontal_alignment == "left" else self.limit_x + self.limit_width - width y = self.limit_y if vertical_alignment == "top" else self.limit_y + self.limit_height - height layout_pages.append({ "rect": ui.Rect(x, y, width, height), "line_count": max(1, line_count - 1), "header_text": header_text, "icon_size": icon_size, "content_text": current_page_text, "header_height": header_height, "content_height": current_content_height }) # Reset the variables total_text_height = current_line_height current_page_text = [text] line_count = 1 # Make sure the remainder of the content gets placed on the final page if len(current_page_text) > 0 or len(layout_pages) == 0: # If we are dealing with a single line going over to the only other page # Just remove the footer to make up for space if len(layout_pages) == 1 and line_count == 1: layout_pages[0]["line_count"] = layout_pages[0]["line_count"] + 1 layout_pages[0]["content_text"].extend(current_page_text) layout_pages[0]["content_height"] += current_line_height else: width = min( self.limit_width, max(self.width, total_text_width + self.padding[1] + self.padding[3])) content_height = header_height if self.minimized else total_text_height + self.padding[0] + self.padding[2] + header_height * 2 height = header_height + self.padding[0] * 2 if self.minimized else min(self.limit_height, max(self.height, content_height)) x = self.x if horizontal_alignment == "left" else self.limit_x + self.limit_width - width y = self.limit_y if vertical_alignment == "top" else self.limit_y + self.limit_height - height layout_pages.append({ "rect": ui.Rect(x, y, width, height), "line_count": max(1, line_count + 2 ), "header_text": header_text, "icon_size": icon_size, "content_text": current_page_text, "header_height": header_height, "content_height": content_height }) return layout_pages def draw_content(self, canvas, paint, dimensions) -> bool: paint.textsize = self.font_size paint.style = paint.Style.FILL # Draw the background first background_colour = self.theme.get_colour("text_box_background", "F5F5F5") paint.color = background_colour self.draw_background(canvas, paint, dimensions["rect"]) paint.color = self.theme.get_colour("text_colour") self.draw_content_text(canvas, paint, dimensions) self.draw_header(canvas, paint, dimensions) if not self.minimized and len(self.layout) > 1: self.draw_footer(canvas, paint, dimensions) self.draw_footer_buttons(canvas, paint, dimensions) self.draw_header_buttons(canvas, paint, dimensions) return False def draw_animation(self, canvas, animation_tick): if self.enabled: paint = canvas.paint if self.mark_layout_invalid and animation_tick == self.animation_max_duration - 1: self.layout = self.layout_content(canvas, paint) if self.page_index > len(self.layout) - 1: self.page_index = len(self.layout) -1 dimensions = self.layout[self.page_index]["rect"] # Determine colour of the animation animation_progress = ( animation_tick - self.animation_max_duration ) / self.animation_max_duration red = self.intro_animation_start_colour[0] - int( self.blink_difference[0] * animation_progress ) green = self.intro_animation_start_colour[1] - int( self.blink_difference[1] * animation_progress ) blue = self.intro_animation_start_colour[2] - int( self.blink_difference[2] * animation_progress ) red_hex = "0" + format(red, "x") if red <= 15 else format(red, "x") green_hex = "0" + format(green, "x") if green <= 15 else format(green, "x") blue_hex = "0" + format(blue, "x") if blue <= 15 else format(blue, "x") paint.color = red_hex + green_hex + blue_hex if self.minimized: self.draw_background(canvas, paint, dimensions) else: header_height = self.layout[self.page_index]["header_height"] growth = (self.animation_max_duration - animation_tick ) / self.animation_max_duration easeInOutQuint = 16 * growth ** 5 if growth < 0.5 else 1 - pow(-2 * growth + 2, 5) / 2 rect = ui.Rect(dimensions.x, dimensions.y, dimensions.width, max(header_height, dimensions.height * easeInOutQuint)) self.draw_background(canvas, paint, rect) if animation_tick == 1: return self.draw(canvas) return True else: return False def draw_header(self, canvas, paint, dimensions): header_height = dimensions["header_height"] dimensions = dimensions["rect"] paint.color = self.theme.get_colour("text_colour") paint.font.embolden = True x = dimensions.x + self.padding[3] canvas.draw_text(self.panel_content.title if self.panel_content.title else self.id, x, dimensions.y + self.font_size) # Small divider between the content and the header if not self.minimized: paint.color = self.theme.get_colour("text_box_line", "000000") canvas.draw_rect(ui.Rect(x - self.padding[3], dimensions.y + header_height + self.padding[0] * 2, dimensions.width, 1)) def draw_header_buttons(self, canvas, paint, dimensions): header_height = dimensions["header_height"] dimensions = dimensions["rect"] # Header button tray x = dimensions.x for index, icon in enumerate(self.icons): # Minimize / maximize icon icon_position = Point2d(x + dimensions.width - (self.icon_radius * 1.5 + ( index * self.icon_radius * 2.2 )), dimensions.y + self.icon_radius + self.padding[0] / 2) self.icons[index].pos = icon_position paint.style = paint.Style.FILL if icon.id == "minimize": hover_colour = self.theme.get_colour("button_hover_background", "999999") if self.icon_hovered == index \ else self.theme.get_colour("button_background", "CCCCCC") paint.shader = linear_gradient(self.x, self.y, self.x, self.y + header_height, (hover_colour, hover_colour)) canvas.draw_circle(icon_position.x, icon_position.y, self.icon_radius, paint) text_colour = self.theme.get_colour("icon_colour", "000000") paint.shader = linear_gradient(self.x, self.y, self.x, self.y + header_height, (text_colour, text_colour)) if not self.minimized: canvas.draw_rect(ui.Rect(icon_position.x - self.icon_radius / 2, icon_position.y - 1, self.icon_radius, 2)) else: paint.style = paint.Style.STROKE canvas.draw_rect(ui.Rect( 1 + icon_position.x - self.icon_radius / 2, icon_position.y - self.icon_radius / 2, self.icon_radius - 2, self.icon_radius - 2)) elif icon.id == "close": close_colour = self.theme.get_colour("close_icon_hover_colour") if self.icon_hovered == index else self.theme.get_colour("close_icon_accent_colour") paint.shader = linear_gradient(self.x, self.y, self.x, self.y + header_height, (self.theme.get_colour("close_icon_colour"), close_colour)) canvas.draw_circle(icon_position.x, icon_position.y, self.icon_radius, paint) def draw_footer(self, canvas, paint, dimensions): footer_height = dimensions["header_height"] dimensions = dimensions["rect"] # Small divider between the content and the header x = dimensions.x + self.padding[3] start_y = dimensions.y + dimensions.height - self.padding[0] - self.padding[2] / 2 paint.color = self.theme.get_colour("text_colour") canvas.draw_text(str(self.page_index + 1 ) + " of " + str(len(self.layout)), x, start_y) paint.color = self.theme.get_colour("text_box_line", "000000") canvas.draw_rect(ui.Rect(x - self.padding[3], start_y - footer_height, dimensions.width, 1)) def draw_footer_buttons(self, canvas, paint, dimensions): footer_height = dimensions["header_height"] dimensions = dimensions["rect"] # Small divider between the content and the footer x = dimensions.x + self.padding[3] start_y = dimensions.y + dimensions.height - self.padding[0] - self.padding[2] / 2 for index, icon in enumerate(self.footer_icons): icon_position = Point2d(x + dimensions.width - self.padding[3] - (self.icon_radius * 1.5 + ( index * self.icon_radius * 2.2 )), start_y - self.padding[0] * 2) self.footer_icons[index].pos = icon_position paint.style = paint.Style.FILL hover_colour = self.theme.get_colour("button_hover_background", "999999") if self.footer_icon_hovered == index \ else self.theme.get_colour("button_background", "CCCCCC") paint.shader = linear_gradient(self.x, self.y, self.x, self.y + footer_height, ("AAAAAA", hover_colour)) canvas.draw_circle(icon_position.x, icon_position.y, self.icon_radius, paint) image = self.theme.get_image(icon.image) if image: canvas.draw_image(image, icon_position.x - image.width / 2, icon_position.y - image.height / 2) def draw_content_text(self, canvas, paint, dimensions): """Draws the content and returns the height of the drawn content""" paint.textsize = self.font_size header_height = dimensions["header_height"] rich_text = dimensions["content_text"] content_height = dimensions["content_height"] line_count = dimensions["line_count"] dimensions = dimensions["rect"] text_x = dimensions.x + self.padding[3] text_y = dimensions.y + header_height + self.padding[0] * 2 #line_height = ( content_height - header_height - self.padding[0] - self.padding[2] ) / line_count self.draw_rich_text(canvas, paint, rich_text, text_x, text_y, self.line_padding) def draw_background(self, canvas, paint, rect): radius = 10 rrect = skia.RoundRect.from_rect(rect, x=radius, y=radius) canvas.draw_rrect(rrect)
class HeadUpWalkthroughPanel(LayoutWidget): preferences = HeadUpDisplayUserWidgetPreferences(type="walkthrough", x=910, y=1000, width=100, height=20, limit_x=420, limit_y=784, limit_width=1080, limit_height=230, enabled=False, sleep_enabled=True, alignment="center", expand_direction="up", font_size=24) mouse_enabled = True step_scheduled = None # Top, right, bottom, left, same order as CSS padding padding = [12, 20, 12, 8] line_padding = 6 button_padding = 8 # New content topic types topic_types = ["walkthrough_step"] current_topics = [] subscriptions = ["*"] # Animation frame related variables animation_max_duration = 30 max_transition_animation_state = 30 max_animated_word_state = 20 transition_animation_state = 0 animated_word_state = 0 animated_words = [] commands_positions = {} icon_hovered = -1 icons = [ HudIcon("close", "", Point2d(0,0), icon_radius, close_widget) ] # Previous dimensions to transition from previous_content_dimensions = None # Options given to the context menu buttons = [ HudButton("next_icon", "Skip this step", ui.Rect(0,0,0,0), lambda widget: actions.user.hud_skip_walkthrough_step()), HudButton("check_icon", "Mark as done", ui.Rect(0,0,0,0), lambda widget: actions.user.hud_skip_walkthrough_all()), HudButton("", "Restore current step", ui.Rect(0,0,0,0), lambda widget: actions.user.hud_restore_walkthrough_step()) ] walkthrough_button_hovered = -1 walkthrough_buttons = [ HudButton("", "Previous", ui.Rect(0,0,0,0), lambda widget: actions.user.hud_previous_walkthrough_step()), HudButton("", "Walkthrough options", ui.Rect(0,0,0,0), lambda widget: actions.user.hud_widget_options(widget.id)), HudButton("", "Continue", ui.Rect(0,0,0,0), lambda widget: actions.user.hud_skip_walkthrough_step()) ] previous_progress = HudContentPage(0,1,0) voice_commands_available = [] previous_walkthrough_step = None def disable(self, persisted=False): self.previous_content_dimensions = None self.transition_animation_state = 0 super().disable(persisted) def should_enable(self): current_walkthrough_step = self.content.get_topic("walkthrough_step") return current_walkthrough_step is not None and len(current_walkthrough_step) > 0 def content_handler(self, event) -> bool: replaced = False if event.topic_type == "walkthrough_step": if event.operation == "replace": current_walkthrough_step = self.content.get_topic("walkthrough_step") self.previous_walkthrough_step = copy.copy(current_walkthrough_step[0]) if len(current_walkthrough_step) > 0 else None self.previous_progress = self.previous_walkthrough_step.progress if self.previous_walkthrough_step is not None else HudContentPage(0,1,0) replaced = True elif event.operation == "remove": self.previous_walkthrough_step = None self.previous_progress = HudContentPage(0,1,0) self.mark_layout_invalid = True super().content_handler(event) if replaced: if self.show_animations and ( self.previous_progress and self.previous_progress.percent != event.content.progress.percent ) or \ self.previous_walkthrough_step is None: if self.layout is None: self.previous_content_dimensions = None else: self.previous_content_dimensions = self.layout[self.page_index]["rect"] if self.page_index < len(self.layout) else None self.page_index = 0 should_animate = self.previous_content_dimensions is not None and self.enabled == True self.transition_animation_state = self.max_transition_animation_state if should_animate else 0 self.animated_words = [] def refresh(self, new_content): # Animate the new words if "event" in new_content and new_content["event"].topic_type == "walkthrough_step": if self.previous_walkthrough_step is None or new_content["event"].operation == "remove": self.animated_words = [] self.animated_word_state = 0 self.previous_progress = HudContentPage(0,1,0) else: if self.show_animations and self.previous_walkthrough_step.said_walkthrough_commands is not None \ and new_content["event"].content.said_walkthrough_commands is not None \ and len(self.previous_walkthrough_step.said_walkthrough_commands) != len(new_content["event"].content.said_walkthrough_commands): animated_words = [] for said_voice_command in new_content["event"].content.said_walkthrough_commands: current_count = self.previous_walkthrough_step.said_walkthrough_commands.count(said_voice_command) new_count = new_content["event"].content.said_walkthrough_commands.count(said_voice_command) if new_count > current_count: for index in range(new_count - current_count): indexed_voice_command = said_voice_command + ":" + str(current_count + index) if indexed_voice_command not in animated_words: animated_words.append(indexed_voice_command) self.animated_words = animated_words self.animated_word_state = self.max_animated_word_state else: self.animated_words = [] self.animated_word_state = 0 if not self.enabled and new_content["event"].show: self.enable() super().refresh(new_content) if self.enabled: if not self.canvas: self.generate_canvases() self.canvas.resume() def on_mouse(self, event): icon_hovered = -1 for index, icon in enumerate(self.icons): if hit_test_icon(icon, event.gpos): icon_hovered = index button_hovered = -1 for index, button in enumerate(self.walkthrough_buttons): if hit_test_button(button, event.gpos): button_hovered = index if event.event == "mouseup" and event.button == 0: clicked_element = None if icon_hovered != -1: clicked_element = self.icons[icon_hovered] if button_hovered != -1: clicked_element = self.walkthrough_buttons[button_hovered] self.event_dispatch.hide_context_menu() if clicked_element != None: self.icon_hovered = -1 self.button_hovered = -1 clicked_element.callback(self) elif event.button == 1 and event.event == "mouseup": self.event_dispatch.show_context_menu(self.id, event.gpos, self.buttons) if icon_hovered != self.icon_hovered or button_hovered != self.walkthrough_button_hovered: self.icon_hovered = icon_hovered self.walkthrough_button_hovered = button_hovered self.canvas.resume() # Allow dragging and dropping with the mouse if icon_hovered == -1 and button_hovered == -1: super().on_mouse(event) def set_preference(self, preference, value, persisted=False): self.mark_layout_invalid = True super().set_preference(preference, value, persisted) def load_theme_values(self): self.intro_animation_start_colour = self.theme.get_colour_as_ints("intro_animation_start_colour") self.intro_animation_end_colour = self.theme.get_colour_as_ints("intro_animation_end_colour") self.blink_difference = [ self.intro_animation_end_colour[0] - self.intro_animation_start_colour[0], self.intro_animation_end_colour[1] - self.intro_animation_start_colour[1], self.intro_animation_end_colour[2] - self.intro_animation_start_colour[2] ] def layout_content(self, canvas, paint): current_walkthrough_step = self.content.get_topic("walkthrough_step") if current_walkthrough_step is not None and len(current_walkthrough_step) > 0: current_walkthrough_step = current_walkthrough_step[0] else: # If there is no current walkthrough step - Just clear the layout completely and don"t calculate anything return [None] paint.textsize = self.font_size self.line_padding = int(self.font_size / 2) + 1 if self.font_size <= 17 else 5 horizontal_alignment = "right" if self.limit_x < self.x else "left" vertical_alignment = "bottom" if self.limit_y < self.y else "top" if self.alignment == "center" or \ ( self.x + self.width < self.limit_x + self.limit_width and self.limit_x < self.x ): horizontal_alignment = "center" icon_padding = icon_radius layout_width = self.limit_width - self.padding[1] * 2 - self.padding[3] * 2 - icon_padding max_text_width = layout_width - icon_padding - self.padding[1] if self.width < self.limit_width else self.limit_width - self.padding[1] - self.padding[3] - icon_padding content_text = [] if self.minimized else layout_rich_text(paint, current_walkthrough_step.content if not current_walkthrough_step.show_context_hint else current_walkthrough_step.context_hint, max_text_width, self.limit_height) footer_height = self.font_size + self.padding[2] + self.padding[0] # Calculate the size of the footer buttons button_x = self.limit_x layout_buttons = [] total_button_width = 0 for index, button in enumerate(self.walkthrough_buttons): button_content_text = layout_rich_text(paint, button.text, layout_width - icon_padding, self.font_size) layout_buttons.append({"text": [button_content_text[0]], "rect": ui.Rect(button_x, self.limit_y + self.limit_height - footer_height + self.padding[0], button_content_text[0].width + self.button_padding * 2, self.font_size + self.padding[2] / 2)}) button_x += button_content_text[0].width + self.button_padding * 3 total_button_width += button_content_text[0].width + self.button_padding * 3 total_button_width -= self.button_padding * 3 # Reposition the buttons based on alignment and their combined width left_offset = 0 if self.alignment == "center": left_offset = ( self.limit_width - total_button_width ) / 2 elif self.alignment == "right": left_offset = self.limit_width - total_button_width for index, layout_button in enumerate(layout_buttons): layout_buttons[index]["rect"].x += left_offset # Start segmenting the available voice commands by indexes self.voice_commands_available = [] self.commands_positions = {} voice_command_words = [] voice_command_indecis = [] for index, text in enumerate(content_text): if "command_available" in text.styles: voice_command_words += string_to_speakable_string(text.text).split() voice_command_indecis.append(index) elif len(voice_command_words) > 0: voice_command = " ".join(voice_command_words) for voice_command_index in voice_command_indecis: self.commands_positions[str(voice_command_index)] = voice_command + ":" + str(self.voice_commands_available.count(voice_command)) self.voice_commands_available.append(voice_command) voice_command_words = [] voice_command_indecis = [] # Add remaining voice commands if they haven"t been added yet if len(voice_command_words) > 0: voice_command = " ".join(voice_command_words) for voice_command_index in voice_command_indecis: self.commands_positions[str(voice_command_index)] = voice_command + ":" + str(self.voice_commands_available.count(voice_command)) voice_command_words = [] voice_command_indecis = [] self.voice_commands_available.append(voice_command) layout_pages = [] line_count = 0 total_text_width = 0 total_text_height = 0 current_line_length = 0 page_height_limit = self.limit_height - footer_height - self.padding[0] + self.padding[2] # We do not render content if the text box is minimized current_content_height = self.padding[0] + self.padding[2] current_page_text = [] current_line_height = 0 if not self.minimized: line_count = 0 for index, text in enumerate(content_text): line_count = line_count + 1 if text.x == 0 else line_count current_line_length = current_line_length + text.width if text.x != 0 else text.width total_text_width = max( total_text_width, current_line_length ) total_text_height = total_text_height + max(text.height, self.font_size) + self.line_padding if text.x == 0 else total_text_height current_content_height = total_text_height current_line_height = max(text.height, self.font_size) + self.line_padding if page_height_limit > current_content_height: current_page_text.append(text) # We have exceeded the page height limit, append the layout and try again else: width = min( self.limit_width, max(self.width, total_text_width + self.padding[1] + self.padding[1] + self.padding[3])) height = self.limit_height x = self.x if horizontal_alignment == "left" else self.limit_x + self.limit_width - width if horizontal_alignment == "center": x = self.limit_x + ( self.limit_width - width ) / 2 y = self.limit_y if vertical_alignment == "top" else self.limit_y + self.limit_height - height - footer_height layout_pages.append({ "rect": ui.Rect(x, y, width, height), "line_count": max(1, line_count - 1), "content_text": current_page_text, "content_height": current_content_height, "layout_buttons": layout_buttons, "footer_height": footer_height }) # Reset the variables total_text_height = current_line_height current_page_text = [text] current_content_height = self.padding[0] + self.padding[2] line_count = 1 # Make sure the remainder of the content gets placed on the final page if len(current_page_text) > 0 or len(layout_pages) == 0: width = min( self.limit_width, max(self.width, total_text_width + self.padding[1] + self.padding[1] + self.padding[3])) content_height = total_text_height + self.padding[0] + self.padding[2] if self.height == self.limit_height: height = min(self.limit_height, min(self.height, content_height) + footer_height) else: height = min(self.limit_height, max(self.height, content_height) + footer_height) x = self.x if horizontal_alignment == "left" else self.limit_x + self.limit_width - width if horizontal_alignment == "center": x = self.limit_x + ( self.limit_width - width ) / 2 y = self.limit_y if vertical_alignment == "top" else self.limit_y + self.limit_height - height last_page_layout_buttons = [] if vertical_alignment == "top": for index, layout_button in enumerate(layout_buttons): last_page_layout_button = { "text": layout_button["text"], "rect": ui.Rect(layout_button["rect"].x, min(self.limit_y + self.limit_height - footer_height + self.padding[0], y + content_height + self.padding[0]), layout_button["rect"].width, layout_button["rect"].height) } last_page_layout_buttons.append(last_page_layout_button) else: last_page_layout_buttons = layout_buttons layout_pages.append({ "rect": ui.Rect(x, y, width, height), "line_count": max(1, line_count + 2 ), "content_text": current_page_text, "content_height": content_height, "layout_buttons": last_page_layout_buttons, "footer_height": footer_height }) return layout_pages def draw_content(self, canvas, paint, dimensions) -> bool: # Disable if there is no content if dimensions is None: self.disable() return False current_walkthrough_step = self.content.get_topic("walkthrough_step") if current_walkthrough_step is not None and len(current_walkthrough_step) > 0: current_walkthrough_step = current_walkthrough_step[0] paint.textsize = self.font_size paint.style = paint.Style.FILL progress_bar_offset = 0 progress_bar_height = 7 # Animate the transition if an animation state has been set self.transition_animation_state = max(0, self.transition_animation_state - 1) animation_in_progress = self.transition_animation_state > 0 # Draw the buttons buttons = dimensions["layout_buttons"] for index, button_layout in enumerate(buttons): self.walkthrough_buttons[index].rect = button_layout["rect"] button_colour = self.theme.get_colour("button_background", "BBBBBB") text_colour = self.theme.get_colour("button_text_colour", "000000FF") button_hovered = self.walkthrough_button_hovered == index if animation_in_progress: if len(button_colour) == 8: button_colour = button_colour[:-2] button_colour += "55" # More transparent during transition elif button_hovered: button_colour = self.theme.get_colour("button_hover_background", "CCCCCC") text_colour = self.theme.get_colour("button_hover_text_colour", "000000FF") paint.color = button_colour canvas.draw_rrect( skia.RoundRect.from_rect(button_layout["rect"], x=10, y=10) ) paint.color = self.theme.get_colour("text_colour") self.draw_rich_text(canvas, paint, button_layout["text"], button_layout["rect"].x + self.button_padding, button_layout["rect"].y, self.font_size, False, current_walkthrough_step) # Draw the background first background_colour = self.theme.get_colour("text_box_background", "F5F5F5") paint.color = background_colour if animation_in_progress: growth = (self.max_transition_animation_state - self.transition_animation_state ) / self.max_transition_animation_state easeInOutQuint = 16 * growth ** 5 if growth < 0.5 else 1 - pow(-2 * growth + 2, 5) / 2 difference = ui.Rect((dimensions["rect"].x - self.previous_content_dimensions.x ) * easeInOutQuint, ( dimensions["rect"].y - self.previous_content_dimensions.y ) * easeInOutQuint, ( dimensions["rect"].width - self.previous_content_dimensions.width ) * easeInOutQuint, ( dimensions["rect"].height - self.previous_content_dimensions.height) * easeInOutQuint) background_rect = ui.Rect(self.previous_content_dimensions.x + difference.x, self.previous_content_dimensions.y + difference.y, self.previous_content_dimensions.width + difference.width, self.previous_content_dimensions.height + difference.height - dimensions["footer_height"]) self.draw_background(canvas, paint, background_rect) # Reverse the growth if we are heading to a previous step growth = growth if current_walkthrough_step.progress.percent > self.previous_progress.percent else growth * -1 # Draw the progress bar progress = ( self.previous_progress.percent + ( growth * (abs(current_walkthrough_step.progress.percent - self.previous_progress.percent))) ) * 0.01 paint.color = self.theme.get_colour("spoken_voice_command_background_colour", "6CC653") rect = ui.Rect(background_rect.x + progress_bar_offset, background_rect.y, \ min(background_rect.width - progress_bar_offset * 2, (background_rect.width - progress_bar_offset * 2) * progress), progress_bar_height) canvas.draw_rect(rect) self.draw_header_buttons(canvas, paint, background_rect) else: self.animated_word_state = max(0, self.animated_word_state - 1) # Draw the background first background_rect = ui.Rect(dimensions["rect"].x, dimensions["rect"].y, dimensions["rect"].width, dimensions["rect"].height - dimensions["footer_height"]) self.draw_background(canvas, paint, background_rect) # Draw the progress bar paint.color = self.theme.get_colour("spoken_voice_command_background_colour", "6CC653") rect = ui.Rect(dimensions["rect"].x + progress_bar_offset, dimensions["rect"].y, \ min(dimensions["rect"].width - progress_bar_offset * 2, (dimensions["rect"].width - progress_bar_offset * 2) * ( current_walkthrough_step.progress.percent * 0.01 )), progress_bar_height) canvas.draw_rect(rect) paint.color = self.theme.get_colour("text_colour") # Keep an offset of previous pages to make sure the animations are properly taken into account for highlighting commands text_index_offset = 0 if self.page_index > 0: current_page_index = self.page_index while(current_page_index > 0): current_page_index -= 1 text_index_offset += len(self.layout[current_page_index]["content_text"]) text_index_offset = max(0, text_index_offset) self.draw_voice_command_backgrounds(canvas, paint, dimensions, self.animated_word_state, current_walkthrough_step, text_index_offset) self.draw_content_text(canvas, paint, dimensions, current_walkthrough_step, text_index_offset) self.draw_header_buttons(canvas, paint, dimensions["rect"]) return self.transition_animation_state > 0 or self.animated_word_state > 0 def draw_animation(self, canvas, animation_tick): if self.enabled and self.should_enable(): paint = canvas.paint if self.mark_layout_invalid and animation_tick == self.animation_max_duration - 1: self.layout = self.layout_content(canvas, paint) if self.page_index > len(self.layout) - 1: self.page_index = len(self.layout) -1 dimensions = self.layout[self.page_index]["rect"] # Determine colour of the animation animation_progress = ( animation_tick - self.animation_max_duration ) / self.animation_max_duration red = self.intro_animation_start_colour[0] - int( self.blink_difference[0] * animation_progress ) green = self.intro_animation_start_colour[1] - int( self.blink_difference[1] * animation_progress ) blue = self.intro_animation_start_colour[2] - int( self.blink_difference[2] * animation_progress ) red_hex = "0" + format(red, "x") if red <= 15 else format(red, "x") green_hex = "0" + format(green, "x") if green <= 15 else format(green, "x") blue_hex = "0" + format(blue, "x") if blue <= 15 else format(blue, "x") paint.color = red_hex + green_hex + blue_hex horizontal_alignment = "right" if self.limit_x < self.x else "left" if self.alignment == "center" or \ ( self.x + self.width < self.limit_x + self.limit_width and self.limit_x < self.x ): horizontal_alignment = "center" growth = (self.animation_max_duration - animation_tick ) / self.animation_max_duration easeInOutQuint = 16 * growth ** 5 if growth < 0.5 else 1 - pow(-2 * growth + 2, 5) / 2 width = dimensions.width * easeInOutQuint if horizontal_alignment == "left": x = dimensions.x elif horizontal_alignment == "right": x = self.limit_x + self.limit_width - width elif horizontal_alignment == "center": x = self.limit_x + ( self.limit_width / 2 ) - ( width / 2 ) rect = ui.Rect(x, dimensions.y, width, dimensions.height - self.layout[self.page_index]["footer_height"]) if animation_tick == 1: return self.draw(canvas) else: self.draw_background(canvas, paint, rect) return True else: return False def draw_content_text(self, canvas, paint, dimensions, current_walkthrough_step, text_index_offset=0) -> int: """Draws the content and returns the height of the drawn content""" paint.textsize = self.font_size # Line padding needs to accumulate to at least 1.5 times the font size # In order to make it readable according to WCAG specifications # https://www.w3.org/TR/WCAG21/#visual-presentation self.line_padding = int(self.font_size / 2) + 1 if self.font_size <= 17 else 5 rich_text = dimensions["content_text"] content_height = dimensions["content_height"] line_count = dimensions["line_count"] dimensions = dimensions["rect"] text_x = dimensions.x + self.padding[3] text_y = dimensions.y + self.padding[0] / 2 self.draw_rich_text(canvas, paint, rich_text, text_x, text_y, self.line_padding, False, current_walkthrough_step, text_index_offset) def draw_background(self, canvas, paint, rect): radius = 10 rrect = skia.RoundRect.from_rect(rect, x=radius, y=radius) canvas.draw_rrect(rrect) def draw_voice_command_backgrounds(self, canvas, paint, dimensions, animation_state, current_walkthrough_step, text_index_offset=0): text_colour = paint.color rich_text = dimensions["content_text"] content_height = dimensions["content_height"] line_count = dimensions["line_count"] dimensions = dimensions["rect"] x = dimensions.x + self.padding[3] y = dimensions.y + self.padding[0] non_spoken_background_colour = self.theme.get_colour("voice_command_background_colour", "535353") spoken_background_colour = self.theme.get_colour("spoken_voice_command_background_colour", "6CC653") current_line = -1 for index, text in enumerate(rich_text): current_line = current_line + 1 if text.x == 0 else current_line if text.x == 0: y += paint.textsize if index != 0: y += self.line_padding if "command_available" in text.styles and text.text.strip() != "": command_padding = self.line_padding / 2 text_size = max(paint.textsize, text.height) rect = ui.Rect(x + text.x - command_padding, y - paint.textsize - self.line_padding, text.width + command_padding * 2, text_size + command_padding * 2) if animation_state > 0 and str(text_index_offset + index) in self.commands_positions and \ self.commands_positions[str(text_index_offset + index)] in self.animated_words: growth = (self.max_animated_word_state - animation_state ) / self.max_animated_word_state easeOutQuad = 1 - pow(1 - growth, 4) easeOutQuint = 1 - pow(1 - growth, 5) # Draw the colour shifting rectangle colour_from = hex_to_ints(non_spoken_background_colour) colour_to = hex_to_ints(spoken_background_colour) red_value = int(round(colour_from[0] + ( colour_to[0] - colour_from[0] ) * easeOutQuad)) green_value = int(round(colour_from[1] + ( colour_to[1] - colour_from[1] ) * easeOutQuad)) blue_value = int(round(colour_from[2] + ( colour_to[2] - colour_from[2] ) * easeOutQuad)) red_hex = "0" + format(red_value, "x") if red_value <= 15 else format(red_value, "x") green_hex = "0" + format(green_value, "x") if green_value <= 15 else format(green_value, "x") blue_hex = "0" + format(blue_value, "x") if blue_value <= 15 else format(blue_value, "x") colour = red_hex + green_hex + blue_hex paint.color = colour paint.style = Paint.Style.FILL canvas.draw_rrect(skia.RoundRect.from_rect(rect, x=5, y=5)) # Draw the expanding border expand = ( self.font_size / 2 ) * easeOutQuint alpha = int(round( 255 * (1 - easeOutQuint) )) alpha_hex = "0" + format(alpha, "x") if alpha <= 15 else format(alpha, "x") paint.color = colour + alpha_hex paint.style = Paint.Style.STROKE paint.stroke_width = 4 rect.x -= int(round(expand / 2)) rect.y -= int(round(expand / 2)) rect.height += int(round(expand)) rect.width += int(round(expand)) canvas.draw_rrect(skia.RoundRect.from_rect(rect, x=5, y=5)) paint.stroke_width = 1 # Not an animated set of words - Just draw the state else: paint.color = non_spoken_background_colour if str(text_index_offset + index) in self.commands_positions: used_voice_commands = [] for voice_command in current_walkthrough_step.said_walkthrough_commands: if self.commands_positions[str(text_index_offset + index)] == voice_command + ":" + str(used_voice_commands.count(voice_command)): paint.color = spoken_background_colour break used_voice_commands.append(voice_command) paint.style = Paint.Style.FILL canvas.draw_rrect(skia.RoundRect.from_rect(rect, x=5, y=5)) paint.color = text_colour paint.style = Paint.Style.FILL def draw_rich_text(self, canvas, paint, rich_text, x, y, line_padding, single_line=False, current_walkthrough_step=None, text_index_offset=0): # Mostly copied over from layout_widget # Draw text line by line text_colour = paint.color error_colour = self.theme.get_colour("error_colour", "AA0000") warning_colour = self.theme.get_colour("warning_colour", "F75B00") success_colour = self.theme.get_colour("success_colour", "00CC00") info_colour = self.theme.get_colour("info_colour", "30AD9E") spoken_voice_command_text_colour = self.theme.get_colour("spoken_voice_command_text_colour", "000000FF") voice_command_text_colour = self.theme.get_colour("voice_command_text_colour", "DDDDDD") current_line = -1 for index, text in enumerate(rich_text): paint.font.embolden = "bold" in text.styles paint.font.skew_x = -0.33 if "italic" in text.styles else 0 paint.color = text_colour if "command_available" in text.styles: paint.color = voice_command_text_colour if str(text_index_offset + index) in self.commands_positions: used_voice_commands = [] for voice_command in current_walkthrough_step.said_walkthrough_commands: if self.commands_positions[str(text_index_offset + index)] == voice_command + ":" + str(used_voice_commands.count(voice_command)): paint.color = spoken_voice_command_text_colour break used_voice_commands.append(voice_command) else: if "warning" in text.styles: paint.color = warning_colour elif "success" in text.styles: paint.color = success_colour elif "error" in text.styles: paint.color = error_colour elif "notice" in text.styles: paint.color = info_colour current_line = current_line + 1 if text.x == 0 else current_line if single_line and current_line > 0: return if text.x == 0: y += paint.textsize if index != 0: y += line_padding canvas.draw_text(text.text, x + text.x, y ) def draw_header_buttons(self, canvas, paint, dimensions): # Header button tray x = dimensions.x for index, icon in enumerate(self.icons): icon_position = Point2d(x + dimensions.width - (icon_radius * 1.5 + ( index * icon_radius * 2.2 )), dimensions.y + icon_radius + self.padding[0] / 2) self.icons[index].pos = icon_position paint.style = paint.Style.FILL if icon.id == "close": close_colour = self.theme.get_colour("close_icon_hover_colour") if self.icon_hovered == index else self.theme.get_colour("close_icon_accent_colour") paint.shader = linear_gradient(icon_position.x, dimensions.y, icon_position.x, icon_position.y + icon_radius, (self.theme.get_colour("close_icon_colour"), close_colour)) canvas.draw_circle(icon_position.x, icon_position.y, icon_radius, paint)