save startup blend for animation tab & whatnot
This commit is contained in:
2026-04-08 12:10:18 -06:00
parent 57a652524a
commit 692e200ffe
180 changed files with 12336 additions and 3431 deletions
@@ -1,14 +1,12 @@
import os
import logging
from typing import Optional
import blf
import bpy
import gpu
from .. import image_utils, ui_bgl
from .bl_ui_widget import BL_UI_Widget
from .bl_ui_image import BL_UI_Image
from .bl_ui_widget import BL_UI_Widget, region_redraw
bk_logger = logging.getLogger(__name__)
@@ -18,6 +16,8 @@ class BL_UI_Button(BL_UI_Widget):
def __init__(self, x, y, width, height):
super().__init__(x, y, width, height)
self.background = True
self.background_padding = (0, 0)
self._text_color = (1.0, 1.0, 1.0, 1.0)
self._hover_bg_color = (0.5, 0.5, 0.5, 1.0)
self._select_bg_color = (0.7, 0.7, 0.7, 1.0)
@@ -30,6 +30,8 @@ class BL_UI_Button(BL_UI_Widget):
self.__image = None
self.__image_size = (24, 24)
self.__image_position = (4, 2)
self.__image_padding = 0.0
self.image_corner_radius = None
@property
def text_color(self):
@@ -38,7 +40,7 @@ class BL_UI_Button(BL_UI_Widget):
@text_color.setter
def text_color(self, value):
if value != self._text_color:
bpy.context.region.tag_redraw()
region_redraw()
self._text_color = value
@property
@@ -48,7 +50,7 @@ class BL_UI_Button(BL_UI_Widget):
@text.setter
def text(self, value):
if value != self._text:
bpy.context.region.tag_redraw()
region_redraw()
self._text = value
@property
@@ -58,7 +60,7 @@ class BL_UI_Button(BL_UI_Widget):
@text_size.setter
def text_size(self, value):
if value != self._text_size:
bpy.context.region.tag_redraw()
region_redraw()
self._text_size = value
@property
@@ -68,7 +70,7 @@ class BL_UI_Button(BL_UI_Widget):
@hover_bg_color.setter
def hover_bg_color(self, value):
if value != self._hover_bg_color:
bpy.context.region.tag_redraw()
region_redraw()
self._hover_bg_color = value
@property
@@ -78,7 +80,7 @@ class BL_UI_Button(BL_UI_Widget):
@select_bg_color.setter
def select_bg_color(self, value):
if value != self._select_bg_color:
bpy.context.region.tag_redraw()
region_redraw()
self._select_bg_color = value
def set_image_size(self, image_size):
@@ -93,13 +95,16 @@ class BL_UI_Button(BL_UI_Widget):
self.__image
self.__image.filepath
# self.__image.pixels
except Exception as e:
except Exception:
self.__image = None
def set_image_colorspace(self, colorspace: str = ""):
image_utils.set_colorspace(self.__image, colorspace)
def set_image(self, rel_filepath):
if rel_filepath is None:
self.__image = None
return
# first try to access the image, for cases where it can get removed
self.check_image_exists()
try:
@@ -117,6 +122,17 @@ class BL_UI_Button(BL_UI_Widget):
return None
return self.__image.filepath
@property
def image_padding(self):
return self.__image_padding
@image_padding.setter
def image_padding(self, padding: float):
self.__image_padding = padding
def get_image_padding(self):
return self.__image_padding
def update(self, x, y):
super().update(x, y)
self._textpos = [x, y]
@@ -127,19 +143,30 @@ class BL_UI_Button(BL_UI_Widget):
area_height = self.get_area_height()
gpu.state.blend_set("ALPHA")
fill_color = self._resolve_panel_color()
self.shader.bind()
self.set_colors()
self.batch_panel.draw(self.shader)
if self.use_rounded_background:
rect_y = area_height - self.y_screen - self.height
self.draw_background_rect(
self.x_screen,
rect_y,
self.width,
self.height,
fill_color,
force=True,
fill_color_override=fill_color,
)
else:
self.shader.bind()
self.shader.uniform_float("color", fill_color)
self.batch_panel.draw(self.shader)
self.draw_image()
# Draw text
self.draw_text(area_height)
def set_colors(self):
def _resolve_panel_color(self):
color = self._bg_color
# pressed
@@ -150,7 +177,7 @@ class BL_UI_Button(BL_UI_Widget):
elif self.__state == 2:
color = self._hover_bg_color
self.shader.uniform_float("color", color)
return color
def draw_text(self, area_height):
font_id = 1
@@ -165,9 +192,19 @@ class BL_UI_Button(BL_UI_Widget):
size = blf.dimensions(font_id, self._text)
# When an image is present, center text in the remaining space after the image
image_offset = 0
if self.__image is not None:
image_offset = self.__image_position[0] + self.__image_size[0]
textpos_y = area_height - self._textpos[1] - (self.height + size[1]) / 2.0
blf.position(
font_id, self._textpos[0] + (self.width - size[0]) / 2.0, textpos_y + 1, 0
font_id,
self._textpos[0]
+ image_offset
+ (self.width - image_offset - size[0]) / 2.0,
textpos_y + 1,
0,
)
r, g, b, a = self._text_color
@@ -180,15 +217,17 @@ class BL_UI_Button(BL_UI_Widget):
y_screen_flip = self.get_area_height() - self.y_screen
off_x, off_y = self.__image_position
sx, sy = self.__image_size
pad = self.__image_padding
ui_bgl.draw_image_runtime(
self.x_screen + off_x,
y_screen_flip - off_y - sy,
sx,
sy,
self.x_screen + off_x + pad,
y_screen_flip - off_y - sy + pad,
sx - 2 * pad,
sy - 2 * pad,
self.__image,
1.0,
crop=(0, 0, 1, 1),
batch=None,
corner_radius=self.image_corner_radius,
)
return True
return False
@@ -203,9 +242,7 @@ class BL_UI_Button(BL_UI_Widget):
self.mouse_down_func(self)
except Exception:
bk_logger.exception("BL_UI_BUTTON mouse_down() error:")
return True
return False
def set_mouse_down_right(self, mouse_down_right_func):
@@ -213,10 +250,11 @@ class BL_UI_Button(BL_UI_Widget):
def mouse_down_right(self, x, y):
if self.is_in_rect(x, y):
try:
self.mouse_down_right_func(self)
except Exception:
bk_logger.exception("BL_UI_BUTTON mouse_down_right() error:")
if hasattr(self, "mouse_down_right_func"):
try:
self.mouse_down_right_func(self)
except Exception:
bk_logger.exception("BL_UI_BUTTON mouse_down_right() error:")
return True
@@ -20,6 +20,15 @@ class BL_UI_Drag_Panel(BL_UI_Widget):
self.widgets = widgets
self.layout_widgets()
def remove_widgets(self):
self.widgets = []
self.update(self.x_screen, self.y_screen)
def remove_widget(self, widget):
if widget in self.widgets:
self.widgets.remove(widget)
self.layout_widgets()
def layout_widgets(self):
for widget in self.widgets:
widget.update(self.x_screen + widget.x, self.y_screen + widget.y)
@@ -4,9 +4,34 @@ from typing import Optional
import bpy
from bpy.types import Operator
from .. import ui_bgl
from .bl_ui_widget import region_redraw
bk_logger = logging.getLogger(__name__)
def get_safely(obj, attr_name, default=None):
"""Get attribute from object while tolerating freed data."""
try:
return getattr(obj, attr_name, default)
except ReferenceError:
return default
except Exception:
return default
def restart_asset_bar():
# ignore failures if already gone
from asset_bar_op import BlenderKitAssetBarOperator
try:
bpy.utils.unregister_class(BlenderKitAssetBarOperator)
except Exception:
pass
bpy.utils.register_class(BlenderKitAssetBarOperator)
bpy.ops.view3d.blenderkit_asset_bar_widget("INVOKE_DEFAULT")
class BL_UI_OT_draw_operator(Operator):
bl_idname = "object.bl_ui_ot_draw_operator"
bl_label = "bl ui widgets operator"
@@ -36,10 +61,6 @@ class BL_UI_OT_draw_operator(Operator):
def invoke(self, context, event):
self.on_invoke(context, event)
args = (self, context)
self.register_handlers(args, context, timer_interval=self._timer_interval)
context.window_manager.modal_handler_add(self)
# first set pointers to keep track if the area is still available
@@ -47,7 +68,10 @@ class BL_UI_OT_draw_operator(Operator):
self.active_area_pointer = context.area.as_pointer()
self.active_region_pointer = context.region.as_pointer()
context.region.tag_redraw()
args = (self, context)
self.register_handlers(args, context, timer_interval=self._timer_interval)
region_redraw(context)
return {"RUNNING_MODAL"}
def register_handlers(self, args, context, timer_interval=0.1):
@@ -80,7 +104,7 @@ class BL_UI_OT_draw_operator(Operator):
return {"FINISHED"}
if context.area:
context.region.tag_redraw()
region_redraw(context)
if self.handle_widget_events(event):
return {"RUNNING_MODAL"}
@@ -93,8 +117,7 @@ class BL_UI_OT_draw_operator(Operator):
def finish(self):
self.unregister_handlers(bpy.context)
# it is possible that the area has been closed, so we check if it is still available
if bpy.context.region is not None:
bpy.context.region.tag_redraw()
region_redraw()
self.on_finish(bpy.context)
# Draw handler to paint onto the screen
@@ -114,7 +137,27 @@ def draw_callback_px_separated(self, op, context):
# hide during animation playback, to improve performance
if context.screen.is_animation_playing:
return
if context.area.as_pointer() == self.active_area_pointer:
area_pointer = (
context.area.as_pointer() if getattr(context, "area", None) else None
)
# get area, check if RNA failed
active_pointer = get_safely(self, "active_area_pointer", None)
if area_pointer is None or area_pointer != active_pointer:
return
active_region_pointer = get_safely(self, "active_region_pointer", None)
if active_region_pointer is not None:
region_pointer = (
context.region.as_pointer()
if getattr(context, "region", None)
else None
)
if region_pointer is None or region_pointer != active_region_pointer:
return
region = getattr(context, "region", None)
with ui_bgl.overlay_matrix_guard(region):
for widget in self.widgets:
widget.draw()
except Exception:
@@ -18,11 +18,21 @@ class BL_UI_Image(BL_UI_Widget):
def __init__(self, x, y, width, height):
super().__init__(x, y, width, height)
self.bg_color = (1.0, 1.0, 1.0, 1.0)
self.__state = 0
self.__image = None
self.__image_size = (24, 24)
self.__image_position = (4, 2)
self.__image_padding: float = 0.0
@property
def image_padding(self):
return self.__image_padding
@image_padding.setter
def image_padding(self, value: float):
self.__image_padding = value
def set_image_size(self, image_size):
self.__image_size = image_size
@@ -67,25 +77,68 @@ class BL_UI_Image(BL_UI_Widget):
return
gpu.state.blend_set("ALPHA")
self.shader.bind()
self.batch_panel.draw(self.shader)
if self.draw_image():
return
self.draw_image()
if self.use_rounded_background:
area_height = self.get_area_height()
rect_y = area_height - self.y_screen - self.height
self.draw_background_rect(
self.x_screen,
rect_y,
self.width,
self.height,
self._bg_color,
force=True,
fill_color_override=self._bg_color,
)
return
self.shader.bind()
self.shader.uniform_float("color", self._bg_color)
self.batch_panel.draw(self.shader)
def draw_image(self):
if self.__image is not None:
y_screen_flip = self.get_area_height() - self.y_screen
off_x, off_y = self.__image_position
sx, sy = self.__image_size
pad = self.image_padding
img_x = self.x_screen + off_x + pad
img_y = y_screen_flip - off_y - sy + pad
img_w = sx - 2 * pad
img_h = sy - 2 * pad
if self.use_rounded_background:
fill_color = self.bg_color or (1.0, 1.0, 1.0, 1.0)
self.draw_background_rect(
img_x,
img_y,
img_w,
img_h,
fill_color,
force=True,
fill_color_override=fill_color,
)
corner_radius = (
self.background_corner_radius
if self.has_background_corner_radius_override()
else None
)
ui_bgl.draw_image_runtime(
self.x_screen + off_x,
y_screen_flip - off_y - sy,
sx,
sy,
img_x,
img_y,
img_w,
img_h,
self.__image,
1.0,
crop=(0, 0, 1, 1),
batch=None,
corner_radius=corner_radius,
corner_segments=12,
)
return True
@@ -1,17 +1,19 @@
import blf
import bpy
import gpu
from typing import Tuple, Union
from gpu_extras.batch import batch_for_shader
from .bl_ui_widget import BL_UI_Widget
from .bl_ui_widget import BL_UI_Widget, region_redraw, set_font_size
class BL_UI_Label(BL_UI_Widget):
"""A simple text label widget."""
def __init__(self, x, y, width, height):
super().__init__(x, y, width, height)
self._set_background_corner_radius_default((4.0,))
self._text_color = (1.0, 1.0, 1.0, 1.0)
self._text = "Label"
@@ -32,7 +34,7 @@ class BL_UI_Label(BL_UI_Widget):
@text_color.setter
def text_color(self, value):
if value != self._text_color:
bpy.context.region.tag_redraw()
region_redraw()
self._text_color = value
@property
@@ -42,13 +44,173 @@ class BL_UI_Label(BL_UI_Widget):
@text.setter
def text(self, value):
if value != self._text:
bpy.context.region.tag_redraw()
region_redraw()
self._text = value
@property
def text_size(self):
return self._text_size
@text_size.setter
def text_size(self, value):
if value != self._text_size:
region_redraw()
self._text_size = value
def is_in_rect(self, x, y):
return False
def draw(self):
if not self._is_visible or not self._text:
return
area_height = self.get_area_height()
font_id = 1
set_font_size(font_id, self._text_size)
textpos_y = area_height - self.y_screen - self.height
x = self.x_screen
y = textpos_y
block_width, block_height = blf.dimensions(font_id, self._text)
if self._halign == "RIGHT":
x -= block_width
elif self._halign == "CENTER":
x -= block_width // 2
if self._halign != "LEFT" and self._valign == "CENTER":
y -= block_height // 2
lines = self._text.split("\n") if self.multiline else [self._text]
entries = []
cursor_y = y
for index, line in enumerate(lines):
if index > 0:
cursor_y -= self.row_height
width, height = blf.dimensions(font_id, line)
if self.multiline and height == 0:
height = self.row_height
elif height == 0:
height = self._text_size
entries.append(
{
"text": line,
"x": x,
"y": cursor_y,
"width": width,
"height": height,
}
)
if not entries:
return
min_x = min(item["x"] for item in entries)
max_x = max(item["x"] + item["width"] for item in entries)
min_y = min(item["y"] for item in entries)
max_y = max(item["y"] + item["height"] for item in entries)
content_width = max(0.0, max_x - min_x)
content_height = max(0.0, max_y - min_y)
self.draw_background_rect(
min_x,
min_y,
content_width,
content_height,
self._bg_color,
)
r, g, b, a = self._text_color
for item in entries:
if not item["text"]:
continue
blf.position(font_id, item["x"], item["y"], 0)
blf.color(font_id, r, g, b, a)
blf.draw(font_id, item["text"])
if content_width > 0:
strike_y = min_y + content_height * 0.5
self.draw_strikethrough(
min_x,
max_x,
strike_y,
self._text_color,
)
class BL_UI_DuoLabel(BL_UI_Widget):
"""A label with two text fields, A and B."""
def __init__(self, x, y, width, height):
super().__init__(x, y, width, height)
self._set_background_corner_radius_default((4.0,))
self._text_a_color = (1.0, 1.0, 1.0, 1.0)
self._text_a = "Label"
self._text_b_color = (1.0, 1.0, 1.0, 1.0)
self._text_b = ""
self._text_size = 16
self._halign = "LEFT"
self._valign = "TOP"
# multiline
self.multiline = False
self.row_height = 20
self.strikethrough_a = False
self.strikethrough_b = False
self.segment_backgrounds = False
self.segment_background_padding = None
self.segment_background_color_a = None
self.segment_background_color_b = None
self.segment_background_gap = 0.0
self.segment_spacing = 4.0
self.segment_background_extra_top = 0.0
self.segment_background_extra_bottom = 1.5
@property
def text_a_color(self):
return self._text_a_color
@text_a_color.setter
def text_a_color(self, value):
if value != self._text_a_color:
bpy.context.region.tag_redraw()
self._text_a_color = value
@property
def text_a(self):
return self._text_a
@text_a.setter
def text_a(self, value):
if value != self._text_a:
bpy.context.region.tag_redraw()
self._text_a = value
@property
def text_b_color(self):
return self._text_b_color
@text_b_color.setter
def text_b_color(self, value):
if value != self._text_b_color:
bpy.context.region.tag_redraw()
self._text_b_color = value
@property
def text_b(self):
return self._text_b
@text_b.setter
def text_b(self, value):
if value != self._text_b:
bpy.context.region.tag_redraw()
self._text_b = value
@property
def text_size(self):
return self._text_size
@text_size.setter
def text_size(self, value):
if value != self._text_size:
@@ -65,98 +227,266 @@ class BL_UI_Label(BL_UI_Widget):
area_height = self.get_area_height()
font_id = 1
if bpy.app.version < (4, 0, 0):
blf.size(font_id, self._text_size, 72)
else:
blf.size(font_id, self._text_size)
lines = self._text.split("\n") if self.multiline else [self._text]
if not lines:
return
default_line_height = self.row_height if self.multiline else self._text_size
line_metrics = []
max_line_width = 0.0
total_height = 0.0
for line in lines:
width, height = blf.dimensions(font_id, line)
if height == 0:
height = default_line_height
line_height = (
self.row_height if self.multiline else max(height, self._text_size)
)
if line_height == 0:
line_height = default_line_height
line_metrics.append((line, width, line_height))
max_line_width = max(max_line_width, width)
total_height += line_height
if not line_metrics:
return
set_font_size(font_id, self._text_size)
textpos_y = area_height - self.y_screen - self.height
r, g, b, a = self._text_color
x = self.x_screen
y = textpos_y
if self._halign != "LEFT":
width, height = blf.dimensions(font_id, self._text)
if self._halign == "RIGHT":
x -= width
elif self._halign == "CENTER":
x -= width // 2
cursor_x = self.x_screen
spacing = max(0.0, float(self.segment_spacing))
blocks = [
(
self._text_a,
self._text_a_color,
self.strikethrough_a,
self.segment_background_color_a,
),
(
self._text_b,
self._text_b_color,
self.strikethrough_b,
self.segment_background_color_b,
),
]
segments = []
for text, color, strike_flag, background_color in blocks:
if not text:
continue
set_font_size(font_id, self._text_size)
width, height = blf.dimensions(font_id, text)
scaled_size = self._text_size
scaled = False
if self.width > 0:
if self._halign == "LEFT":
available_width = max(1, self.x_screen + self.width - cursor_x)
else:
available_width = self.width
if width > available_width and width > 0:
scale = available_width / width
scaled_size = max(8, int(self._text_size * scale))
if scaled_size < self._text_size:
set_font_size(font_id, scaled_size)
width, height = blf.dimensions(font_id, text)
scaled = True
x = cursor_x if self._halign == "LEFT" else self.x_screen
y = textpos_y
if self._halign != "LEFT":
if self._halign == "RIGHT":
x -= width
elif self._halign == "CENTER":
x -= width // 2
if self._valign == "CENTER":
y -= height // 2
# bottom could be here but there's no reason for it
first_line_height = line_metrics[0][2]
if not self.multiline:
lines = [
{
"text": text,
"x": x,
"y": y,
"width": width,
"height": height or self._text_size,
"font_size": scaled_size,
}
]
else:
lines = []
current_y = y
split_lines = text.split("\n")
for index, line in enumerate(split_lines):
if index > 0:
current_y -= self.row_height
line_width, line_height = blf.dimensions(font_id, line)
if line_height == 0:
line_height = self.row_height
lines.append(
{
"text": line,
"x": x,
"y": current_y,
"width": line_width,
"height": line_height,
"font_size": scaled_size,
}
)
width = max((line["width"] for line in lines), default=width)
height = max(len(lines) * self.row_height, height)
if self.background and (max_line_width > 0 or total_height > 0):
pad_x, pad_y = self._padding_tuple()
text_top = y + first_line_height
text_bottom = text_top - total_height
left = x - pad_x
right = x + max_line_width + pad_x
top = text_top + pad_y
bottom = text_bottom - pad_y
self._draw_background_rect(left, right, bottom, top)
if lines:
seg_min_x = min(line["x"] for line in lines)
seg_max_x = max(line["x"] + line["width"] for line in lines)
seg_min_y = min(line["y"] for line in lines)
seg_max_y = max(line["y"] + line["height"] for line in lines)
bounds = {
"min_x": seg_min_x,
"max_x": seg_max_x,
"min_y": seg_min_y,
"max_y": seg_max_y,
}
else:
bounds = None
current_y = y
if not self.multiline:
blf.position(font_id, x, current_y, 0)
blf.color(font_id, r, g, b, a)
blf.draw(font_id, self._text)
else:
for line, _, line_height in line_metrics:
blf.position(font_id, x, current_y, 0)
segments.append(
{
"lines": lines,
"color": color,
"strikethrough": strike_flag,
"bounds": bounds,
"background_color": background_color,
}
)
if self._halign == "LEFT" and bounds:
cursor_x = bounds["max_x"] + spacing
if scaled:
set_font_size(font_id, self._text_size)
if not segments:
return
all_lines = [line for segment in segments for line in segment["lines"]]
if not all_lines:
return
min_x = min(line["x"] for line in all_lines)
max_x = max(line["x"] + line["width"] for line in all_lines)
min_y = min(line["y"] for line in all_lines)
max_y = max(line["y"] + line["height"] for line in all_lines)
content_width = max(0.0, max_x - min_x)
content_height = max(0.0, max_y - min_y)
if self.segment_backgrounds:
pad_source = (
self.segment_background_padding
if self.segment_background_padding is not None
else self.background_padding
)
if isinstance(pad_source, (list, tuple)):
base_pad_x = float(pad_source[0])
base_pad_y = (
float(pad_source[1])
if len(pad_source) > 1
else float(pad_source[0])
)
else:
base_pad_x = base_pad_y = float(pad_source)
bounded_segments = [seg for seg in segments if seg.get("bounds")]
total_bounded = len(bounded_segments)
desired_gap = max(0.0, float(self.segment_background_gap))
spacing = max(0.0, float(self.segment_spacing))
interior_pad = max(0.0, (spacing - desired_gap) * 0.5)
extra_top = max(0.0, float(self.segment_background_extra_top))
extra_bottom = max(0.0, float(self.segment_background_extra_bottom))
def coerce_corner_radii(value):
if isinstance(value, (tuple, list)):
values = list(value)
else:
values = [value]
if not values:
values = [0.0]
if len(values) == 1:
values = values * 4
elif len(values) == 2:
values = [values[0], values[1], values[1], values[0]]
elif len(values) < 4:
values = values + [values[-1]] * (4 - len(values))
return tuple(values[:4])
base_corner_radii = coerce_corner_radii(self.background_corner_radius)
for idx, segment in enumerate(bounded_segments):
bounds = segment.get("bounds")
if not bounds:
continue
seg_width = max(0.0, bounds["max_x"] - bounds["min_x"])
seg_height = max(0.0, bounds["max_y"] - bounds["min_y"])
if seg_width <= 0 or seg_height <= 0:
continue
pad_left = base_pad_x if idx == 0 else interior_pad
pad_right = base_pad_x if idx == total_bounded - 1 else interior_pad
if total_bounded > 1:
left_edge = idx == 0
right_edge = idx == total_bounded - 1
corner_override = (
base_corner_radii[0] if left_edge else 0.0,
base_corner_radii[1] if right_edge else 0.0,
base_corner_radii[2] if right_edge else 0.0,
base_corner_radii[3] if left_edge else 0.0,
)
else:
corner_override = None
padding_override = (
pad_left,
pad_right,
base_pad_y + extra_bottom,
base_pad_y + extra_top,
)
self.draw_background_rect(
bounds["min_x"],
bounds["min_y"],
seg_width,
seg_height,
segment.get("background_color") or segment["color"],
force=True,
padding_override=padding_override,
corner_radius_override=corner_override,
)
background_drawn = False
if not self.segment_backgrounds:
base_color = segments[0]["color"] if segments else self._text_a_color
self.draw_background_rect(
min_x,
min_y,
content_width,
content_height,
base_color,
)
background_drawn = True
for segment in segments:
r, g, b, a = segment["color"]
for line in segment["lines"]:
if not line["text"]:
continue
set_font_size(font_id, line.get("font_size", self._text_size))
blf.position(font_id, line["x"], line["y"], 0)
blf.color(font_id, r, g, b, a)
blf.draw(font_id, line)
current_y -= line_height
blf.draw(font_id, line["text"])
set_font_size(font_id, self._text_size)
def _padding_tuple(self) -> Tuple[float, float]:
pad = self.padding
if isinstance(pad, (list, tuple)):
if len(pad) == 0:
return (0.0, 0.0)
if len(pad) == 1:
value = float(pad[0])
return (value, value)
return (float(pad[0]), float(pad[1]))
value = float(pad)
return (value, value)
for segment in segments:
if not segment.get("strikethrough"):
continue
bounds = segment.get("bounds")
if not bounds:
continue
segment_min_x = bounds["min_x"]
segment_max_x = bounds["max_x"]
if segment_max_x <= segment_min_x:
continue
strike_y = bounds["min_y"] + (bounds["max_y"] - bounds["min_y"]) * 0.5
self.draw_strikethrough(
segment_min_x,
segment_max_x,
strike_y,
segment["color"],
force=True,
)
def _draw_background_rect(self, left, right, bottom, top):
vertices = (
(left, top),
(left, bottom),
(right, bottom),
(right, top),
)
indices = ((0, 1, 2), (0, 2, 3))
gpu.state.blend_set("ALPHA")
self.shader.bind()
self.shader.uniform_float("color", self._bg_color)
batch = batch_for_shader(
self.shader, "TRIS", {"pos": vertices}, indices=indices
)
batch.draw(self.shader)
if content_width > 0 and background_drawn:
strike_color = segments[0]["color"]
strike_y = min_y + content_height * 0.5
self.draw_strikethrough(
min_x,
max_x,
strike_y,
strike_color,
)
@@ -1,7 +1,53 @@
import blf
import bpy
import gpu
from gpu_extras.batch import batch_for_shader
from .. import ui_bgl
from typing import Union
def clamp(value, min_value=0.0, max_value=1.0):
return max(min_value, min(max_value, value))
def set_font_size(font_id, size):
if bpy.app.version < (4, 0, 0):
blf.size(font_id, size, 72)
else:
blf.size(font_id, size)
def tint_color(color, tint_amount):
if tint_amount == 0.0:
return color
r, g, b, a = color
if tint_amount > 0.0:
r += (1.0 - r) * tint_amount
g += (1.0 - g) * tint_amount
b += (1.0 - b) * tint_amount
else:
r *= 1.0 + tint_amount
g *= 1.0 + tint_amount
b *= 1.0 + tint_amount
return (clamp(r), clamp(g), clamp(b), a)
def resolve_fill_color(preferred_color, fallback_color):
color = preferred_color or fallback_color or (1.0, 1.0, 1.0, 1.0)
r, g, b, a = color
return (clamp(r), clamp(g), clamp(b), clamp(a))
def region_redraw(ctx: bpy.types.Context = None):
if ctx is not None:
context = ctx
else:
context = bpy.context
if context.region is not None:
context.region.tag_redraw()
class BL_UI_Widget:
def __init__(self, x, y, width, height):
@@ -19,15 +65,141 @@ class BL_UI_Widget:
self._mouse_down_right = False
self._is_visible = True
self._is_active = True # if the widget needs to be disabled
# decorative helpers (opt-in per widget)
self._background_enabled = False
self.use_rounded_background = False
self.background_padding: tuple[int, int] = (0, 0)
# Radius can be '50%' for pill shape, each corner individually
self._background_corner_radius: Union[
tuple[Union[str, float], ...],
str,
float,
] = (0.0,)
self._background_corner_radius_custom = False
self.background_border = False
self.background_border_color = None
self.background_border_tint = 0.2
self.background_border_thickness = 1.0
self.strikethrough = False
self.strikethrough_thickness = 1.25
if bpy.app.version < (4, 0, 0):
self.shader = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
else:
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
@property
def background_corner_radius(self):
return self._background_corner_radius
@background_corner_radius.setter
def background_corner_radius(self, value):
self._background_corner_radius = value
self._background_corner_radius_custom = True
@property
def background(self):
return self._background_enabled
@background.setter
def background(self, value):
enabled = bool(value)
self._background_enabled = enabled
if not enabled:
self.use_rounded_background = False
elif enabled and not self.use_rounded_background:
self.use_rounded_background = True
def _set_background_corner_radius_default(self, value):
self._background_corner_radius = value
self._background_corner_radius_custom = False
def has_background_corner_radius_override(self):
return self._background_corner_radius_custom
def resolve_background_fill(self, fallback_color, preferred_color=None):
base_color = fallback_color if preferred_color is None else preferred_color
return resolve_fill_color(
base_color,
fallback_color,
)
def resolve_background_border(self, fill_color):
if not self.background_border:
return None
if self.background_border_color:
return self.background_border_color
return tint_color(fill_color, self.background_border_tint)
def draw_background_rect(
self,
min_x,
min_y,
width,
height,
fallback_color,
*,
force=False,
padding_override=None,
corner_radius_override=None,
fill_color_override=None,
):
if (not self.use_rounded_background and not force) or width <= 0 or height <= 0:
return
if padding_override is None:
pad_x = self.background_padding[0]
pad_y = self.background_padding[1]
pad_left = pad_right = pad_x
pad_bottom = pad_top = pad_y
else:
if len(padding_override) == 4:
pad_left, pad_right, pad_bottom, pad_top = padding_override
elif len(padding_override) == 2:
pad_left = pad_right = padding_override[0]
pad_bottom = pad_top = padding_override[1]
else:
pad_left = pad_right = padding_override[0]
pad_bottom = pad_top = padding_override[1]
rect_x = min_x - pad_left
rect_y = min_y - pad_bottom
rect_width = width + pad_left + pad_right
rect_height = height + pad_top + pad_bottom
fill_color = self.resolve_background_fill(
fallback_color,
preferred_color=fill_color_override,
)
border_color = self.resolve_background_border(fill_color)
corner_radius = (
corner_radius_override
if corner_radius_override is not None
else self.background_corner_radius
)
ui_bgl.draw_rounded_rect_with_border(
rect_x,
rect_y,
rect_width,
rect_height,
radius=corner_radius,
fill_color=fill_color,
border_color=border_color,
border_thickness=self.background_border_thickness,
)
def draw_strikethrough(self, min_x, max_x, y, color, *, force=False):
if (not self.strikethrough and not force) or max_x <= min_x:
return
ui_bgl.draw_line2d(
min_x,
y,
max_x,
y,
self.strikethrough_thickness,
color,
)
def set_location(self, x, y):
# if self.x != x or self.y != y or self.x_screen != x or self.y_screen != y:
# bpy.context.region.tag_redraw()
# region_redraw()
self.x = x
self.y = y
self.x_screen = x
@@ -41,8 +213,15 @@ class BL_UI_Widget:
@bg_color.setter
def bg_color(self, value):
self._bg_color = value
if bpy.context.region is not None:
bpy.context.region.tag_redraw()
region_redraw()
@property
def background_color(self):
return self._bg_color
@background_color.setter
def background_color(self, value):
self.bg_color = value
@property
def visible(self):
@@ -51,17 +230,17 @@ class BL_UI_Widget:
@visible.setter
def visible(self, value):
if value != self._is_visible:
bpy.context.region.tag_redraw()
region_redraw()
self._is_visible = value
@property
def active(self):
return self._is_active
@visible.setter
@active.setter
def active(self, value):
if value != self._is_active:
bpy.context.region.tag_redraw()
region_redraw()
self._is_active = value
@property
@@ -78,6 +257,20 @@ class BL_UI_Widget:
gpu.state.blend_set("ALPHA")
if self.use_rounded_background:
area_height = self.get_area_height()
rect_y = area_height - self.y_screen - self.height
self.draw_background_rect(
self.x_screen,
rect_y,
self.width,
self.height,
self._bg_color,
force=True,
fill_color_override=self._bg_color,
)
return
self.shader.bind()
self.shader.uniform_float("color", self._bg_color)
@@ -107,7 +300,7 @@ class BL_UI_Widget:
self.batch_panel = batch_for_shader(
self.shader, "TRIS", {"pos": vertices}, indices=indices
)
bpy.context.region.tag_redraw()
region_redraw()
def handle_event(self, event):
"""
@@ -121,28 +314,27 @@ class BL_UI_Widget:
if not self._is_active:
return False
x = event.mouse_region_x
y = event.mouse_region_y
x, y = self._to_widget_region_coords(event)
if event.type == "LEFTMOUSE":
if event.value == "PRESS":
self._mouse_down = True
bpy.context.region.tag_redraw()
region_redraw()
return self.mouse_down(x, y)
else:
self._mouse_down = False
bpy.context.region.tag_redraw()
region_redraw()
self.mouse_up(x, y)
return False
elif event.type == "RIGHTMOUSE":
if event.value == "PRESS":
self._mouse_down_right = True
bpy.context.region.tag_redraw()
region_redraw()
return self.mouse_down_right(x, y)
else:
self._mouse_down_right = False
bpy.context.region.tag_redraw()
region_redraw()
self.mouse_up(x, y)
elif event.type == "MOUSEMOVE":
@@ -154,13 +346,13 @@ class BL_UI_Widget:
self.__inrect = True
self.mouse_enter(event, x, y)
# we tag redraw since the hover colors are picked in the draw function
bpy.context.region.tag_redraw()
region_redraw()
# we are leaving the rect
elif self.__inrect and not inrect:
self.__inrect = False
self.mouse_exit(event, x, y)
bpy.context.region.tag_redraw()
region_redraw()
# return always false to enable mouse exit events on other buttons.(would sometimes not hide the tooltip)
return False # self.__inrect
@@ -174,6 +366,26 @@ class BL_UI_Widget:
return False
def _to_widget_region_coords(self, event):
region = None
ctx = self.context
if isinstance(ctx, dict):
region = ctx.get("region")
elif hasattr(ctx, "region"):
region = getattr(ctx, "region")
if (
region is not None
and hasattr(event, "mouse_x")
and hasattr(event, "mouse_y")
):
try:
return event.mouse_x - region.x, event.mouse_y - region.y
except AttributeError:
pass
return getattr(event, "mouse_region_x", 0), getattr(event, "mouse_region_y", 0)
def get_input_keys(self):
return []