OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / ui_framework / form_ui / elements / scroll_box.py

434 lines · python ref:
import bpy
from ....utility.screen import dpi_factor
from ... graphics.draw import render_text, draw_border_lines, render_quad
from ... utils.geo import get_blf_text_dims
from ... utils.checks import is_mouse_in_quad
from . import Dims, Tips


class Row:
    def __init__(self):
        self.elements = []
        self.max_height = 0
    

    def add_element(self, element):
        self.elements.append(element)


class Scroll_Group:
    def __init__(self):
        self.rows = []


    def clear(self, context):
        for row in self.rows:
            for element in row.elements:
                element.shut_down(context)
        self.rows = []


    def row(self):
        return Row()


    def row_insert(self, row):
        self.rows.append(row)


class Scroll_Box:
    def __init__(self, width=0, height=0, scroll_group=None, view_scroll_enabled=False):
        '''20px is reserved for the scroll bar'''

        self.dims = Dims()
        self.width = width * dpi_factor(min=0.5)
        self.__height = height * dpi_factor(min=0.5)
        self.scroll_group = scroll_group if scroll_group else Scroll_Group()
        self.view_scroll_enabled = view_scroll_enabled
        self.bar = Bar(height=height)
        self.bg_color = (0,0,0,1)
        self.border_color = (0,0,0,1)
        # State
        self.mouse_over = False
        self.locked = False
        # Slice
        self.pages = []
        self.page_index = 0

        # Preserve
        self.prev_pages = None

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, val):
        self.__height = val * dpi_factor(min=0.5)


    def build(self, db, x_offset, h_offset):
        self.bg_color = db.color.cell_background
        self.border_color = db.color.border

        self.bar.height = self.__height

        # Dims
        self.dims.max_height = self.__height
        self.dims.max_width = self.width

        bot_x = x_offset
        bot_y = h_offset
        w = self.dims.max_width
        h = self.dims.max_height
        self.dims.bot_left  = (bot_x    , bot_y)
        self.dims.top_left  = (bot_x    , bot_y + h)
        self.dims.top_right = (bot_x + w, bot_y + h)
        self.dims.bot_right = (bot_x + w, h_offset)

        # Rows
        bot_x = self.dims.bot_left[0]
        bot_y = self.dims.bot_left[1]
        for index, row in enumerate(reversed(self.scroll_group.rows)):
            height = 0
            width = 0
            for element in row.elements:
                element.build(db, bot_x + width, bot_y)
                height = element.dims.max_height if element.dims.max_height > height else height
                width += element.dims.max_width
            row.max_height = height
            bot_y += height

        self.__build_pages()

        if self.pages != self.prev_pages or self.prev_pages == None:
            self.prev_pages = self.pages
            self.page_index = 0

        self.__build_rows(db)

        # Bar
        self.bar.build(db, self.dims, self.page_index, self.pages)


    def update(self, context, event, db):
        self.mouse_over = is_mouse_in_quad(self.dims.quad(), db.mouse_pos, tolerance=-1)

        self.bar.update(context, event, db)
        if self.bar.handle.locked:
            self.locked = True
            db.locked_element = self
            return
        
        can_scroll = self.bar.mouse_over
        if self.view_scroll_enabled:
            if self.mouse_over and db.clicked == False:
                can_scroll = True

        if can_scroll:
            # Scroll
            if db.increment > 0:
                self.__scroll(db, direction=1)
            elif db.increment < 0:
                self.__scroll(db, direction=-1)

            # Jump To
            if self.bar.handle.mouse_over == False and db.clicked:
                if len(self.pages) > 1:
                    self.page_index = self.bar.handle.jump_to(db.mouse_pos[1], self.pages, self.bar.dims)
                    self.__build_rows(db)

        if len(self.pages) == 0: return
        start, end = self.pages[self.page_index]
        for index, row in enumerate(reversed(self.scroll_group.rows)):
            if index < start: continue
            if index > end: break

            for element in row.elements:
                element.update(context, event, db)


    def locked_update(self, context, event, db):

        self.locked = True
        if event.value == 'RELEASE':
            self.locked = False
            self.bar.handle.locked = False
            db.locked_element = None
            return

        self.page_index = self.bar.handle.locked_update(context, event, db, self.page_index, self.pages, self.bar.dims)
        self.__build_rows(db)


    def __build_pages(self):
        self.pages = []
        
        if len(self.scroll_group.rows) == 0: return

        def fit_after_index(start=0):
            height = 0
            for index, row in enumerate(reversed(self.scroll_group.rows)):
                if index < start: continue
                if row.max_height + height >= self.__height:
                    if round(row.max_height + height, 4) > round(self.__height, 4):
                        return index - 1
                    return index
                height += row.max_height
            return None

        for index, row in enumerate(reversed(self.scroll_group.rows)):
            end = fit_after_index(index)
            if end != None:
                self.pages.append((index, end))
            else:
                if index == 0:
                    self.pages.append((index, len(self.scroll_group.rows)))
                    return


    def __scroll(self, db, direction=0):
        if len(self.pages) == 0: return

        self.page_index += direction
        if self.page_index > len(self.pages) - 1:
            self.page_index = len(self.pages) - 1
        if self.page_index < 0:
            self.page_index = 0
        
        self.bar.handle.set_location(self.page_index, self.pages, self.bar.dims)
        self.__build_rows(db)


    def __build_rows(self, db):

        bot_x = self.dims.bot_left[0]
        bot_y = self.dims.bot_left[1]

        if len(self.pages) == 0: return
        start, end = self.pages[self.page_index]
        for index, row in enumerate(reversed(self.scroll_group.rows)):
            if index < start: continue
            if index > end: break

            height = 0
            width = 0
            for element in row.elements:
                element.build(db, bot_x + width, bot_y)
                height = element.dims.max_height if element.dims.max_height > height else height
                width += element.dims.max_width
            bot_y += height


    def draw(self, db):

        if len(self.pages) == 0:
            if self.dims.max_height == 0:
                return

        render_quad(self.dims.quad(), color=self.bg_color)
        draw_border_lines(self.dims.quad(), color=self.border_color, width=2)

        self.bar.draw(db, self.pages)

        if len(self.pages) == 0: return
        start, end = self.pages[self.page_index]
        for index, row in enumerate(reversed(self.scroll_group.rows)):
            if index < start: continue
            if index > end: break

            for element in row.elements:
                element.draw(db)


    def draw_tips(self, db):
        if len(self.pages) == 0: return
        start, end = self.pages[self.page_index]
        for index, row in enumerate(reversed(self.scroll_group.rows)):
            if index < start: continue
            if index > end: break

            for element in row.elements:
                element.draw_tips(db)


    def shut_down(self, context):
        for index, row in enumerate(reversed(self.scroll_group.rows)):
            for element in row.elements:
                element.shut_down(context)


class Bar:
    def __init__(self, height):
        self.dims = Dims()
        self.width = 20 * dpi_factor(min=0.5)
        self.height = height * dpi_factor(min=0.5)
        self.bg_color = (0,0,0,1)
        self.border_color = (0,0,0,1)
        self.mouse_over = False

        self.handle = Handle()


    def build(self, db, dims, page_index, pages):
        self.bg_color = db.color.cell_background
        self.border_color = db.color.border

        self.dims.max_width = self.width
        self.dims.max_height = self.height

        bot_x = dims.bot_right[0]
        bot_x -= self.width
        bot_y = dims.bot_right[1]
        w = self.dims.max_width
        h = self.dims.max_height

        self.dims.bot_left  = (bot_x    , bot_y)
        self.dims.top_left  = (bot_x    , bot_y + h)
        self.dims.top_right = (bot_x + w, bot_y + h)
        self.dims.bot_right = (bot_x + w, bot_y)

        self.handle.build(db, dims, page_index, pages)


    def update(self, context, event, db):
        self.mouse_over = is_mouse_in_quad(self.dims.quad(), db.mouse_pos, tolerance=-1)
        self.handle.update(context, event, db)


    def draw(self, db, pages):
        render_quad(self.dims.quad(), color=self.bg_color)
        draw_border_lines(self.dims.quad(), color=self.border_color)

        if len(pages) > 1:
            self.handle.draw(db)


class Handle:
    def __init__(self):
        self.dims = Dims()
        self.width = 20 * dpi_factor(min=0.5)
        self.max_h = 10 * dpi_factor(min=0.5)
        self.bg_color = (0,0,0,1)
        self.border_color = (0,0,0,1)
        self.mouse_over = False
        # Locked
        self.locked = False
        self.start_y = 0
        self.diff = 0


    def build(self, db, dims, page_index, pages):
        # Color
        self.bg_color = db.color.cell_background
        self.border_color = db.color.border
        
        # Build Dims
        self.dims.max_width = self.width
        self.set_location(page_index, pages, dims)


    def update(self, context, event, db):
        self.mouse_over = is_mouse_in_quad(self.dims.quad(), db.mouse_pos, tolerance=-1)

        if self.mouse_over: self.bg_color = db.color.mouse_over
        else: self.bg_color = db.color.cell_background

        if self.mouse_over and db.clicked:
            self.locked = True
            self.start_y = db.mouse_pos[1]
            self.diff = self.start_y - self.dims.bot_left[1]


    def locked_update(self, context, event, db, page_index, pages, dims):
        # Mouse
        mouse_y = db.mouse_pos[1]
        if mouse_y > dims.top_left[1]:
            mouse_y = dims.top_left[1]
        elif mouse_y < dims.bot_left[1]:
            mouse_y = dims.bot_left[1]

        # Row Height
        rows = len(pages) if len(pages) > 0 else 1
        row_h = self.__row_height(rows, dims)

        # Index
        normalize = mouse_y - dims.bot_left[1]
        page_index = int(normalize / row_h)
        page_index = self.__clamp_page_index(page_index, pages)

        self.set_location(page_index, pages, dims)
        return page_index


    def jump_to(self, y, pages, dims):
        rows = len(pages) if len(pages) > 0 else 1
        row_h = self.__row_height(rows, dims)

        if y > dims.top_left[1]:
            y = dims.top_left[1]
        elif y < dims.bot_left[1]:
            y = dims.bot_left[1]

        normalize = abs(y - dims.bot_left[1])
        page_index = int(normalize / row_h)
        page_index = self.__clamp_page_index(page_index, pages)
        self.set_location(page_index, pages, dims)
        return page_index


    def set_location(self, page_index, pages, dims):
        bot_x = dims.bot_right[0]
        bot_x -= self.width
        bot_y = dims.bot_right[1]
        w = self.dims.max_width
        rows = len(pages) if len(pages) > 0 else 1

        row_h = self.__row_height(rows, dims)
        bot_y += row_h * page_index

        # Clamp : Size
        top_y = dims.top_left[1]
        if row_h < self.max_h:
            row_h = self.max_h
        
        # Clamp : Loc Top
        if bot_y + row_h > top_y:
            bot_y = top_y - row_h

        # Clamp : Loc Bottom
        if bot_y < dims.bot_right[1]:
            bot_y = dims.bot_right[1]


        self.dims.bot_left  = (bot_x    , bot_y)
        self.dims.top_left  = (bot_x    , bot_y + row_h)
        self.dims.top_right = (bot_x + w, bot_y + row_h)
        self.dims.bot_right = (bot_x + w, bot_y)     


    def __row_height(self, rows, dims):
        row_h = dims.max_height / rows
        if row_h < self.max_h:
            over = self.max_h - row_h
            row_h = (dims.max_height - over) / rows

        return row_h


    def __clamp_page_index(self, page_index, pages):
        if page_index > len(pages) - 1:
            page_index = len(pages) - 1
        if page_index < 0:
            page_index = 0
        return page_index


    def draw(self, db):
        render_quad(self.dims.quad(), color=self.bg_color)
        draw_border_lines(self.dims.quad(), color=self.border_color)