Skip to content

Instantly share code, notes, and snippets.

@LouisGameDev
Created July 3, 2025 20:45
Show Gist options
  • Save LouisGameDev/61966ce089556a8bf5de2f87be37036a to your computer and use it in GitHub Desktop.
Save LouisGameDev/61966ce089556a8bf5de2f87be37036a to your computer and use it in GitHub Desktop.

Revisions

  1. LouisGameDev created this gist Jul 3, 2025.
    242 changes: 242 additions & 0 deletions frame_style_nil_bug_demo.lua
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,242 @@
    --@module = true

    local gui = require('gui')
    local widgets = require('gui.widgets')

    local INVISIBLE_FRAME = {
    frame_pen=gui.CLEAR_PEN,
    signature_pen=false,
    }


    -- MAIN BUG TRIGGERED HERE
    -----------------------------------------
    BugDemoPanel = defclass(BugDemoPanel, widgets.Panel)
    BugDemoPanel.ATTRS {
    --frame_style = INVISIBLE_FRAME
    -- ^ This is the key part that triggers the bug.
    -- if style is nil, it will be nil and panel.lua::Panel_end_drag will never be called
    -- https://github.com/DFHack/dfhack/blob/e3165854e80411eec584c74949c73000f2aa0dcb/library/lua/gui/widgets/containers/panel.lua#L497C1-L498C1
    -- even if user intends to finish drag
    }
    -----------------------------------------

    function BugDemoPanel:onInput(keys)
    local key_info = {}
    for key, value in pairs(keys) do
    table.insert(key_info, string.format("%s=%s", tostring(key), tostring(value)))
    end

    table.sort(key_info)

    return BugDemoPanel.super.onInput(self, keys)
    end

    BugDemoWindow = defclass(BugDemoWindow, widgets.Window)
    BugDemoWindow.ATTRS {
    view_id = 'bugdemo_window',
    frame_title = 'Frame Style Bug Demo',
    resizable = true,
    resize_min = {w = 54, h = 20},
    frame_inset = {l = 0, r = 0, t = 0, b = 0}
    }

    BugDemoScreen = defclass(BugDemoScreen, gui.ZScreen)
    BugDemoScreen.ATTRS {
    view_id = 'bugdemo_screen',
    focus_path = 'bugdemo',
    pass_movement_keys = true,
    defocusable = true
    }

    function BugDemoWindow:init()
    self.resizing_panels = false

    self:addviews{
    BugDemoPanel {
    view_id = 'category_panel',
    frame = {l = 0, w = 22, t = 0, b = 1},
    frame_inset = {l = 1, t = 0, b = 1, r = 1},
    resizable = true,
    resize_anchors = {l = false, t = false, r = true, b = true},
    resize_min = {w = 24},
    on_resize_begin = self:callback('onPanelResizeBegin'),
    on_resize_end = self:callback('onPanelResizeEnd'),
    subviews = {
    widgets.Label {
    frame = {t = 0, l = 0},
    text = 'category_panel',
    text_pen = COLOR_YELLOW
    }
    }
    },

    widgets.Divider {
    view_id = 'divider1',
    frame = {l = 30, t = 0, b = 2, w = 1},
    interior_b = true,
    frame_style_t = false
    },

    widgets.Panel {
    view_id = 'item_panel',
    frame = {t = 1, b = 3, l = 25, r = 0},
    resize_min = {w = 30, h = 10},
    frame_inset = {l = 1, r = 0},
    subviews = {
    widgets.Label {
    view_id = 'item_header',
    frame = {t = 0, l = 0, r = 0, h = 1},
    text = 'item_panel',
    text_pen = COLOR_YELLOW
    }
    }
    },

    widgets.HelpButton {
    command = "gui/stockmonitor",
    frame = {r = 0, t = 1}
    },

    widgets.Divider {
    frame = {l = 0, r = 0, b = 2, h = 1},
    frame_style_l = false,
    frame_style_r = false,
    interior_l = true
    },

    widgets.Panel {
    frame = {l = 0, r = 0, b = 1, h = 1},
    frame_inset = {l = 1, r = 1, t = 0, w = 100},
    subviews = {
    widgets.HotkeyLabel {
    frame = {l = 0},
    key = 'CUSTOM_CTRL_R',
    label = 'Button A',
    auto_width = true,
    on_activate = function()
    self:testResize()
    end
    },
    widgets.HotkeyLabel {
    frame = {l = 25},
    key = 'CUSTOM_CTRL_O',
    label = 'Button B',
    auto_width = true,
    on_activate = function()
    end
    }
    }
    }
    }
    end

    function BugDemoWindow:testResize()
    local category_panel = self.subviews.category_panel
    if category_panel then
    category_panel.frame.w = (category_panel.frame.w == 22) and 32 or 22
    self:ensurePanelsRelSize()
    self:updateLayout()
    end
    end

    function BugDemoWindow:onPanelResizeBegin()
    self.resizing_panels = true
    end

    function BugDemoWindow:onPanelResizeEnd()
    self.resizing_panels = false
    self:ensurePanelsRelSize()
    self:updateLayout()
    end

    function BugDemoWindow:ensurePanelsRelSize()
    local category_panel = self.subviews.category_panel
    local item_panel = self.subviews.item_panel
    local divider1 = self.subviews.divider1

    if not category_panel or not item_panel or not divider1 then return end

    local main_width = self.frame.w or 100
    local min_item_width = 30

    category_panel.frame.w =
    math.min(math.max(category_panel.frame.w, 24),
    main_width - min_item_width)

    local cat_width = category_panel.frame.w
    item_panel.frame.l = category_panel.visible and cat_width or 1
    divider1.frame.l = item_panel.frame.l - 1
    end

    function BugDemoWindow:onRenderBody(dc)
    if self.resizing_panels then
    self:ensurePanelsRelSize()
    self:updateLayout()
    end
    return BugDemoWindow.super.onRenderBody(self, dc)
    end

    function BugDemoWindow:preUpdateLayout()
    self:ensurePanelsRelSize()
    end

    function BugDemoWindow:onInput(keys)
    local key_info = {}
    for key, value in pairs(keys) do
    table.insert(key_info, string.format("%s=%s", tostring(key), tostring(value)))
    end

    table.sort(key_info)

    return BugDemoWindow.super.onInput(self, keys)
    end

    function BugDemoScreen:init()
    local optimal_size = self:calculateOptimalFrameSize()
    self:addviews{BugDemoWindow {frame = optimal_size}}
    end

    function BugDemoScreen:calculateOptimalFrameSize()
    local base_width = 100
    local base_height = 35

    local optimal_width = math.max(base_width, 90)
    local optimal_height = math.max(base_height, 25)

    return {w = optimal_width, h = optimal_height}
    end

    function BugDemoScreen:onDismiss()
    bug_demo_screen = nil
    end

    bug_demo_screen = bug_demo_screen or nil

    if dfhack_flags.module then
    return
    end

    -- Main entry point
    -- if not dfhack.isWorldLoaded() then
    -- qerror("This script requires a loaded world")
    -- end

    -- Check for existing instance
    if bug_demo_screen and not bug_demo_screen._native then
    bug_demo_screen = nil
    end

    if bug_demo_screen then
    local success, is_dismissed = pcall(function()
    return bug_demo_screen:isDismissed()
    end)
    if not success or is_dismissed then
    bug_demo_screen = nil
    else
    return
    end
    end

    -- Create and show new instance
    bug_demo_screen = BugDemoScreen{}:show()