Setting Manual Breakpoints

I’ve found that, while grid_flow is great for quick fixed-column-count sub-layouts, it doesn’t set the best breakpoints for dynamic column counts. It manages to make columns too wide by default and simultaneously cuts off labels when it shouldn’t need to, making dynamic column counts a bad choice for both narrow and wide layouts.

I’ve worked out away to set manual breakpoints that’s relatively simple. Integrating it into existing add-ons is snap, and it brings in a tiny seed of CSS-like functionality that can be applied to a ton of other UI elements. It isn’t 100% feature-complete, but I figured someone else might find the bones useful.

This grabs the width of the sidebar in pixels, adds some padding, converts the width into UI units, and compensates for UI scale:

area = bpy.context.area
resolution = bpy.context.preferences.system.ui_scale
resolution_label = str(resolution)

for reg in area.regions:
    if reg.type == 'UI':
        region_width_raw = reg.width

region_width = region_width_raw - 40
region_width_label = str(flex_box)

region_width_int = round(flex_box / (20 * resolution))
region_width_int_label = str(flex_box_int)

You can use these variables to set manual breakpoints for grid_flow sub-layouts, and even auto abbreviating labels, by doing something like this:

col_set_1 = 8
col_set_2 = 6
col_set_3 = 4
col_set_4 = 6
label = "Long Label Text"

if region_width_int < 12:
    col_set_1 = 6
    col_set_2 = 4
    col_set_3 = 2
    col_set_4 = 3
    label = "Label Text"

if region_width_int <= 8:
    col_set_1 = 3
    col_set_2 = 2
    col_set_3 = 1
    col_set_4 = 1
    label = "Label"

You can also use them to scale other variables, like the split factor for split sub-layouts, by doing something like this:

region_width_factor = round((region_width / 440), 3)

if region_width_factor < 1:
    split_factor = 0.5*region_width_factor
else:
    split_factor = 0.5

layout.split(factor=split_factor, align=True)

Here’s a quick and dirty example of these layout techniques in action:

My eventual goal is to pull together a set of functions, snippets, and mix-ins that can be included with Blender’s python templates. I’m posting this here on devtalk, in part, to get a feel for what features I should focus on and to suss out what improvements I can make on the C side of things.

This class method lets you generate dynamic column counts, with optional debug info:

    @classmethod
    def get_width(context, region, thresholds, values):
        area = bpy.context.area
        resolution = bpy.context.preferences.system.ui_scale

        for reg in area.regions:
            if reg.type == region:
                region_width_raw = reg.width

        region_width = region_width_raw - 40
        region_width_int = round(region_width / (20 * resolution))

        count = [0, 1]
        count[0] = 1
        count[1] = region_width_int

        for i, val in enumerate(thresholds):
            if region_width_int >= thresholds[i]:
                count[0] = values[i]

        return count

In your layout, you can use it like so:

        layout = self.layout

        thresholds = (4, 8, 10, 14, 16)
        values = (1, 2, 3, 4, 6)

        col_num = self.get_panel_width('UI', thresholds, values)
        col_num_label_1 = str(col_num[0])
        col_num_label_2 = str(col_num[1])

        col = layout.column(align=True)

        label_row = col.row(align=True)
        label_row.label(text=col_num_label_1)
        label_row.label(text=col_num_label_2)

        col = layout.grid_flow(columns=col_num[0], align=True)

It doesn’t encapsulate everything from my first post, but does make it easier to create and manage multiple sets of breakpoints. Here’s a class method that applies the same idea to width factors:

    @classmethod
    def get_width_factor(context, region, max, clip):
        area = bpy.context.area
        resolution = bpy.context.preferences.system.ui_scale

        for reg in area.regions:
            if reg.type == region:
                region_width_raw = reg.width

        region_width = region_width_raw - 40
        region_width_int = round(region_width / (20 * resolution))

        region_width_factor = round((region_width_int / max), 3)

        if clip:
            if region_width_factor > 1:
                region_width_factor = 1

        return region_width_factor

And one for quick true/false width checks:

    @classmethod
    def check_width(context, region, threshold, type, max):
        area = bpy.context.area
        resolution = bpy.context.preferences.system.ui_scale

        for reg in area.regions:
            if reg.type == region:
                region_width_raw = reg.width

        if not max:
            max = 440

        region_width = region_width_raw - 40
        region_width_int = round(region_width / (20 * resolution))
        region_width_factor = round((region_width_int / max), 3)

        width_type = [region_width, region_width_int, region_width_factor]

        if width_type[type] >= threshold:
            return True
        else:
            return False