''' Copyright (C) 2023 CG Cookie http://cgcookie.com hello@cgcookie.com Created by Jonathan Denning, Jonathan Williamson This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ''' import os import re import sys import math import time import random import traceback import contextlib from math import floor, ceil from inspect import signature from itertools import dropwhile import bpy import blf import gpu from gpu_extras.presets import draw_texture_2d from mathutils import Vector, Matrix from . import gpustate from . import ui_settings from .gpustate import ScissorStack from .ui_linefitter import LineFitter from .ui_core import UI_Element from .ui_core_preventmulticalls import UI_Core_PreventMultiCalls from .blender import tag_redraw_all from .ui_styling import UI_Styling, ui_defaultstylings from .ui_core_utilities import helper_wraptext, convert_token_to_cursor from .fsm import FSM from .useractions import ActionHandler from .boundvar import BoundVar from .blender import get_view3d_area, get_view3d_region from .debug import debugger, dprint, tprint from .decorators import debug_test_call, blender_version_wrapper, add_cache from .globals import Globals from .hasher import Hasher from .maths import Vec2D, Color, mid, Box2D, Size1D, Size2D, Point2D, RelPoint2D, Index2D, clamp, NumberUnit from .profiler import profiler, time_it from .utils import iter_head from ..ext import png from ..ext.apng import APNG class UI_Document: default_keymap = { 'commit': {'RET',}, 'cancel': {'ESC',}, 'keypress': {c for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'} | {'NUMPAD_%d'%i for i in range(10)} | {'NUMPAD_PERIOD','NUMPAD_MINUS','NUMPAD_PLUS','NUMPAD_SLASH','NUMPAD_ASTERIX'} | {'ZERO', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE'} | {'PERIOD', 'MINUS', 'SPACE', 'SEMI_COLON', 'COMMA', 'QUOTE', 'ACCENT_GRAVE', 'PLUS', 'SLASH', 'BACK_SLASH', 'EQUAL', 'LEFT_BRACKET', 'RIGHT_BRACKET'}, 'scroll top': {'HOME'}, 'scroll bottom': {'END'}, 'scroll up': {'WHEELUPMOUSE', 'PAGE_UP', 'UP_ARROW', }, 'scroll down': {'WHEELDOWNMOUSE', 'PAGE_DOWN', 'DOWN_ARROW', }, 'scroll': {'TRACKPADPAN'}, } doubleclick_time = bpy.context.preferences.inputs.mouse_double_click_time / 1000 # 0.25 wheel_scroll_lines = 3 # bpy.context.preferences.inputs.wheel_scroll_lines, see https://developer.blender.org/rBbec583951d736776d2096368ef8d2b764287ac11 allow_disabled_to_blur = False show_tooltips = True tooltip_delay = 0.50 max_click_dist = 10 # allows mouse to travel off element and still register a click event allow_click_time = 0.50 # allows for very fast clicking. ignore max_click_dist if time(mouseup-mousedown) is at most allow_click_time def __init__(self): self._context = None self._area = None self._exception_callbacks = [] self._ui_scale = Globals.drawing.get_dpi_mult() self._draw_count = 0 self._draw_time = 0 self._draw_fps = 0 def add_exception_callback(self, fn): self._exception_callbacks += [fn] def _callback_exception_callbacks(self, e): for fn in self._exception_callbacks: try: fn(e) except Exception as e2: print(f'UI_Document: Caught exception while calling back exception callbacks: {fn.__name__}') print(f' original: {e}') print(f' additional: {e2}') debugger.print_exception() # @profiler.function def init(self, context, **kwargs): self._callbacks = { 'preclean': set(), 'postclean': set(), 'postflow': set(), 'postflow once': set(), } self.defer_cleaning = False self._context = context self._area = get_view3d_area(context) self.actions = ActionHandler(context, UI_Document.default_keymap) self._body = UI_Element(tagName='body', document=self) # root level element self._tooltip = UI_Element(tagName='dialog', classes='tooltip', can_hover=False, parent=self._body) self._tooltip.is_visible = False self._tooltip_message = None self._tooltip_wait = None self._tooltip_mouse = None self._reposition_tooltip_before_draw = False self.fsm = FSM(self, start='main') self.ignore_hover_change = False self._sticky_dist = 20 self._sticky_element = None # allows the mouse to drift a few pixels off before handling mouseleave self._under_mouse = None self._under_mousedown = None self._under_down = None self._focus = None self._focus_full = False self._last_mx = -1 self._last_my = -1 self._last_mouse = None self._last_under_mouse = None self._last_under_click = None self._last_click_time = 0 self._last_sz = None self._last_w = -1 self._last_h = -1 def update_callbacks(self, ui_element, force_remove=False): for cb,fn in [('preclean', ui_element.preclean), ('postclean', ui_element.postclean), ('postflow', ui_element.postflow)]: if force_remove or not fn: self._callbacks[cb].discard(ui_element) else: self._callbacks[cb].add(ui_element) @property def body(self): return self._body @property def activeElement(self): return self._focus def center_on_mouse(self, element): # centers element under mouse, must be done between first and second layout calls if element is None: return def center(): element._relative_pos = None mx, my = self.actions.mouse if self.actions.mouse else (10, 10) # w,h = element.width_pixels,element.height_pixels w, h = element.width_pixels, element._dynamic_full_size.height l = mx-w/2 t = -self._body.height_pixels + my + h/2 element.reposition(left=l, top=t) self._callbacks['postflow once'].add(center) def _reposition_tooltip(self, force=False): if self._tooltip_mouse == self.actions.mouse and not force: return self._tooltip_mouse = self.actions.mouse if self._tooltip.width_pixels is None or type(self._tooltip.width_pixels) is str or self._tooltip._mbp_width is None or self._tooltip.height_pixels is None or type(self._tooltip.height_pixels) is str or self._tooltip._mbp_height is None: ttl,ttt = self.actions.mouse else: ttl = self.actions.mouse.x if self.actions.mouse.x < self._body.width_pixels/2 else self.actions.mouse.x - (self._tooltip.width_pixels + (self._tooltip._mbp_width or 0)) ttt = self.actions.mouse.y if self.actions.mouse.y > self._body.height_pixels/2 else self.actions.mouse.y + (self._tooltip.height_pixels + (self._tooltip._mbp_height or 0)) hp = self._body.height_pixels if type(self._body.height_pixels) is not str else 0.0 self._tooltip.reposition(left=ttl, top=ttt - hp) def removed_element(self, ui_element): if self._under_mouse and self._under_mouse.is_descendant_of(ui_element): self._under_mouse = None if self._under_mousedown and self._under_mousedown.is_descendant_of(ui_element): self._under_mousedown = None if self._focus and self._focus.is_descendant_of(ui_element): self._focus = None def force_dirty_all(self): self._body.dirty(children=True) self._body.dirty_styling() self._body.dirty_flow() tag_redraw_all('Force Dirty All') # @profiler.function def update(self, context, event): self._context = context self._area = get_view3d_area(context) # if context.area != self._area: return # self._ui_scale = Globals.drawing.get_dpi_mult() UI_Core_PreventMultiCalls.reset_multicalls() region = get_view3d_region(context) w,h = region.width, region.height if self._last_w != w or self._last_h != h: # print('Document:', (self._last_w, self._last_h), (w,h)) self._last_w,self._last_h = w,h self._body.dirty(cause='changed document size', children=True) self._body.dirty_flow() tag_redraw_all("UI_Element update: w,h change") if ui_settings.DEBUG_COLOR_CLEAN: tag_redraw_all("UI_Element DEBUG_COLOR_CLEAN") #self.actions.update(context, event, self._timer, print_actions=False) # self.actions.update(context, event, print_actions=False) if self._sticky_element and not self._sticky_element.is_visible: self._sticky_element = None self._mx,self._my = self.actions.mouse if self.actions.mouse else (-1,-1) if not self.ignore_hover_change: self._under_mouse = self._body.get_under_mouse(self.actions.mouse) if self._sticky_element: if self._sticky_element.get_mouse_distance(self.actions.mouse) < self._sticky_dist * self._ui_scale: if self._under_mouse is None or not self._under_mouse.is_descendant_of(self._sticky_element): self._under_mouse = self._sticky_element next_message = None if self._under_mouse and self._under_mouse.title_with_for(): # and not self._under_mouse.disabled: next_message = self._under_mouse.title_with_for() if self._under_mouse.disabled: next_message = f'(Disabled) {next_message}' if self._tooltip_message != next_message: self._tooltip_message = next_message self._tooltip_mouse = None self._tooltip_wait = time.time() + self.tooltip_delay self._tooltip.is_visible = False if self._tooltip_message and time.time() > self._tooltip_wait: if self._tooltip_mouse != self.actions.mouse or self._tooltip.innerText != self._tooltip_message or not self._tooltip.is_visible: # TODO: markdown support?? self._tooltip.innerText = self._tooltip_message self._tooltip.is_visible = True and self.show_tooltips self._reposition_tooltip_before_draw = True tag_redraw_all("reposition tooltip") self.fsm.update() self._last_mx = self._mx self._last_my = self._my self._last_mouse = self.actions.mouse if not self.ignore_hover_change: self._last_under_mouse = self._under_mouse uictrld = False uictrld |= self._under_mouse is not None and self._under_mouse != self._body uictrld |= self.fsm.state != 'main' uictrld |= self._focus_full # uictrld |= self._focus is not None return {'hover'} if uictrld else None def _addrem_pseudoclass(self, pseudoclass, remove_from=None, add_to=None): rem = remove_from.get_pathToRoot() if remove_from else [] add = add_to.get_pathToRoot() if add_to else [] rem.reverse() add.reverse() roots = [] if rem: roots.append(rem[0]) if add: roots.append(add[0]) while rem and add and rem[0] == add[0]: rem = rem[1:] add = add[1:] # print(f'addrem_pseudoclass: {pseudoclass} {rem} {add}') self.defer_cleaning = True for root in roots: root.defer_dirty_propagation = True for e in rem: e.del_pseudoclass(pseudoclass) for e in add: e.add_pseudoclass(pseudoclass) for root in roots: root.defer_dirty_propagation = False self.defer_cleaning = False def debug_print(self): print('') print('UI_Document.debug_print') self._body.debug_print(0, set()) def debug_print_toroot(self, fromHovered=True, fromFocused=False): print('') print('UI_Document.debug_print_toroot') if fromHovered: self._debug_print(self._under_mouse) if fromFocused: self._debug_print(self._focus) def _debug_print(self, ui_from): # debug print! path = ui_from.get_pathToRoot() for i,ui_elem in enumerate(reversed(path)): def tprint(*args, extra=0, **kwargs): print(' '*(i+extra), end='') print(*args, **kwargs) tprint(str(ui_elem)) tprint(f'selector={ui_elem._selector}', extra=1) tprint(f'l={ui_elem._l} t={ui_elem._t} w={ui_elem._w} h={ui_elem._h}', extra=1) @property def sticky_element(self): return self._sticky_element @sticky_element.setter def sticky_element(self, element): self._sticky_element = element def clear_last_under(self): self._last_under_mouse = None def handle_hover(self, change_cursor=True): # handle :hover, on_mouseenter, on_mouseleave if self.ignore_hover_change: return if change_cursor and self._under_mouse and self._under_mouse._tagName != 'body': cursor = self._under_mouse._computed_styles.get('cursor', 'default') Globals.cursors.set(convert_token_to_cursor(cursor)) if self._under_mouse == self._last_under_mouse: return if self._under_mouse and not self._under_mouse.can_hover: return self._addrem_pseudoclass('hover', remove_from=self._last_under_mouse, add_to=self._under_mouse) if self._last_under_mouse: self._last_under_mouse.dispatch_event('on_mouseleave') if self._under_mouse: self._under_mouse.dispatch_event('on_mouseenter') def handle_mousemove(self, ui_element=None): ui_element = ui_element or self._under_mouse if ui_element is None: return if self._last_mouse == self.actions.mouse: return ui_element.dispatch_event('on_mousemove') def handle_keypress(self, ui_element=None): ui_element = ui_element or self._focus if self.actions.pressed('clipboard paste') and ui_element: ui_element.dispatch_event('on_paste', clipboardData=bpy.context.window_manager.clipboard) pressed = self.actions.as_char(self.actions.just_pressed) if pressed and ui_element: ui_element.dispatch_event('on_keypress', key=pressed) @FSM.on_state('main', 'enter') def modal_main_enter(self): Globals.cursors.set('DEFAULT') @FSM.on_state('main') def modal_main(self): # print('UI_Document.main', self.actions.event_type, time.time()) if self.actions.just_pressed: pressed = self.actions.just_pressed if pressed not in {'WINDOW_DEACTIVATE'}: if self._focus and self._focus_full: self._focus.dispatch_event('on_keypress', key=pressed) elif self._under_mouse: self._under_mouse.dispatch_event('on_keypress', key=pressed) self.handle_hover() self.handle_mousemove() if self.actions.pressed('MIDDEMOUSE'): return 'scroll' if self.actions.pressed('LEFTMOUSE', unpress=False, ignoremods=True, ignoremulti=True): if self._under_mouse == self._body: # clicking body always blurs focus self.blur() elif UI_Document.allow_disabled_to_blur and self._under_mouse and self._under_mouse.is_disabled: # user clicked on disabled element, so blur current focused element self.blur() return 'mousedown' if self.actions.pressed('SHIFT+F10'): profiler.clear() return if self.actions.pressed('SHIFT+F11'): profiler.printout() self.debug_print() return if self.actions.pressed('CTRL+SHIFT+F11'): self.debug_print_toroot() print(f'{self._under_mouse._computed_styles}') return # if self.actions.pressed('RIGHTMOUSE') and self._under_mouse: # self._debug_print(self._under_mouse) # #print('focus:', self._focus) if self.actions.pressed({'scroll top', 'scroll bottom'}, unpress=False): move = 100000 * (-1 if self.actions.pressed({'scroll top'}) else 1) self.actions.unpress() if self._get_scrollable(): self._scroll_element.scrollTop = self._scroll_last.y + move self._scroll_element._setup_ltwh(recurse_children=False) if self.actions.pressed({'scroll', 'scroll up', 'scroll down'}, unpress=False): if self.actions.event_type == 'TRACKPADPAN': move = self.actions.scroll[1] # self.actions.mouse.y - self.actions.mouse_prev.y # print(f'UI_Document.update: trackpad pan {move}') else: d = self.wheel_scroll_lines * 8 * Globals.drawing.get_dpi_mult() move = Globals.drawing.scale(d) * (-1 if self.actions.pressed({'scroll up'}) else 1) self.actions.unpress() if self._get_scrollable(): self._scroll_element.scrollTop = self._scroll_last.y + move self._scroll_element._setup_ltwh(recurse_children=False) # if self.actions.pressed('F8') and self._under_mouse: # print('\n\n') # for e in self._under_mouse.get_pathFromRoot(): # print(e) # print(e._dirty_causes) # for s in e._debug_list: # print(f' {s}') if False: print('---------------------------') if self._focus: print('FOCUS', self._focus, self._focus.pseudoclasses) else: print('FOCUS', None) if self._under_down: print('DOWN', self._under_down, self._under_down.pseudoclasses) else: print('DOWN', None) if under_mouse: print('UNDER', under_mouse, under_mouse.pseudoclasses) else: print('UNDER', None) def _get_scrollable(self): # find first along root to path that can scroll if not self._under_mouse: return None self._scroll_element = next((e for e in self._under_mouse.get_pathToRoot() if e.is_scrollable_y), None) if self._scroll_element: self._scroll_last = RelPoint2D((self._scroll_element.scrollLeft, self._scroll_element.scrollTop)) return self._scroll_element @FSM.on_state('scroll', 'can enter') def scroll_canenter(self): if not self._get_scrollable(): return False @FSM.on_state('scroll', 'enter') def scroll_enter(self): self._scroll_point = self.actions.mouse self.ignore_hover_change = True Globals.cursors.set('SCROLL_Y') @FSM.on_state('scroll') def scroll_main(self): if self.actions.released('MIDDLEMOUSE', ignoremods=True, ignoremulti=True): # done scrolling return 'main' nx = self._scroll_element.scrollLeft + (self._scroll_point.x - self._mx) ny = self._scroll_element.scrollTop - (self._scroll_point.y - self._my) self._scroll_element.scrollLeft = nx self._scroll_element.scrollTop = ny self._scroll_point = self.actions.mouse self._scroll_element._setup_ltwh(recurse_children=False) @FSM.on_state('scroll', 'exit') def scroll_exit(self): self.ignore_hover_change = False @FSM.on_state('mousedown', 'can enter') def mousedown_canenter(self): return self._focus or ( self._under_mouse and self._under_mouse != self._body and not self._under_mouse.is_disabled ) @FSM.on_state('mousedown', 'enter') def mousedown_enter(self): self._mousedown_time = time.time() self._under_mousedown = self._under_mouse if not self._under_mousedown: # likely, self._under_mouse or an ancestor was deleted? # mousedown main event handler below will switch FSM back to main, effectively ignoring the mousedown event # see RetopoFlow issue #857 self.blur() return self._addrem_pseudoclass('active', add_to=self._under_mousedown) self._under_mousedown.dispatch_event('on_mousedown') # print(self._under_mouse.get_pathToRoot()) change_focus = self._focus != self._under_mouse if change_focus: if self._under_mouse.can_focus: # element under mouse takes focus (or whichever it's for points to) if self._under_mouse.forId: f = self._under_mouse.get_for_element() if f and f.can_focus: self.focus(f) else: self.focus(self._under_mouse) else: self.focus(self._under_mouse) elif self._focus and self._is_ancestor(self._focus, self._under_mouse): # current focus is an ancestor of new element, so don't blur! pass else: self.blur() @FSM.on_state('mousedown') def mousedown_main(self): if not self._under_mousedown: return 'main' if self.actions.released('LEFTMOUSE', ignoremods=True, ignoremulti=True): # done with mousedown return 'focus' if self._under_mousedown.can_focus else 'main' if self.actions.pressed('RIGHTMOUSE', ignoremods=True, unpress=False): self._under_mousedown.dispatch_event('on_mousedown') self.handle_hover(change_cursor=False) self.handle_mousemove(ui_element=self._under_mousedown) self.handle_keypress(ui_element=self._under_mousedown) @FSM.on_state('mousedown', 'exit') def mousedown_exit(self): if not self._under_mousedown: # likely, self._under_mousedown or an ancestor was deleted while under mousedown # need to reset variables enough to get us back to main FSM state! self._last_under_click = None self._last_click_time = 0 self.ignore_hover_change = False return self._under_mousedown.dispatch_event('on_mouseup') under_mouseclick = self._under_mousedown click = False click |= time.time() - self._mousedown_time < self.allow_click_time click |= self._under_mousedown.get_mouse_distance(self.actions.mouse) <= self.max_click_dist * self._ui_scale if not click: # find closest common ancestor of self._under_mouse and self._under_mousedown that is getting clicked ancestors0 = self._under_mousedown.get_pathFromRoot() ancestors1 = self._under_mouse.get_pathFromRoot() if self._under_mouse else [] ancestors = [a0 for (a0, a1) in zip(ancestors0, ancestors1) if a0 == a1 and a0.get_mouse_distance(self.actions.mouse) < 1] if ancestors: under_mouseclick = ancestors[-1] click = True # print('mousedown_exit', time.time()-self._mousedown_time, self.allow_click_time, self.actions.mouse, self._under_mousedown.get_mouse_distance(self.actions.mouse), self.max_click_dist) if click: # old/simple: self._under_mouse == self._under_mousedown: dblclick = True dblclick &= under_mouseclick == self._last_under_click dblclick &= time.time() < self._last_click_time + self.doubleclick_time under_mouseclick.dispatch_event('on_mouseclick') self._last_under_click = under_mouseclick if dblclick: under_mouseclick.dispatch_event('on_mousedblclick') # self._last_under_click = None # if self._under_mousedown: # # if applicable, send mouseclick events to ui_element indicated by forId # ui_for = self._under_mousedown.get_for_element() # print(f'mousedown_exit:') # print(f' ui under: {self._under_mousedown}') # print(f' ui for: {ui_for}') # if ui_for: ui_for.dispatch_event('on_mouseclick') self._last_click_time = time.time() else: self._last_under_click = None self._last_click_time = 0 self._addrem_pseudoclass('active', remove_from=self._under_mousedown) # self._under_mousedown.del_pseudoclass('active') def _is_ancestor(self, ancestor, descendant): return ancestor in descendant.get_pathToRoot() def blur(self, stop_at=None): self._focus_full = False if self._focus is None: return self._focus.del_pseudoclass('focus') self._focus.dispatch_event('on_blur') self._focus.dispatch_event('on_focusout', stop_at=stop_at) self._addrem_pseudoclass('active', remove_from=self._focus) self._focus = None def focus(self, ui_element, full=False): if ui_element is None: return if self._focus == ui_element: return stop_focus_at = None if self._focus: stop_blur_at = None p_focus = ui_element.get_pathFromRoot() p_blur = self._focus.get_pathFromRoot() for i in range(min(len(p_focus), len(p_blur))): if p_focus[i] != p_blur[i]: stop_focus_at = p_focus[i] stop_blur_at = p_blur[i] break self.blur(stop_at=stop_blur_at) #print('focusout to', p_blur, stop_blur_at) #print('focusin from', p_focus, stop_focus_at) self._focus_full = full self._focus = ui_element self._focus.add_pseudoclass('focus') self._focus.dispatch_event('on_focus') self._focus.dispatch_event('on_focusin', stop_at=stop_focus_at) @FSM.on_state('focus') def focus_main(self): if not self._focus: return 'main' if self._focus_full: pass if self.actions.pressed('LEFTMOUSE', unpress=False): return 'mousedown' # if self.actions.pressed('RIGHTMOUSE'): # self._debug_print(self._focus) # if self.actions.pressed('ESC'): # self.blur() # return 'main' self.handle_hover() self.handle_mousemove() self.handle_keypress() if not self._focus: return 'main' def force_clean(self, context): if self.defer_cleaning: return time_start = time.time() region = get_view3d_region(context) w,h = region.width, region.height sz = Size2D(width=w, max_width=w, height=h, max_height=h) UI_Core_PreventMultiCalls.reset_multicalls() Globals.ui_draw.update() if Globals.drawing.get_dpi_mult() != self._ui_scale: print(f'DPI CHANGED: {self._ui_scale} -> {Globals.drawing.get_dpi_mult()}') self._ui_scale = Globals.drawing.get_dpi_mult() self._body.dirty(cause='DPI changed', children=True) self._body.dirty_styling() self._body.dirty_flow(children=True) if (w,h) != self._last_sz: self._last_sz = (w,h) self._body.dirty_flow() # self._body.dirty('region size changed', 'style', children=True) # UI_Core_PreventMultiCalls.reset_multicalls() for o in self._callbacks['preclean']: o._call_preclean() self._body.clean() for o in self._callbacks['postclean']: o._call_postclean() self._body._layout( # linefitter=LineFitter(left=0, top=h-1, width=w, height=h), fitting_size=sz, fitting_pos=Point2D((0,h-1)), parent_size=sz, nonstatic_elem=self._body, table_data={}, ) self._body.set_view_size(sz) for o in self._callbacks['postflow']: o._call_postflow() for fn in self._callbacks['postflow once']: fn() self._callbacks['postflow once'].clear() # UI_Core_PreventMultiCalls.reset_multicalls() self._body._layout( # linefitter=LineFitter(left=0, top=h-1, width=w, height=h), fitting_size=sz, fitting_pos=Point2D((0,h-1)), parent_size=sz, nonstatic_elem=self._body, table_data={}, ) self._body.set_view_size(sz) if self._reposition_tooltip_before_draw: self._reposition_tooltip_before_draw = False self._reposition_tooltip() # @profiler.function def draw(self, context): self._context = context self._area = get_view3d_area(context) # if self._area != context.area: return Globals.drawing.glCheckError('UI_Document.draw: start') time_start = time.time() self.force_clean(context) Globals.drawing.glCheckError('UI_Document.draw: setting options') ScissorStack.start(context) gpustate.blend('ALPHA') gpustate.scissor_test(True) gpustate.depth_test('NONE') Globals.drawing.glCheckError('UI_Document.draw: drawing') self._body.draw() ScissorStack.end() self._draw_count += 1 self._draw_time += time.time() - time_start if self._draw_count % 100 == 0: fps = (self._draw_count / self._draw_time) if self._draw_time>0 else float('inf') self._draw_fps = fps # print('~%f fps (%f / %d = %f)' % (self._draw_fps, self._draw_time, self._draw_count, self._draw_time / self._draw_count)) self._draw_count = 0 self._draw_time = 0 Globals.drawing.glCheckError('UI_Document.draw: done') ui_document = Globals.set(UI_Document())