    def _test_size(self, s):
        """Tests if a certain font size is acceptable."""
        return self.base_font[s].get_linesize() <= self.abs_size[1]

    def _recalculate_text_size(self):
        """Find the largest font size that is at most max_size and makes the text fit
        within boundaries. abs_size must be up-to-date"""
        left = 0
        right = min(len(self.base_font), self.max_font_size)
        result = 0

        # Use binary search, but test the max_font_size first
        if self._test_size(right):
            result = right
        else:
            while left + 1 < right:
                s = (left + right) // 2
                if self._test_size(s):
                    left = s
                else:
                    right = s
            result = left

        if result != self._current_size:
            self.needs_redraw = True
            self._current_size = result



    def rebuild(self):
        super().rebuild()
        if not self.invalid_abs_size:
            _recalculate_text_size()

    def _recalculate_abs_size(self):
        super()._recalculate_abs_size()
        self._recalculate_text_size()

    def prepare_for_redraw(self):
        if self.needs_redraw and self.background == graphics.colors["clear"]:
            self.parent.needs_redraw = True
        super().prepare_for_redraw()


class TextBox(widget.BorderedWidget):
    """A textbox is equivalent to Singularity's Text. A box. With text in it."""
    def __init__(self, parent, pos, size, text, **kwargs):
        super().__init__(parent, pos, size, **kwargs)
        label_pos = kwargs.get("label_pos", (.075,.075))
        label_size = kwargs.get("label_size", (.85,.85))
        kwargs.setdefault("background", self.background)

        self.label = Label(self, label_pos, label_size, text, **kwargs)

    def mouse_move(self, mx, my, dx, dy, lb):
        """Override mouse_move so it doesn't check further down."""
        pass

    def set_background(self, background):
        if background == self.background:
            return
        self._background = background
        self.needs_redraw = True
        self.label.background = background

    background = property(lambda self: self._background, set_background)

def shift_pressed():
    return (pygame.key.get_mods() & pygame.KMOD_SHIFT) or False

class EditableLabel(FastLabel):
    """An EditableLabel can be written by the user at runtime. It will present a caret.
    Since this is intended as a user input field rather than an actual text editor, it
    only implements the basic functionality of a FastLabel."""

    BLINK_INTERVAL = 600 #ms

    def __init__(self, parent, pos, size, text, **kwargs):
        super().__init__(parent, pos, size, text, **kwargs)

        self.caret_timer = 0
        self.caret_visible = False
        self._caret_pos = 0
        self._selection = False
        self.select_start = 0
        self.select_end = 0
        self.click_pos = 0
        self.text_offset = 0
        self._select_color = kwargs.get("select_color", self.background)
        if self._select_color == graphics.colors["clear"]:
            self._select_color = graphics.colors["black"]
        self._default_align = self.align

    select_color = util.causes_redraw("_select_color")
    focusable = property(lambda self: True)
    default_align = util.causes_redraw("_default_align")
    selection = util.causes_redraw("_selection")

    def set_text(self, text):
        if text == self.text:
            return
        self._text = text
        self.needs_redraw = True
        self._recalculate_text_offset()

    text = property(lambda self: self._text, set_text)

    def set_caret_pos(self, caret_pos):
        if caret_pos == self.caret_pos:
            return
        self._caret_pos = caret_pos
        self.needs_redraw = True
        self.caret_visible = True
        self.caret_timer = 0
        self._recalculate_text_offset()

    caret_pos = property(lambda self: self._caret_pos, set_caret_pos)

    def _recalculate_abs_size(self):
        super()._recalculate_abs_size()
        self._recalculate_text_offset()

    def focus_gained(self):
        super().focus_gained()
        self.caret_timer = 0
        self.caret_visible = True
        self.needs_redraw = True

    def focus_lost(self):
        super().focus_lost()
        self.caret_visible = False
        self.needs_redraw = True
        self.selection = False

    def key_press(self, key):
        if not self.in_focus:
            return False

        if key == pygame.K_BACKSPACE:
            if not self.selection:
                if self.caret_pos != 0 and len(self.text) != 0:
                    self._caret_pos -= 1
                    self.text = self.text[:self.caret_pos] + self.text[self.caret_pos+1:]
                # else:
                    # TODO:Error sounds?
            else:
                self.selection = False
                self._caret_pos = self.select_start
                self.text = self.text[:self.select_start] + self.text[self.select_end:]
        elif key == pygame.K_DELETE:
            if not self.selection:
                if self.caret_pos != len(self.text) and len(self.text) != 0:
                    self.text = self.text[:self.caret_pos] + self.text[self.caret_pos+1:]
            else:
                self.selection = False
                self._caret_pos = self.select_start
                self.text = self.text[:self.select_start] + self.text[self.select_end:]
        elif key == pygame.K_LEFT:
            if self.selection:
                if shift_pressed():
                    assert self.caret_pos in (self.select_start, self.select_end)
                    if self.caret_pos == self.select_start:
                        self.select_start = max(self.select_start - 1, 0)
                    else:
                        self.select_end -= 1
                    if self.select_start == self.select_end:
                        self.selection = False
                    self.caret_pos = max(self.caret_pos - 1, 0)
                else:
                    self.caret_pos = self.select_start
                    self.selection = False
            else:
                self.caret_pos = max(self.caret_pos - 1, 0)
                if shift_pressed():
                    self.selection = True
                    self.select_start = self.caret_pos
                    self.select_end = self.caret_pos + 1
        elif key == pygame.K_RIGHT:
            if self.selection:
                if shift_pressed():
                    assert self.caret_pos in (self.select_start, self.select_end)
                    if self.caret_pos == self.select_start:
                        self.select_start += 1
                    else:
                        self.select_end = min(self.select_end + 1, len(self.text))
                    if self.select_start == self.select_end:
                        self.selection = False
                    self.caret_pos = min(self.caret_pos + 1, len(self.text))
                else:
                    self.caret_pos = self.select_end
                    self.selection = False
            else:
                self.caret_pos = min(self.caret_pos + 1, len(self.text))
                if shift_pressed():
                    self.selection = True
                    self.select_start = self.caret_pos - 1
                    self.select_end = self.caret_pos
        elif key == pygame.K_UP or key == pygame.K_END:
            self.caret_pos = len(self.text)
            self.selection = False
        elif key == pygame.K_DOWN or key == pygame.K_HOME:
            self.caret_pos = 0
            self.selection = False
        elif type(key) == str and key.isprintable():
            assert len(key) == 1
            if not self.selection:
                self._caret_pos += 1
                self.text = self.text[:self.caret_pos-1] + key + self.text[self.caret_pos-1:]
            else:
                self.selection = False
                self._caret_pos = self.select_start + 1
                self.text = self.text[:self.select_start] + key + self.text[self.select_end:]
        else:
            return False

        return True

    def timer_tick(self, dt):
        if not self.in_focus:
            return

        self.caret_timer += dt
        if self.caret_timer >= EditableLabel.BLINK_INTERVAL:
            self.caret_timer = 0
            self.caret_visible = not self.caret_visible
            self.needs_redraw = True

    def _recalculate_text_offset(self):
        """Always attempt to keep the caret within the bounds of the label. Depending on text alignment,
        This will require offsetting the text or temporarily changing text alignment."""
        if self.invalid_abs_size:
            return
        font = self.base_font[self._current_size]
        total_size = font.size(self.text)[0]                    # total size of the text

        if total_size <= self.abs_size[0]:
            self.align = self.default_align
            self.text_offset = 0
            return

        text_start = self._calc_text_location(total_size)       # relative x of text start, with current align and offset
        text_end = text_start + total_size                      # relative x of text end, as above
        caret_offset = font.size(self.text[:self.caret_pos])[0] # size of text before caret
        if self.default_align in (graphics.LEFT, graphics.RIGHT):
            # The opposite alignment RIGHT <-> LEFT
            opposite = graphics.RIGHT if self.default_align == graphics.LEFT else graphics.LEFT
            # The opposite end of the text (i.e. the right one if alignment is left)
            # With the current alignment, which may be different from default
            opposite_end = text_end if self.default_align == graphics.LEFT else text_start
            # Caret location
            caret_loc = (0 if self.default_align == graphics.LEFT else (self.abs_size[0] - total_size)) \
                    + self.text_offset + caret_offset
            if 0 <= opposite_end <= self.abs_size[0]:
                self.align = opposite
                self.text_offset = 0
            else:
                self.align = self.default_align
                if caret_loc < 0:
                    self.text_offset = -caret_offset
                elif caret_loc > self.abs_size[0]:
                    self.text_offset = self.abs_size[0] - caret_offset
        else:
            # When the text is centered, attempt to center around the caret
            # Does one of the ends fit on screen together with the caret?
            min_dist = min(caret_offset, total_size - caret_offset)
            if min_dist <= self.abs_size[0]:
                if caret_offset == min_dist:
                    self.align = graphics.LEFT
                    self.text_offset = 0
                else:
                    self.align = graphics.RIGHT
                    self.text_offset = 0
            else:
                self.align = graphics.CENTER
                self.text_offset = total_size // 2 - caret_offset


    def _calc_text_location(self, text_width = None):
        """Calculate the local x position where the text will start. If the text is wider than
        the label, this will be negative."""
        width = text_width or self.base_font[self._current_size].size(self.text)[0]
        x = self.text_offset
        excess_space = self.abs_size[0] - width
        if self.align == graphics.CENTER:
            x += excess_space // 2
        elif self.align == graphics.RIGHT:
            x += excess_space
        return x

    def _calc_character_index(self, x):
        """Calculate which character of the text is under x, where x is in global coordinates"""
        font = self.base_font[self._current_size]
        widths = [m[4] for m in font.metrics(self.text)]
        local_x = x - self.abs_pos[0] - self._calc_text_location()
        s = 0
        s2 = 0
        for i, w in enumerate(widths):
            s += w // 2
            if s > local_x:
                return i
            s2 += w
            s = s2
        return len(widths)

    def mouse_button_press(self, mx, my, button):
        if button != 1:
            return

        self.selection = False
        self.click_pos = self._calc_character_index(mx)
        self.caret_pos = self.click_pos

    def mouse_move(self, mx, my, dx, dy, lb):
        if not lb:
            return

        index = self._calc_character_index(mx)
        if index != self.caret_pos:
            self.select_start = min(self.click_pos, index)
            self.select_end = max(self.click_pos, index)
            self.selection = self.select_start != self.select_end
            self.caret_pos = index

    def redraw_self(self):
        bg = self.background
        if self.background != graphics.colors["clear"]:
            self.surface.fill(self.background)
            bg = None

        styles = []
        if self.selection:
            styles.append((self.text_color, bg, False, self.select_start))
            styles.append((self.select_color, self.text_color, False, self.select_end))
        styles.append((self.text_color, bg, False, len(self.text)))

        font = self.base_font[self._current_size]
        render_string(self.surface, self.text, (self.text_offset, 0), self.abs_size,
                font, styles, self.align, self.valign, False)

        if self.caret_visible:
            caret_color = self.text_color
            if self.selection and self.select_start <= self.caret_pos < self.select_end:
                caret_color = self.select_color
            caret_x = self._calc_text_location() + font.size(self.text[:self.caret_pos])[0] - 1
            if caret_x <= 0:
                caret_x = 1
            self.surface.fill(caret_color, (caret_x, 0, 1, font.get_linesize())) # TODO:Wider caret on high resolutions?


class InputBox(widget.BorderedWidget):
    """An InputBox allows the user to input one line of text. It does not scroll."""

    def __init__(self, parent, pos, size, text, **kwargs):
        kwargs.setdefault("align", graphics.LEFT)
        kwargs.setdefault("borders", graphics.ALL)
        super().__init__(parent, pos, size, **kwargs)

        label_pos = kwargs.get("label_pos", (.015, .075))
        label_size = kwargs.get("label_size", (.97,.85))
        self._selected_border_color = kwargs.get("selected_border_color", graphics.colors["white"])
        self._regular_border_color = self.border_color

        kwargs.setdefault("background", self.background)
        self.label = EditableLabel(self, label_pos, label_size, text, **kwargs)

    focusable = property(lambda self: True)
    selected_border_color = util.causes_redraw("_selected_border_color")
    regular_border_color = util.causes_redraw("_regular_border_color")

    def set_background(self, background):
        if background == self.background:
            return
        self._background = background
        self.needs_redraw = True
        self.label.background = background

    background = property(lambda self: self._background, set_background)

    def focus_gained(self):
        super().focus_gained()
        self.label.focus_gained()
        self.border_color = self.selected_border_color

    def focus_lost(self):
        super().focus_lost()
        self.label.focus_lost()
        self.border_color = self.regular_border_color

    def mouse_enter(self, mx, my, lb):
        self.parent.child_requests_focus(self)

    def mouse_button_press(self, mx, my, button):
        self.label.mouse_button_press(mx, my, button)

    def mouse_button_release(self, mx, my, button):
        self.label.mouse_button_release(mx, my, button)

    def key_press(self, key):
        return self.label.key_press(key)
