from ui_colors import UIColors TEXT_LEFT = 0 TEXT_CENTER = 1 TEXT_RIGHT = 2 BUTTON_STATES = ["normal", "hovered", "clicked", "dimmed"] class UIButton: "clickable button that does something in a UIElement" # x/y/width/height given in tile scale x, y = 0, 0 width, height = 1, 1 caption = "TEST" caption_justify = TEXT_LEFT # paint caption from string, or not should_draw_caption = True caption_y = 0 callback = None normal_fg_color = UIColors.black normal_bg_color = UIColors.lightgrey hovered_fg_color = UIColors.black hovered_bg_color = UIColors.white clicked_fg_color = UIColors.white clicked_bg_color = UIColors.black dimmed_fg_color = UIColors.black dimmed_bg_color = UIColors.medgrey # dimmed is a special, alternative-to-normal state dimmed = False can_hover = True can_click = True visible = True # if true, this button is invisible and used for special trickery never_draw = False # weird (gross?) thing: other code can stash an argument to callback here cb_arg = None # if True, pass in mouse button # pass_mouse_button = False # if true, clear all characters before painting a new caption clear_before_caption_draw = False # if true, display a tooltip when hovered, and dismiss it when unhovered. # contents set from get_tooltip_text and positioned by get_tooltip_location. tooltip_on_hover = False def __init__(self, element, starting_state=None): self.element = element self.state = starting_state or "normal" def log_event(self, event_type): "common code for button event logging" if self.element.ui.logg: self.element.ui.app.log( f"UIButton: {self.element.__class__.__name__}'s {self.__class__.__name__} {event_type}" ) def set_state(self, new_state): if new_state not in BUTTON_STATES: self.element.ui.app.log( f"Unrecognized state for button {self.__class__.__name__}: {new_state}" ) return self.dimmed = new_state == "dimmed" self.state = new_state self.set_state_colors() def get_state_colors(self, state): fg = getattr(self, f"{state}_fg_color") bg = getattr(self, f"{state}_bg_color") return fg, bg def set_state_colors(self): if self.never_draw: return # set colors for entire button area based on current state if self.dimmed and self.state == "normal": self.state = "dimmed" # just bail if we're trying to draw something out of bounds if ( self.x + self.width > self.element.art.width or self.y + self.height > self.element.art.height ): return fg, bg = self.get_state_colors(self.state) for y in range(self.height): for x in range(self.width): self.element.art.set_tile_at(0, 0, self.x + x, self.y + y, None, fg, bg) def update_tooltip(self): tt = self.element.ui.tooltip tt.reset_art() tt.set_text(self.get_tooltip_text()) tt.tile_x, tt.tile_y = self.get_tooltip_location() tt.reset_loc() def hover(self): self.log_event("hovered") self.set_state("hovered") if self.tooltip_on_hover: self.element.ui.tooltip.visible = True self.update_tooltip() def unhover(self): self.log_event("unhovered") if self.dimmed: self.set_state("dimmed") else: self.set_state("normal") if self.tooltip_on_hover: # if two buttons are adjacent, we might be unhovering this one # right after hovering the other in the same frame. if so, # don't dismiss the tooltip another_tooltip = False for b in self.element.hovered_buttons: if b is self: continue if b.tooltip_on_hover: another_tooltip = True if not another_tooltip: self.element.ui.tooltip.visible = False def click(self): self.log_event("clicked") self.set_state("clicked") def unclick(self): self.log_event("unclicked") if self in self.element.hovered_buttons: self.hover() else: self.unhover() def get_tooltip_text(self): "override in a subclass to define this button's tooltip text" return "ERROR" def get_tooltip_location(self): "override in a subclass to define this button's tooltip screen location" return 10, 10 def draw_caption(self): y = self.y + self.caption_y text = self.caption # trim if too long text = text[: self.width] if self.caption_justify == TEXT_CENTER: text = text.center(self.width) elif self.caption_justify == TEXT_RIGHT: text = text.rjust(self.width) # just bail if we're trying to draw something out of bounds if self.x + len(text) > self.element.art.width: return if self.clear_before_caption_draw: for ty in range(self.height): for tx in range(self.width): self.element.art.set_char_index_at(0, 0, self.x + tx, y + ty, 0) # leave FG color None; should already have been set self.element.art.write_string(0, 0, self.x, y, text, None) def draw(self): if self.never_draw: return self.set_state_colors() if self.should_draw_caption: self.draw_caption()