This commit is contained in:
Scott Duensing 2026-08-25 16:42:49 -05:00
parent aab9facb6c
commit b638ca9b9a
71 changed files with 4432 additions and 2504 deletions

View file

@ -624,8 +624,8 @@ img { max-width: 100%; }
<li>Properties Panel -- a tree of controls and a list of editable properties for the selected control.</li> <li>Properties Panel -- a tree of controls and a list of editable properties for the selected control.</li>
<li>Output Window -- displays PRINT output and runtime errors.</li> <li>Output Window -- displays PRINT output and runtime errors.</li>
<li>Immediate Window -- an interactive REPL for evaluating expressions and modifying variables at runtime.</li> <li>Immediate Window -- an interactive REPL for evaluating expressions and modifying variables at runtime.</li>
<li>Debug Windows -- Locals, Call Stack, Watch, and Breakpoints windows that appear automatically when debugging.</li>
</ul> </ul>
<p>Debug Windows -- Locals, Call Stack, Watch, and Breakpoints windows that appear automatically when debugging.</p>
<p>The IDE compiles BASIC source into bytecode and runs it in an integrated virtual machine. Programs execute in cooperative slices, yielding to DVX between slices so the GUI remains responsive. Call DoEvents in long-running loops to keep the event loop flowing.</p> <p>The IDE compiles BASIC source into bytecode and runs it in an integrated virtual machine. Programs execute in cooperative slices, yielding to DVX between slices so the GUI remains responsive. Call DoEvents in long-running loops to keep the event loop flowing.</p>
<p><a href="#ide.menu.file">File Menu</a></p> <p><a href="#ide.menu.file">File Menu</a></p>
<p><a href="#ide.menu.edit">Edit Menu</a></p> <p><a href="#ide.menu.edit">Edit Menu</a></p>
@ -803,8 +803,8 @@ img { max-width: 100%; }
<p>At the top of the Code Editor are two dropdown lists:</p> <p>At the top of the Code Editor are two dropdown lists:</p>
<ul> <ul>
<li>Object -- lists (General) plus all objects (form name, control names, menu item names). Selecting an object filters the Function dropdown.</li> <li>Object -- lists (General) plus all objects (form name, control names, menu item names). Selecting an object filters the Function dropdown.</li>
<li>Function -- lists all event handlers (procedures) for the selected object. Implemented handlers are listed first (plain text); unimplemented handlers follow in brackets (e.g., [Click]). Selecting an unimplemented event creates a new event handler stub.</li>
</ul> </ul>
<p>Function -- lists all event handlers (procedures) for the selected object. Implemented handlers are listed first (plain text); unimplemented handlers follow in brackets (e.g., [Click]). Selecting an unimplemented event creates a new event handler stub.</p>
<p>The editor shows one procedure at a time. Each procedure has its own buffer, and switching between them is instantaneous. The (General) section contains module-level declarations and code.</p> <p>The editor shows one procedure at a time. Each procedure has its own buffer, and switching between them is instantaneous. The (General) section contains module-level declarations and code.</p>
<h2>Syntax Highlighting</h2> <h2>Syntax Highlighting</h2>
<p>The editor applies real-time syntax coloring as you type. Seven categories are highlighted in distinct colors; the colors themselves are editable in Tools &gt; Preferences &gt; Colors.</p> <p>The editor applies real-time syntax coloring as you type. Seven categories are highlighted in distinct colors; the colors themselves are editable in Tools &gt; Preferences &gt; Colors.</p>
@ -823,8 +823,8 @@ img { max-width: 100%; }
<li>Auto-indent -- new lines are automatically indented to match the previous line.</li> <li>Auto-indent -- new lines are automatically indented to match the previous line.</li>
<li>Tab handling -- the Tab key is captured by the editor. Tab width and whether to insert spaces or tab characters are configurable in Preferences (default: 3 spaces).</li> <li>Tab handling -- the Tab key is captured by the editor. Tab width and whether to insert spaces or tab characters are configurable in Preferences (default: 3 spaces).</li>
<li>Gutter click -- clicking in the line number gutter toggles a breakpoint on that line.</li> <li>Gutter click -- clicking in the line number gutter toggles a breakpoint on that line.</li>
<li>Line decorations -- breakpoint lines show a red dot in the gutter. The current debug line (when paused) is highlighted with a yellow background.</li>
</ul> </ul>
<p>Line decorations -- breakpoint lines show a red dot in the gutter. The current debug line (when paused) is highlighted with a yellow background.</p>
<p><a href="#ide.overview">Back to Overview</a></p> <p><a href="#ide.overview">Back to Overview</a></p>
</div> </div>
<div class="topic" id="ide.designer"> <div class="topic" id="ide.designer">
@ -838,8 +838,8 @@ img { max-width: 100%; }
<li>Reordering -- drag a control vertically to reorder it within the form's layout (VBox/HBox).</li> <li>Reordering -- drag a control vertically to reorder it within the form's layout (VBox/HBox).</li>
<li>Placing controls -- select a control type in the Toolbox, then click on the form to place a new instance. The control is auto-named (e.g., Command1, Command2). Clicking the same tool again deselects it (toggles back to pointer mode).</li> <li>Placing controls -- select a control type in the Toolbox, then click on the form to place a new instance. The control is auto-named (e.g., Command1, Command2). Clicking the same tool again deselects it (toggles back to pointer mode).</li>
<li>Menu bar preview -- if the form has menu items (defined via the Menu Editor), a preview menu bar is rendered on the design window.</li> <li>Menu bar preview -- if the form has menu items (defined via the Menu Editor), a preview menu bar is rendered on the design window.</li>
<li>Delete key -- removes the selected control from the form.</li>
</ul> </ul>
<p>Delete key -- removes the selected control from the form.</p>
<h2>Form Properties</h2> <h2>Form Properties</h2>
<p>Forms have the following design-time properties: Name, Caption, Width, Height, Left, Top, Layout (VBox or HBox), Centered, AutoSize, and Resizable.</p> <p>Forms have the following design-time properties: Name, Caption, Width, Height, Left, Top, Layout (VBox or HBox), Centered, AutoSize, and Resizable.</p>
<p><a href="#ide.properties">Properties Panel</a></p> <p><a href="#ide.properties">Properties Panel</a></p>
@ -854,8 +854,8 @@ img { max-width: 100%; }
<li>Name -- the project display name (up to 32 characters).</li> <li>Name -- the project display name (up to 32 characters).</li>
<li>Startup Form -- which form to show automatically when the program starts.</li> <li>Startup Form -- which form to show automatically when the program starts.</li>
<li>Metadata -- Author, Company, Version, Copyright, Description, and Icon Path (for compiled binaries).</li> <li>Metadata -- Author, Company, Version, Copyright, Description, and Icon Path (for compiled binaries).</li>
<li>File list -- relative paths of all .bas and .frm files in the project. Each entry tracks whether it is a form file.</li>
</ul> </ul>
<p>File list -- relative paths of all .bas and .frm files in the project. Each entry tracks whether it is a form file.</p>
<h2>How Projects are Compiled</h2> <h2>How Projects are Compiled</h2>
<p>When the project is compiled, all source files are concatenated into a single source stream. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form.</p> <p>When the project is compiled, all source files are concatenated into a single source stream. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form.</p>
<h2>Compile-Time Validation</h2> <h2>Compile-Time Validation</h2>
@ -863,8 +863,8 @@ img { max-width: 100%; }
<ul> <ul>
<li>Unknown widget types -- any Begin TypeName ... End block whose TypeName does not match a registered widget is flagged. The error message names the offending form, control, and type, and hints that DVX uses VB6-style names (SpinButton, not Spinner).</li> <li>Unknown widget types -- any Begin TypeName ... End block whose TypeName does not match a registered widget is flagged. The error message names the offending form, control, and type, and hints that DVX uses VB6-style names (SpinButton, not Spinner).</li>
<li>Unknown properties -- statements of the form CtrlName.Property = value, or reads of CtrlName.Property, are checked against the widget's declared property list. A typo such as btn.Captoin triggers a compile error with the form, control, and property name.</li> <li>Unknown properties -- statements of the form CtrlName.Property = value, or reads of CtrlName.Property, are checked against the widget's declared property list. A typo such as btn.Captoin triggers a compile error with the form, control, and property name.</li>
<li>Unknown methods -- method calls CtrlName.Method ... are checked the same way against the widget's method list, catching mistakes like list.AddTiem before the program ever runs.</li>
</ul> </ul>
<p>Unknown methods -- method calls CtrlName.Method ... are checked the same way against the widget's method list, catching mistakes like list.AddTiem before the program ever runs.</p>
<p>These checks use the same metadata the Properties panel and Toolbox read from each widget DXE, so property and method names always match the current widget set without any separate list to maintain.</p> <p>These checks use the same metadata the Properties panel and Toolbox read from each widget DXE, so property and method names always match the current widget set without any separate list to maintain.</p>
<h2>Project Operations</h2> <h2>Project Operations</h2>
<pre> Operation Description <pre> Operation Description
@ -885,8 +885,8 @@ img { max-width: 100%; }
<p>The Properties panel (Window &gt; Properties) has two sections:</p> <p>The Properties panel (Window &gt; Properties) has two sections:</p>
<ul> <ul>
<li>Control tree -- a TreeView at the top listing the form and all its controls in layout order. Click a control name to select it in both the Properties panel and the Form Designer. Drag items in the tree to reorder controls in the form's layout.</li> <li>Control tree -- a TreeView at the top listing the form and all its controls in layout order. Click a control name to select it in both the Properties panel and the Form Designer. Drag items in the tree to reorder controls in the form's layout.</li>
<li>Property list -- a two-column ListView below the tree showing property names and values for the selected control. Double-click a property value to edit it via an InputBox dialog. Changes take effect immediately in the designer preview.</li>
</ul> </ul>
<p>Property list -- a two-column ListView below the tree showing property names and values for the selected control. Double-click a property value to edit it via an InputBox dialog. Changes take effect immediately in the designer preview.</p>
<p>Each control type exposes different properties (e.g., Caption, Text, Width, Height, MaxWidth, MaxHeight, Weight, Alignment, Enabled, Visible, and type-specific properties like DataSource and DataField for data-bound controls).</p> <p>Each control type exposes different properties (e.g., Caption, Text, Width, Height, MaxWidth, MaxHeight, Weight, Alignment, Enabled, Visible, and type-specific properties like DataSource and DataField for data-bound controls).</p>
<p><a href="#ide.designer">Form Designer</a></p> <p><a href="#ide.designer">Form Designer</a></p>
<p><a href="#ide.overview">Back to Overview</a></p> <p><a href="#ide.overview">Back to Overview</a></p>
@ -945,29 +945,29 @@ img { max-width: 100%; }
<ul> <ul>
<li>Shift+F5 (Debug) -- compiles the project and starts execution in debug mode. Breakpoints are active but execution does not pause at the first statement.</li> <li>Shift+F5 (Debug) -- compiles the project and starts execution in debug mode. Breakpoints are active but execution does not pause at the first statement.</li>
<li>F8 (Step Into) -- if idle, starts a debug session and breaks at the first statement.</li> <li>F8 (Step Into) -- if idle, starts a debug session and breaks at the first statement.</li>
<li>F5 (Run) -- compiles and runs without the debugger. No breakpoints are active. If already paused, resumes execution with debugging disabled.</li>
</ul> </ul>
<p>F5 (Run) -- compiles and runs without the debugger. No breakpoints are active. If already paused, resumes execution with debugging disabled.</p>
<h2>Breakpoints</h2> <h2>Breakpoints</h2>
<h3>Setting Breakpoints</h3> <h3>Setting Breakpoints</h3>
<ul> <ul>
<li>Press F9 to toggle a breakpoint on the current editor line.</li> <li>Press F9 to toggle a breakpoint on the current editor line.</li>
<li>Click in the line number gutter to toggle a breakpoint on that line.</li>
</ul> </ul>
<p>Click in the line number gutter to toggle a breakpoint on that line.</p>
<h3>Breakpoint Validation</h3> <h3>Breakpoint Validation</h3>
<p>Not every line can have a breakpoint. The IDE validates the line content and silently refuses to set breakpoints on:</p> <p>Not every line can have a breakpoint. The IDE validates the line content and silently refuses to set breakpoints on:</p>
<ul> <ul>
<li>Blank lines</li> <li>Blank lines</li>
<li>Comment lines (' or REM)</li> <li>Comment lines (' or REM)</li>
<li>SUB and FUNCTION declaration lines</li> <li>SUB and FUNCTION declaration lines</li>
<li>END SUB and END FUNCTION lines</li>
</ul> </ul>
<p>END SUB and END FUNCTION lines</p>
<h3>Breakpoint Storage</h3> <h3>Breakpoint Storage</h3>
<p>Each breakpoint records the file it belongs to, the line number within that file, and the procedure name (Object.Event). Breakpoints are stored in the project and survive compilation.</p> <p>Each breakpoint records the file it belongs to, the line number within that file, and the procedure name (Object.Event). Breakpoints are stored in the project and survive compilation.</p>
<h3>Visual Indicators</h3> <h3>Visual Indicators</h3>
<ul> <ul>
<li>Breakpoint lines show a red dot in the gutter.</li> <li>Breakpoint lines show a red dot in the gutter.</li>
<li>The current debug line (when paused) has a yellow background.</li>
</ul> </ul>
<p>The current debug line (when paused) has a yellow background.</p>
<h3>Breakpoint Adjustment on Edit</h3> <h3>Breakpoint Adjustment on Edit</h3>
<p>When lines are added or removed in the editor, breakpoints below the edit point are automatically shifted to stay on the correct line.</p> <p>When lines are added or removed in the editor, breakpoints below the edit point are automatically shifted to stay on the correct line.</p>
<h2>Stepping</h2> <h2>Stepping</h2>
@ -983,8 +983,8 @@ img { max-width: 100%; }
<li>The program runs in short cooperative slices so the IDE remains responsive.</li> <li>The program runs in short cooperative slices so the IDE remains responsive.</li>
<li>When the program hits a breakpoint, execution pauses immediately. The IDE switches to Code View, navigates to the breakpoint line, highlights it in yellow, and opens the Locals and Call Stack windows if they are not already visible.</li> <li>When the program hits a breakpoint, execution pauses immediately. The IDE switches to Code View, navigates to the breakpoint line, highlights it in yellow, and opens the Locals and Call Stack windows if they are not already visible.</li>
<li>While paused, you can inspect variables in Locals and Watch, evaluate expressions in the Immediate window, assign new values to variables, toggle breakpoints, step, continue, or stop.</li> <li>While paused, you can inspect variables in Locals and Watch, evaluate expressions in the Immediate window, assign new values to variables, toggle breakpoints, step, continue, or stop.</li>
<li>Resuming (F5, Shift+F5, or any Step command) returns the program to Running state.</li>
</ul> </ul>
<p>Resuming (F5, Shift+F5, or any Step command) returns the program to Running state.</p>
<h2>Stopping</h2> <h2>Stopping</h2>
<p>Press Esc or click the Stop toolbar button at any time to halt execution. The program is terminated, the debugger returns to Idle, and the IDE restores any designer or code windows that were hidden at the start of the run.</p> <p>Press Esc or click the Stop toolbar button at any time to halt execution. The program is terminated, the debugger returns to Idle, and the IDE restores any designer or code windows that were hidden at the start of the run.</p>
<p><a href="#ide.debug.locals">Locals Window</a></p> <p><a href="#ide.debug.locals">Locals Window</a></p>
@ -1006,8 +1006,8 @@ img { max-width: 100%; }
<ul> <ul>
<li>Local variables for the current procedure (matched by proc index).</li> <li>Local variables for the current procedure (matched by proc index).</li>
<li>Global (module-level) variables.</li> <li>Global (module-level) variables.</li>
<li>Form-scoped variables for the current form (if the program is executing within a form context).</li>
</ul> </ul>
<p>Form-scoped variables for the current form (if the program is executing within a form context).</p>
<p>Up to 64 variables are displayed. The window is resizable.</p> <p>Up to 64 variables are displayed. The window is resizable.</p>
<p><a href="#ide.debug.callstack">Call Stack Window</a></p> <p><a href="#ide.debug.callstack">Call Stack Window</a></p>
<p><a href="#ide.overview">Back to Overview</a></p> <p><a href="#ide.overview">Back to Overview</a></p>
@ -1039,13 +1039,13 @@ img { max-width: 100%; }
<li>Array subscripts: arr(5), matrix(2, 3)</li> <li>Array subscripts: arr(5), matrix(2, 3)</li>
<li>UDT field access: player.name</li> <li>UDT field access: player.name</li>
<li>Combined: items(i).price</li> <li>Combined: items(i).price</li>
<li>Arbitrary BASIC expressions (compiled and evaluated against the paused VM's state): x + y * 2, Len(name$)</li>
</ul> </ul>
<p>Arbitrary BASIC expressions (compiled and evaluated against the paused VM's state): x + y * 2, Len(name$)</p>
<h2>Editing and Deleting</h2> <h2>Editing and Deleting</h2>
<ul> <ul>
<li>Double-click or press Enter on a watch entry to move it back into the input box for editing.</li> <li>Double-click or press Enter on a watch entry to move it back into the input box for editing.</li>
<li>Press Delete to remove the selected watch expression.</li>
</ul> </ul>
<p>Press Delete to remove the selected watch expression.</p>
<p><a href="#ide.debug.breakpoints">Breakpoints Window</a></p> <p><a href="#ide.debug.breakpoints">Breakpoints Window</a></p>
<p><a href="#ide.overview">Back to Overview</a></p> <p><a href="#ide.overview">Back to Overview</a></p>
</div> </div>
@ -1059,8 +1059,8 @@ img { max-width: 100%; }
Line Code line number within the file.</pre> Line Code line number within the file.</pre>
<ul> <ul>
<li>Double-click a breakpoint to navigate the code editor to that location.</li> <li>Double-click a breakpoint to navigate the code editor to that location.</li>
<li>Press Delete to remove selected breakpoints (multi-select is supported).</li>
</ul> </ul>
<p>Press Delete to remove selected breakpoints (multi-select is supported).</p>
<p><a href="#ide.debugger">Debugger</a></p> <p><a href="#ide.debugger">Debugger</a></p>
<p><a href="#ide.overview">Back to Overview</a></p> <p><a href="#ide.overview">Back to Overview</a></p>
</div> </div>
@ -1073,8 +1073,8 @@ img { max-width: 100%; }
<ul> <ul>
<li>PRINT x * 2 -- evaluate and print an expression.</li> <li>PRINT x * 2 -- evaluate and print an expression.</li>
<li>DIM tmp As Integer -- declare a temporary variable.</li> <li>DIM tmp As Integer -- declare a temporary variable.</li>
<li>LET x = 42 -- explicit assignment (see below).</li>
</ul> </ul>
<p>LET x = 42 -- explicit assignment (see below).</p>
<p>Parse or runtime errors are displayed inline with an Error: prefix.</p> <p>Parse or runtime errors are displayed inline with an Error: prefix.</p>
<h2>Inspecting Variables While Paused</h2> <h2>Inspecting Variables While Paused</h2>
<p>When the debugger is paused at a breakpoint, the Immediate window has access to the running program's state. Expressions like count or name$ &amp; &quot; test&quot; display live values.</p> <p>When the debugger is paused at a breakpoint, the Immediate window has access to the running program's state. Expressions like count or name$ &amp; &quot; test&quot; display live values.</p>
@ -1088,8 +1088,8 @@ img { max-width: 100%; }
<li>Scalar variables -- x = 42, name$ = &quot;test&quot;</li> <li>Scalar variables -- x = 42, name$ = &quot;test&quot;</li>
<li>Array elements -- arr(5) = 100, matrix(2, 3) = 7.5</li> <li>Array elements -- arr(5) = 100, matrix(2, 3) = 7.5</li>
<li>UDT fields -- player.score = 1000</li> <li>UDT fields -- player.score = 1000</li>
<li>Combined -- items(0).price = 9.99</li>
</ul> </ul>
<p>Combined -- items(0).price = 9.99</p>
<p>The new value is written directly into the running program's variable (local, global, or form scope). A confirmation message is displayed, and the Locals and Watch windows update automatically to reflect the change.</p> <p>The new value is written directly into the running program's variable (local, global, or form scope). A confirmation message is displayed, and the Locals and Watch windows update automatically to reflect the change.</p>
<p>If the assignment target cannot be resolved (unknown variable, out-of-bounds index, wrong type), an error message is displayed.</p> <p>If the assignment target cannot be resolved (unknown variable, out-of-bounds index, wrong type), an error message is displayed.</p>
<p><a href="#ide.overview">Back to Overview</a></p> <p><a href="#ide.overview">Back to Overview</a></p>
@ -1100,8 +1100,8 @@ img { max-width: 100%; }
<ul> <ul>
<li>PRINT output -- all PRINT statement output from the running program is appended here.</li> <li>PRINT output -- all PRINT statement output from the running program is appended here.</li>
<li>Runtime errors -- if the VM encounters a runtime error (division by zero, out-of-bounds, etc.), the error message and line number are displayed in the output with an Error on line N: prefix.</li> <li>Runtime errors -- if the VM encounters a runtime error (division by zero, out-of-bounds, etc.), the error message and line number are displayed in the output with an Error on line N: prefix.</li>
<li>Compile errors -- if compilation fails, the error message and location are shown.</li>
</ul> </ul>
<p>Compile errors -- if compilation fails, the error message and location are shown.</p>
<p>The output buffer holds up to 32,768 characters. Use Run &gt; Clear Output to clear it.</p> <p>The output buffer holds up to 32,768 characters. Use Run &gt; Clear Output to clear it.</p>
<p>INPUT statements prompt the user via a modal InputBox dialog; the prompt text is also echoed to the Output window.</p> <p>INPUT statements prompt the user via a modal InputBox dialog; the prompt text is also echoed to the Output window.</p>
<p><a href="#ide.overview">Back to Overview</a></p> <p><a href="#ide.overview">Back to Overview</a></p>
@ -1606,13 +1606,13 @@ PRINT USING format$; expression [; expression] ...</code></pre>
<li>; between items -- no separator (items appear next to each other, or separated by a space for numbers)</li> <li>; between items -- no separator (items appear next to each other, or separated by a space for numbers)</li>
<li>, between items -- advance to the next 14-column tab zone</li> <li>, between items -- advance to the next 14-column tab zone</li>
<li>A trailing ; or , suppresses the newline normally written at the end of the statement</li> <li>A trailing ; or , suppresses the newline normally written at the end of the statement</li>
<li>? is an alias for PRINT</li>
</ul> </ul>
<p>? is an alias for PRINT</p>
<p>Special functions inside PRINT:</p> <p>Special functions inside PRINT:</p>
<ul> <ul>
<li>SPC(n) -- print n spaces</li> <li>SPC(n) -- print n spaces</li>
<li>TAB(n) -- advance to column n (first column is 1)</li>
</ul> </ul>
<p>TAB(n) -- advance to column n (first column is 1)</p>
<pre><code>Print &quot;Name:&quot;; Tab(20); name$ <pre><code>Print &quot;Name:&quot;; Tab(20); name$
Print &quot;X=&quot;; x, &quot;Y=&quot;; y ' comma = next 14-column zone Print &quot;X=&quot;; x, &quot;Y=&quot;; y ' comma = next 14-column zone
Print #1, &quot;Written to file&quot;</code></pre> Print #1, &quot;Written to file&quot;</code></pre>
@ -2289,14 +2289,14 @@ End If</code></pre>
<li>Place one or more display/edit controls (TextBox, Label, etc.) and set their DataSource to the Data control's name and DataField to a column name.</li> <li>Place one or more display/edit controls (TextBox, Label, etc.) and set their DataSource to the Data control's name and DataField to a column name.</li>
<li>When the form loads, the Data control auto-refreshes: it opens the database, runs the query, and navigates to the first record.</li> <li>When the form loads, the Data control auto-refreshes: it opens the database, runs the query, and navigates to the first record.</li>
<li>Bound controls are updated automatically each time the Data control repositions (the Reposition event fires, and the runtime pushes the current record's field values into all bound controls).</li> <li>Bound controls are updated automatically each time the Data control repositions (the Reposition event fires, and the runtime pushes the current record's field values into all bound controls).</li>
<li>When a bound control loses focus (LostFocus), its current text is written back to the Data control's record cache, and Update is called automatically to persist changes.</li>
</ul> </ul>
<p>When a bound control loses focus (LostFocus), its current text is written back to the Data control's record cache, and Update is called automatically to persist changes.</p>
<h2>Master-Detail Binding</h2> <h2>Master-Detail Binding</h2>
<p>For hierarchical data (e.g. orders and order items), use two Data controls:</p> <p>For hierarchical data (e.g. orders and order items), use two Data controls:</p>
<ul> <ul>
<li>A master Data control bound to the parent table.</li> <li>A master Data control bound to the parent table.</li>
<li>A detail Data control with its MasterSource set to the master's name, MasterField set to the key column in the master, and DetailField set to the foreign key column in the detail table.</li>
</ul> </ul>
<p>A detail Data control with its MasterSource set to the master's name, MasterField set to the key column in the master, and DetailField set to the foreign key column in the detail table.</p>
<p>When the master record changes, the detail Data control automatically re-queries using the master's current value for filtering. All controls bound to the detail are refreshed.</p> <p>When the master record changes, the detail Data control automatically re-queries using the master's current value for filtering. All controls bound to the detail are refreshed.</p>
<h2>DBGrid Binding</h2> <h2>DBGrid Binding</h2>
<p>Set the DBGrid's DataSource to a Data control name. The grid auto-populates columns from the query results and refreshes whenever the Data control refreshes.</p> <p>Set the DBGrid's DataSource to a Data control name. The grid auto-populates columns from the query results and refreshes whenever the Data control refreshes.</p>
@ -2384,8 +2384,8 @@ End</code></pre>
<ul> <ul>
<li>Level 0: top-level menu bar headers (e.g. &quot;File&quot;, &quot;Edit&quot;).</li> <li>Level 0: top-level menu bar headers (e.g. &quot;File&quot;, &quot;Edit&quot;).</li>
<li>Level 1: items within a top-level menu.</li> <li>Level 1: items within a top-level menu.</li>
<li>Level 2+: submenu items.</li>
</ul> </ul>
<p>Level 2+: submenu items.</p>
<p>A level-0 menu that contains children becomes a top-level menu header. A non-level-0 menu that contains children becomes a submenu.</p> <p>A level-0 menu that contains children becomes a top-level menu header. A non-level-0 menu that contains children becomes a submenu.</p>
<h2>Event Dispatch</h2> <h2>Event Dispatch</h2>
<p>Each clickable menu item (not headers, not separators) receives a unique numeric ID at load time. When clicked, the form's onMenu handler maps the ID to the menu item's name and fires MenuName_Click.</p> <p>Each clickable menu item (not headers, not separators) receives a unique numeric ID at load time. When clicked, the form's onMenu handler maps the ID to the menu item's name and fires MenuName_Click.</p>
@ -2477,8 +2477,8 @@ End Sub</code></pre>
<li>Properties are assigned as Key = Value. String values are optionally quoted.</li> <li>Properties are assigned as Key = Value. String values are optionally quoted.</li>
<li>Everything after the form's closing End is BASIC source code.</li> <li>Everything after the form's closing End is BASIC source code.</li>
<li>Comments in the form section use ' (single quote).</li> <li>Comments in the form section use ' (single quote).</li>
<li>Blank lines are ignored in the form section.</li>
</ul> </ul>
<p>Blank lines are ignored in the form section.</p>
<h2>Common FRM Properties</h2> <h2>Common FRM Properties</h2>
<pre> Property Applies To Description <pre> Property Applies To Description
----------------------- --------------- ------------------------------------------- ----------------------- --------------- -------------------------------------------

View file

@ -63,8 +63,8 @@ img { max-width: 100%; }
<li>Click underlined links in the content to jump to other topics</li> <li>Click underlined links in the content to jump to other topics</li>
<li>Use the Back and Forward buttons (or Navigate menu) to retrace your steps</li> <li>Use the Back and Forward buttons (or Navigate menu) to retrace your steps</li>
<li>Use Navigate &gt; Index to browse an alphabetical keyword list</li> <li>Use Navigate &gt; Index to browse an alphabetical keyword list</li>
<li>Use Navigate &gt; Search to find topics by keyword</li>
</ul> </ul>
<p>Use Navigate &gt; Search to find topics by keyword</p>
<h2>Keyboard Shortcuts</h2> <h2>Keyboard Shortcuts</h2>
<pre> Alt+Left Back <pre> Alt+Left Back
Alt+Right Forward Alt+Right Forward

View file

@ -893,8 +893,8 @@ img { max-width: 100%; }
<li>A dirty-rectangle compositor that minimises LFB traffic.</li> <li>A dirty-rectangle compositor that minimises LFB traffic.</li>
<li>A window manager with Motif-style chrome, drag/resize, menus, and scrollbars.</li> <li>A window manager with Motif-style chrome, drag/resize, menus, and scrollbars.</li>
<li>A retained-mode widget toolkit with automatic layout.</li> <li>A retained-mode widget toolkit with automatic layout.</li>
<li>Modal dialog helpers, an INI preferences system, a DXE resource system, and per-app memory tracking.</li>
</ul> </ul>
<p>Modal dialog helpers, an INI preferences system, a DXE resource system, and per-app memory tracking.</p>
<p>Applications written in C link against libdvx (and the widget DXEs they need) to build native DVX programs. Applications written in DVX BASIC compile to bytecode that runs on top of libdvx via the form runtime.</p> <p>Applications written in C link against libdvx (and the widget DXEs they need) to build native DVX programs. Applications written in DVX BASIC compile to bytecode that runs on top of libdvx via the form runtime.</p>
<h2>Target Audience</h2> <h2>Target Audience</h2>
<p>This document is aimed at developers writing native C code for DVX:</p> <p>This document is aimed at developers writing native C code for DVX:</p>
@ -902,14 +902,14 @@ img { max-width: 100%; }
<li>System-level contributors maintaining libdvx itself.</li> <li>System-level contributors maintaining libdvx itself.</li>
<li>Widget authors writing new .wgt DXE modules.</li> <li>Widget authors writing new .wgt DXE modules.</li>
<li>Application authors writing .app DXE modules.</li> <li>Application authors writing .app DXE modules.</li>
<li>Tool authors (e.g. the BASIC compiler/runtime) that sit on top of libdvx.</li>
</ul> </ul>
<p>Tool authors (e.g. the BASIC compiler/runtime) that sit on top of libdvx.</p>
<p>All examples and signatures assume the DJGPP cross-compiler toolchain.</p> <p>All examples and signatures assume the DJGPP cross-compiler toolchain.</p>
<h2>What's Covered</h2> <h2>What's Covered</h2>
<ul> <ul>
<li>Architecture -- Five-layer model, display pipeline, event model, build system.</li> <li>Architecture -- Five-layer model, display pipeline, event model, build system.</li>
<li>API Reference -- Every public function, struct, enum, and constant documented with parameters and return values.</li>
</ul> </ul>
<p>API Reference -- Every public function, struct, enum, and constant documented with parameters and return values.</p>
<p>Use the table of contents on the left to navigate. The API reference is organised by header file; each function has a one-line summary, parameter table, and (where useful) a working example.</p> <p>Use the table of contents on the left to navigate. The API reference is organised by header file; each function has a one-line summary, parameter table, and (where useful) a working example.</p>
<h2>Conventions</h2> <h2>Conventions</h2>
<ul> <ul>
@ -917,8 +917,8 @@ img { max-width: 100%; }
<li>Enum types end in a capital E (e.g. ColorIdE, WallpaperModeE, ScrollbarOrientE).</li> <li>Enum types end in a capital E (e.g. ColorIdE, WallpaperModeE, ScrollbarOrientE).</li>
<li>Public functions use camelCase and prefixes that identify the subsystem: dvx* (application), wm* (window manager), wgt* (widget), prefs* (preferences), draw* / rect* (drawing), dirtyList* / flush* (compositor), video* / packColor / setClipRect (video), platform* (platform layer).</li> <li>Public functions use camelCase and prefixes that identify the subsystem: dvx* (application), wm* (window manager), wgt* (widget), prefs* (preferences), draw* / rect* (drawing), dirtyList* / flush* (compositor), video* / packColor / setClipRect (video), platform* (platform layer).</li>
<li>Constants use SCREAMING_SNAKE_CASE (e.g. HIT_CONTENT, MB_OK, CURSOR_ARROW).</li> <li>Constants use SCREAMING_SNAKE_CASE (e.g. HIT_CONTENT, MB_OK, CURSOR_ARROW).</li>
<li>Every header uses stdint.h types (int32_t, uint8_t) and stdbool.h types (bool).</li>
</ul> </ul>
<p>Every header uses stdint.h types (int32_t, uint8_t) and stdbool.h types (bool).</p>
<h2>Getting Started</h2> <h2>Getting Started</h2>
<p>If you're new to DVX, read these topics in order:</p> <p>If you're new to DVX, read these topics in order:</p>
<ul> <ul>
@ -926,8 +926,8 @@ img { max-width: 100%; }
<li>Display Pipeline -- How the backbuffer, dirty list, and compositor work together.</li> <li>Display Pipeline -- How the backbuffer, dirty list, and compositor work together.</li>
<li>Event Model -- How input becomes window/widget callbacks.</li> <li>Event Model -- How input becomes window/widget callbacks.</li>
<li>Widget System -- The retained-mode toolkit layered on the window manager.</li> <li>Widget System -- The retained-mode toolkit layered on the window manager.</li>
<li>DXE Module System -- How apps and widgets are loaded dynamically.</li>
</ul> </ul>
<p>DXE Module System -- How apps and widgets are loaded dynamically.</p>
<p>Then dip into the API Reference to find specific functions. The public entry point for any application is dvxInit / dvxRun in dvxApp.h.</p> <p>Then dip into the API Reference to find specific functions. The public entry point for any application is dvxInit / dvxRun in dvxApp.h.</p>
<h2>License</h2> <h2>License</h2>
<p>DVX is distributed under the MIT License (see the copyright notice at the top of every source file). Third-party code (stb_image, stb_ds, stb_image_write in thirdparty/) is used under its own permissive license.</p> <p>DVX is distributed under the MIT License (see the copyright notice at the top of every source file). Third-party code (stb_image, stb_ds, stb_image_write in thirdparty/) is used under its own permissive license.</p>
@ -942,8 +942,8 @@ img { max-width: 100%; }
<li>486 baseline -- all hot paths are written to be fast on a 486, with Pentium-specific paths where the gain is significant.</li> <li>486 baseline -- all hot paths are written to be fast on a 486, with Pentium-specific paths where the gain is significant.</li>
<li>Single-tasking cooperative model -- applications yield the CPU via tsYield(); there is no preemptive scheduler.</li> <li>Single-tasking cooperative model -- applications yield the CPU via tsYield(); there is no preemptive scheduler.</li>
<li>A single emulator is the trusted reference platform for testing; any bugs observed there are treated as DVX bugs.</li> <li>A single emulator is the trusted reference platform for testing; any bugs observed there are treated as DVX bugs.</li>
<li>No external font or cursor files -- all bitmaps are compiled in as static const data.</li>
</ul> </ul>
<p>No external font or cursor files -- all bitmaps are compiled in as static const data.</p>
<p>The runtime environment consists of a bootstrap loader (dvx.exe) that loads core DXE libraries, widget plugins, and the shell, which in turn loads and manages DXE application modules.</p> <p>The runtime environment consists of a bootstrap loader (dvx.exe) that loads core DXE libraries, widget plugins, and the shell, which in turn loads and manages DXE application modules.</p>
<h2>Contents</h2> <h2>Contents</h2>
<p><a href="#arch.layers">Five-Layer Architecture</a></p> <p><a href="#arch.layers">Five-Layer Architecture</a></p>
@ -1038,8 +1038,8 @@ img { max-width: 100%; }
<li>A full 640x480x32bpp frame is 1.2 MB -- far too much to flush every frame over a slow PCI bus.</li> <li>A full 640x480x32bpp frame is 1.2 MB -- far too much to flush every frame over a slow PCI bus.</li>
<li>A typical dirty region during normal interaction (typing, menu open) is a few KB.</li> <li>A typical dirty region during normal interaction (typing, menu open) is a few KB.</li>
<li>Merging overlapping dirty rects into larger rects reduces per-rect overhead and improves bus utilization.</li> <li>Merging overlapping dirty rects into larger rects reduces per-rect overhead and improves bus utilization.</li>
<li>Per-window content buffers persist across frames, so windows don't repaint on expose -- only when their own content changes.</li>
</ul> </ul>
<p>Per-window content buffers persist across frames, so windows don't repaint on expose -- only when their own content changes.</p>
</div> </div>
<div class="topic" id="arch.windows"> <div class="topic" id="arch.windows">
<h1>Window System</h1> <h1>Window System</h1>
@ -1058,8 +1058,8 @@ img { max-width: 100%; }
<ul> <ul>
<li>Back-to-front iteration for painting (painter's algorithm).</li> <li>Back-to-front iteration for painting (painter's algorithm).</li>
<li>Front-to-back iteration for hit testing (first hit wins).</li> <li>Front-to-back iteration for hit testing (first hit wins).</li>
<li>Reordering by pointer swap (no copying of large WindowT structs).</li>
</ul> </ul>
<p>Reordering by pointer swap (no copying of large WindowT structs).</p>
<p>Only one drag/resize/scroll operation can be active system-wide at a time (single mouse), so that state lives on the stack, not on individual windows.</p> <p>Only one drag/resize/scroll operation can be active system-wide at a time (single mouse), so that state lives on the stack, not on individual windows.</p>
<h2>Chrome Layout</h2> <h2>Chrome Layout</h2>
<pre><code> +-------------------------------------------+ <pre><code> +-------------------------------------------+
@ -1104,8 +1104,8 @@ img { max-width: 100%; }
<p>Two-pass flexbox-like algorithm:</p> <p>Two-pass flexbox-like algorithm:</p>
<ul> <ul>
<li>Bottom-up (calcMinSize) -- compute minimum sizes for every widget, starting from leaves.</li> <li>Bottom-up (calcMinSize) -- compute minimum sizes for every widget, starting from leaves.</li>
<li>Top-down (layout) -- allocate space within available bounds, distributing extra space according to weight values (0 = fixed, 100 = normal stretch).</li>
</ul> </ul>
<p>Top-down (layout) -- allocate space within available bounds, distributing extra space according to weight values (0 = fixed, 100 = normal stretch).</p>
<p>Size hints use a tagged encoding: the top 2 bits of an int32_t select the unit (pixels, character widths, or percentage of parent), the low 30 bits hold the value. Macros: wgtPixels(v), wgtChars(v), wgtPercent(v).</p> <p>Size hints use a tagged encoding: the top 2 bits of an int32_t select the unit (pixels, character widths, or percentage of parent), the low 30 bits hold the value. Macros: wgtPixels(v), wgtChars(v), wgtPercent(v).</p>
<h2>Widget Class Dispatch (WidgetClassT)</h2> <h2>Widget Class Dispatch (WidgetClassT)</h2>
<p>Each widget type provides a WidgetClassT with a handlers[] array indexed by stable method IDs. Method IDs are never reordered or reused -- new methods append at the end. This provides ABI-stable dispatch so that widget DXEs compiled against an older DVX version continue to work.</p> <p>Each widget type provides a WidgetClassT with a handlers[] array indexed by stable method IDs. Method IDs are never reordered or reused -- new methods append at the end. This provides ABI-stable dispatch so that widget DXEs compiled against an older DVX version continue to work.</p>
@ -1280,8 +1280,8 @@ prefsClose(h);</code></pre>
<li>Poll keyboard -- platformKeyboardRead() returns ASCII + scancode. Non-blocking; returns false if buffer is empty.</li> <li>Poll keyboard -- platformKeyboardRead() returns ASCII + scancode. Non-blocking; returns false if buffer is empty.</li>
<li>Dispatch to focused window -- the event loop fires window callbacks (onKey, onMouse, etc.) on the focused window. If the window has a widget tree, the widget system's installed handlers dispatch to individual widgets.</li> <li>Dispatch to focused window -- the event loop fires window callbacks (onKey, onMouse, etc.) on the focused window. If the window has a widget tree, the widget system's installed handlers dispatch to individual widgets.</li>
<li>Compositor pass -- merge dirty rects, composite, flush to LFB.</li> <li>Compositor pass -- merge dirty rects, composite, flush to LFB.</li>
<li>Yield -- platformYield() or idle callback.</li>
</ul> </ul>
<p>Yield -- platformYield() or idle callback.</p>
<h2>Event Dispatch Chain</h2> <h2>Event Dispatch Chain</h2>
<pre><code> Mouse/Keyboard Input <pre><code> Mouse/Keyboard Input
| |
@ -1324,8 +1324,8 @@ prefsClose(h);</code></pre>
<li>Glyph lookup is a single array index.</li> <li>Glyph lookup is a single array index.</li>
<li>Each scanline of a glyph is exactly one byte (1bpp at 8 pixels wide).</li> <li>Each scanline of a glyph is exactly one byte (1bpp at 8 pixels wide).</li>
<li>No glyph-width tables, kerning, or per-character positioning needed.</li> <li>No glyph-width tables, kerning, or per-character positioning needed.</li>
<li>8-pixel width aligns with byte boundaries -- no bit shifting in per-scanline rendering.</li>
</ul> </ul>
<p>8-pixel width aligns with byte boundaries -- no bit shifting in per-scanline rendering.</p>
<h2>Text Rendering Functions</h2> <h2>Text Rendering Functions</h2>
<p>drawChar() -- Renders a single character. Supports opaque (background fill) and transparent modes.</p> <p>drawChar() -- Renders a single character. Supports opaque (background fill) and transparent modes.</p>
<p>drawTextN() -- Optimized batch rendering for a known character count. Clips once for the entire run, fills background in a single rectFill, then overlays glyph foreground pixels. Significantly faster than per-character rendering for long runs.</p> <p>drawTextN() -- Optimized batch rendering for a known character count. Clips once for the entire run, fills background in a single rectFill, then overlays glyph foreground pixels. Significantly faster than per-character rendering for long runs.</p>
@ -1356,8 +1356,8 @@ prefsClose(h);</code></pre>
<li>menuBg/Fg, menuHighlightBg/Fg -- menus</li> <li>menuBg/Fg, menuHighlightBg/Fg -- menus</li>
<li>buttonFace -- button background</li> <li>buttonFace -- button background</li>
<li>scrollbarBg/Fg/Trough -- scrollbar components</li> <li>scrollbarBg/Fg/Trough -- scrollbar components</li>
<li>cursorFg/Bg -- mouse cursor colors</li>
</ul> </ul>
<p>cursorFg/Bg -- mouse cursor colors</p>
<p>Source RGB values are kept in AppContextT.colorRgb[] for theme save/load. Themes are stored as INI files with a [colors] section. The API provides dvxLoadTheme(), dvxSaveTheme(), dvxSetColor(), and dvxResetColorScheme().</p> <p>Source RGB values are kept in AppContextT.colorRgb[] for theme save/load. Themes are stored as INI files with a [colors] section. The API provides dvxLoadTheme(), dvxSaveTheme(), dvxSetColor(), and dvxResetColorScheme().</p>
<h2>Bevel Styles</h2> <h2>Bevel Styles</h2>
<p>Bevels are the defining visual element of the Motif aesthetic. Convenience macros create bevel style descriptors by swapping highlight and shadow colors:</p> <p>Bevels are the defining visual element of the Motif aesthetic. Convenience macros create bevel style descriptors by swapping highlight and shadow colors:</p>
@ -1428,8 +1428,8 @@ prefsClose(h);</code></pre>
<li>Verifies critical outputs exist (dvx.exe, libtasks.lib, libdvx.lib, dvxshell.lib).</li> <li>Verifies critical outputs exist (dvx.exe, libtasks.lib, libdvx.lib, dvxshell.lib).</li>
<li>Counts widget modules.</li> <li>Counts widget modules.</li>
<li>Creates an ISO 9660 image from bin/ using mkisofs: -iso-level 1 (strict 8.3 filenames for DOS), -J (Joliet extensions for long names), -V DVX (volume label).</li> <li>Creates an ISO 9660 image from bin/ using mkisofs: -iso-level 1 (strict 8.3 filenames for DOS), -J (Joliet extensions for long names), -V DVX (volume label).</li>
<li>Places the ISO at the target emulator's CD-ROM mount path.</li>
</ul> </ul>
<p>Places the ISO at the target emulator's CD-ROM mount path.</p>
<h2>Compiler Flags</h2> <h2>Compiler Flags</h2>
<pre><code> -O2 Optimization level 2 <pre><code> -O2 Optimization level 2
-march=i486 486 instruction set baseline -march=i486 486 instruction set baseline
@ -1492,8 +1492,8 @@ prefsClose(h);</code></pre>
<li>dvxMem.h -- Per-app memory tracking</li> <li>dvxMem.h -- Per-app memory tracking</li>
<li>dvxWgt.h -- Widget system public API</li> <li>dvxWgt.h -- Widget system public API</li>
<li>dvxWgtP.h -- Widget plugin API</li> <li>dvxWgtP.h -- Widget plugin API</li>
<li>platform/dvxPlat.h -- Platform abstraction layer</li>
</ul> </ul>
<p>platform/dvxPlat.h -- Platform abstraction layer</p>
<p><a href="#api.types">dvxTypes.h -- Shared Type Definitions</a></p> <p><a href="#api.types">dvxTypes.h -- Shared Type Definitions</a></p>
<p><a href="#api.cursor">dvxCur.h -- Cursor Definitions</a></p> <p><a href="#api.cursor">dvxCur.h -- Cursor Definitions</a></p>
<p><a href="#api.font">dvxFont.h -- Bitmap Font Data</a></p> <p><a href="#api.font">dvxFont.h -- Bitmap Font Data</a></p>
@ -1795,8 +1795,8 @@ prefsClose(h);</code></pre>
<li>128-175 -- Accented Latin letters, currency, fractions</li> <li>128-175 -- Accented Latin letters, currency, fractions</li>
<li>176-223 -- Box-drawing characters (essential for chrome gadgets)</li> <li>176-223 -- Box-drawing characters (essential for chrome gadgets)</li>
<li>224-254 -- Greek letters, math symbols, super/subscripts</li> <li>224-254 -- Greek letters, math symbols, super/subscripts</li>
<li>255 -- Non-breaking space</li>
</ul> </ul>
<p>255 -- Non-breaking space</p>
<h2>Data</h2> <h2>Data</h2>
<h3>font8x16</h3> <h3>font8x16</h3>
<p>Static const uint8_t array of 256 * 16 = 4096 bytes containing the packed 1bpp glyph bitmaps, in ASCII code order.</p> <p>Static const uint8_t array of 256 * 16 = 4096 bytes containing the packed 1bpp glyph bitmaps, in ASCII code order.</p>
@ -4310,8 +4310,8 @@ void platformSplashShutdown(void);</code></pre>
<p>The shell supports two kinds of DXE apps:</p> <p>The shell supports two kinds of DXE apps:</p>
<ul> <ul>
<li>Callback-only (hasMainLoop = false) -- appMain() runs in the shell's task 0, creates windows, registers event callbacks, and returns immediately. The app lives through GUI callbacks. Lifecycle ends when the last window is closed. Simpler and cheaper (no extra stack/task).</li> <li>Callback-only (hasMainLoop = false) -- appMain() runs in the shell's task 0, creates windows, registers event callbacks, and returns immediately. The app lives through GUI callbacks. Lifecycle ends when the last window is closed. Simpler and cheaper (no extra stack/task).</li>
<li>Main-loop (hasMainLoop = true) -- A dedicated cooperative task is created. appMain() runs in that task and can do its own polling loop, calling tsYield() to share CPU. Lifecycle ends when appMain() returns or the task is killed. Needed for terminal emulators, games, or long computations.</li>
</ul> </ul>
<p>Main-loop (hasMainLoop = true) -- A dedicated cooperative task is created. appMain() runs in that task and can do its own polling loop, calling tsYield() to share CPU. Lifecycle ends when appMain() returns or the task is killed. Needed for terminal emulators, games, or long computations.</p>
<p>Both types use the same DXE interface: an exported appDescriptor and appMain function.</p> <p>Both types use the same DXE interface: an exported appDescriptor and appMain function.</p>
<h2>DXE Interface</h2> <h2>DXE Interface</h2>
<p>Every .app DXE module must export these symbols (COFF convention uses leading underscore):</p> <p>Every .app DXE module must export these symbols (COFF convention uses leading underscore):</p>
@ -4325,15 +4325,15 @@ void platformSplashShutdown(void);</code></pre>
<p>The app descriptor's hasMainLoop flag selects between two very different lifecycles:</p> <p>The app descriptor's hasMainLoop flag selects between two very different lifecycles:</p>
<ul> <ul>
<li>hasMainLoop = false (callback-only). The shell calls appMain directly on task 0 at dlopen time. The function creates windows, registers event callbacks, and returns. After that the app has no executing thread of its own -- it exists purely through GUI callbacks dispatched by dvxUpdate. The shell reaps the app automatically when its last window closes. Best for modal tools, dialogs, and event-driven utilities.</li> <li>hasMainLoop = false (callback-only). The shell calls appMain directly on task 0 at dlopen time. The function creates windows, registers event callbacks, and returns. After that the app has no executing thread of its own -- it exists purely through GUI callbacks dispatched by dvxUpdate. The shell reaps the app automatically when its last window closes. Best for modal tools, dialogs, and event-driven utilities.</li>
<li>hasMainLoop = true (main-loop). The shell creates a dedicated cooperative task (via tsCreate) with the descriptor's stackSize (or TS_DEFAULT_STACK_SIZE) and priority. appMain runs in that task and can do its own work loop, calling tsYield or any GUI function that yields. The app terminates when appMain returns (the wrapper sets AppStateTerminatingE) or when forced via shellForceKillApp. Best for terminal emulators, games, and any app with continuous background work.</li>
</ul> </ul>
<p>hasMainLoop = true (main-loop). The shell creates a dedicated cooperative task (via tsCreate) with the descriptor's stackSize (or TS_DEFAULT_STACK_SIZE) and priority. appMain runs in that task and can do its own work loop, calling tsYield or any GUI function that yields. The app terminates when appMain returns (the wrapper sets AppStateTerminatingE) or when forced via shellForceKillApp. Best for terminal emulators, games, and any app with continuous background work.</p>
<p>Both app types use the same export interface; only the descriptor's flags differ. Apps cannot switch modes at runtime.</p> <p>Both app types use the same export interface; only the descriptor's flags differ. Apps cannot switch modes at runtime.</p>
<h2>Icon Conventions</h2> <h2>Icon Conventions</h2>
<p>Shell-level UI (Program Manager, Task Manager) displays app icons at 16x16 and 32x32. Icons are not handled by the shell itself; each app embeds its own icons via the DVX resource system (DVX_RES_ICON):</p> <p>Shell-level UI (Program Manager, Task Manager) displays app icons at 16x16 and 32x32. Icons are not handled by the shell itself; each app embeds its own icons via the DVX resource system (DVX_RES_ICON):</p>
<ul> <ul>
<li>16x16 BMP for toolbar entries and list rows</li> <li>16x16 BMP for toolbar entries and list rows</li>
<li>32x32 BMP for desktop shortcuts and Program Manager tiles</li>
</ul> </ul>
<p>32x32 BMP for desktop shortcuts and Program Manager tiles</p>
<p>The Program Manager reads the app's 32x32 icon resource when building shortcut tiles. Apps without an icon resource fall back to a default shell-provided bitmap.</p> <p>The Program Manager reads the app's 32x32 icon resource when building shortcut tiles. Apps without an icon resource fall back to a default shell-provided bitmap.</p>
<h2>State Machine</h2> <h2>State Machine</h2>
<p>App slots progress through four states:</p> <p>App slots progress through four states:</p>
@ -4505,8 +4505,8 @@ shellConfigPath(ctx, &quot;settings.ini&quot;, path, sizeof(path));
<p>dvxSql wraps the bundled SQLite3 amalgamation (sql/thirdparty/sqlite/examples/sqlite3.h). It manages two internal dynamic tables keyed by 1-based handles:</p> <p>dvxSql wraps the bundled SQLite3 amalgamation (sql/thirdparty/sqlite/examples/sqlite3.h). It manages two internal dynamic tables keyed by 1-based handles:</p>
<ul> <ul>
<li>Database table: each slot holds a sqlite3 * plus a per-database error string and affected-row count.</li> <li>Database table: each slot holds a sqlite3 * plus a per-database error string and affected-row count.</li>
<li>Cursor table: each slot holds a sqlite3_stmt *, the owning database handle, and EOF tracking.</li>
</ul> </ul>
<p>Cursor table: each slot holds a sqlite3_stmt *, the owning database handle, and EOF tracking.</p>
<p>Growing either table doubles its capacity. Closed slots are recycled by subsequent dvxSqlOpen or dvxSqlQuery calls, keeping handle values stable for the caller.</p> <p>Growing either table doubles its capacity. Closed slots are recycled by subsequent dvxSqlOpen or dvxSqlQuery calls, keeping handle values stable for the caller.</p>
<h3>Handle Model</h3> <h3>Handle Model</h3>
<p>Database and cursor handles are int32_t values. A successful open or query returns a handle greater than zero. Handle 0 is reserved as the invalid/error sentinel. Closing a database automatically finalizes all cursors that belong to it, so callers do not need to track cursor lifetimes per-database.</p> <p>Database and cursor handles are int32_t values. A successful open or query returns a handle greater than zero. Handle 0 is reserved as the invalid/error sentinel. Closing a database automatically finalizes all cursors that belong to it, so callers do not need to track cursor lifetimes per-database.</p>
@ -4515,14 +4515,14 @@ shellConfigPath(ctx, &quot;settings.ini&quot;, path, sizeof(path));
<li>dvxSqlOpen allocates a handle and calls sqlite3_open. The database file is created if it does not exist.</li> <li>dvxSqlOpen allocates a handle and calls sqlite3_open. The database file is created if it does not exist.</li>
<li>Use dvxSqlExec for DDL and non-query DML. On success, dvxSqlAffectedRows returns the row count for the last call on this handle.</li> <li>Use dvxSqlExec for DDL and non-query DML. On success, dvxSqlAffectedRows returns the row count for the last call on this handle.</li>
<li>Use dvxSqlQuery to obtain a cursor over a SELECT. Iterate with dvxSqlNext and read columns with dvxSqlFieldText, dvxSqlFieldInt, dvxSqlFieldDbl, or dvxSqlFieldByName.</li> <li>Use dvxSqlQuery to obtain a cursor over a SELECT. Iterate with dvxSqlNext and read columns with dvxSqlFieldText, dvxSqlFieldInt, dvxSqlFieldDbl, or dvxSqlFieldByName.</li>
<li>Call dvxSqlFreeResult when the cursor is no longer needed. Call dvxSqlClose when the database is no longer needed; any cursors still open on that database are finalized automatically.</li>
</ul> </ul>
<p>Call dvxSqlFreeResult when the cursor is no longer needed. Call dvxSqlClose when the database is no longer needed; any cursors still open on that database are finalized automatically.</p>
<h3>Common Patterns</h3> <h3>Common Patterns</h3>
<ul> <ul>
<li>Parameterize user input with dvxSqlEscape before interpolating into SQL strings.</li> <li>Parameterize user input with dvxSqlEscape before interpolating into SQL strings.</li>
<li>Check dvxSqlError(db) for the last error on a handle; its message is stable until the next operation on the same database.</li> <li>Check dvxSqlError(db) for the last error on a handle; its message is stable until the next operation on the same database.</li>
<li>dvxSqlFieldByName matches column names case-insensitively, suitable for most real-world schemas.</li>
</ul> </ul>
<p>dvxSqlFieldByName matches column names case-insensitively, suitable for most real-world schemas.</p>
<p><a href="#sql.db">Database Operations</a></p> <p><a href="#sql.db">Database Operations</a></p>
<p><a href="#sql.cursor">Cursor Operations</a></p> <p><a href="#sql.cursor">Cursor Operations</a></p>
<p><a href="#sql.utility">Utility Functions</a></p> <p><a href="#sql.utility">Utility Functions</a></p>
@ -4709,8 +4709,8 @@ dvxSqlExec(db, sql);</code></pre>
<li>Cursor blink. The library tracks a 250 ms blink timer in a static global and the focused widget reads sCursorBlinkOn (exposed via libdvx) when repainting.</li> <li>Cursor blink. The library tracks a 250 ms blink timer in a static global and the focused widget reads sCursorBlinkOn (exposed via libdvx) when repainting.</li>
<li>Selection clearing. When a widget gains focus it calls clearOtherSelections(self) so only one widget ever has an active text selection.</li> <li>Selection clearing. When a widget gains focus it calls clearOtherSelections(self) so only one widget ever has an active text selection.</li>
<li>Word boundaries. isWordChar, wordStart/wordEnd, and wordBoundaryLeft/wordBoundaryRight implement the logic for double-click word selection and Ctrl+Left/Right navigation in a uniform way.</li> <li>Word boundaries. isWordChar, wordStart/wordEnd, and wordBoundaryLeft/wordBoundaryRight implement the logic for double-click word selection and Ctrl+Left/Right navigation in a uniform way.</li>
<li>Single-line editing engine. widgetTextEditOnKey, widgetTextEditMouseClick, widgetTextEditDragUpdateLine, and widgetTextEditPaintLine form a pointer-parameterized implementation of keyboard, mouse, drag, and paint behaviors. Widgets (TextInput, Spinner, ComboBox, AnsiTerm) hand the library pointers to their internal buffer, cursor, scroll offset, and selection state.</li>
</ul> </ul>
<p>Single-line editing engine. widgetTextEditOnKey, widgetTextEditMouseClick, widgetTextEditDragUpdateLine, and widgetTextEditPaintLine form a pointer-parameterized implementation of keyboard, mouse, drag, and paint behaviors. Widgets (TextInput, Spinner, ComboBox, AnsiTerm) hand the library pointers to their internal buffer, cursor, scroll offset, and selection state.</p>
<p>The engine is intentionally pointer-parameterized rather than struct-based so widgets can reuse it without adopting a shared state struct. Each widget owns its own buffer and state and passes pointers in on every call.</p> <p>The engine is intentionally pointer-parameterized rather than struct-based so widgets can reuse it without adopting a shared state struct. Each widget owns its own buffer and state and passes pointers in on every call.</p>
<h2>Constants</h2> <h2>Constants</h2>
<pre> Constant Value Description <pre> Constant Value Description
@ -4824,8 +4824,8 @@ dvxSqlExec(db, sql);</code></pre>
<ul> <ul>
<li>Single click -- places the cursor and begins a potential drag selection.</li> <li>Single click -- places the cursor and begins a potential drag selection.</li>
<li>Double click -- selects the word under the cursor (if wordSelect is true), or selects all text.</li> <li>Double click -- selects the word under the cursor (if wordSelect is true), or selects all text.</li>
<li>Triple click -- selects all text in the field.</li>
</ul> </ul>
<p>Triple click -- selects all text in the field.</p>
<pre><code>void widgetTextEditMouseClick( <pre><code>void widgetTextEditMouseClick(
WidgetT *w, int32_t vx, int32_t vy, WidgetT *w, int32_t vx, int32_t vy,
int32_t textLeftX, const BitmapFontT *font, int32_t textLeftX, const BitmapFontT *font,
@ -5054,8 +5054,8 @@ dvxSqlExec(db, sql);</code></pre>
<li>End Task button (Alt+E): force-kills the selected app via shellForceKillApp.</li> <li>End Task button (Alt+E): force-kills the selected app via shellForceKillApp.</li>
<li>Run... button (Alt+R): opens a file dialog to browse for and launch a .app file.</li> <li>Run... button (Alt+R): opens a file dialog to browse for and launch a .app file.</li>
<li>Status bar showing running app count plus total and used system memory.</li> <li>Status bar showing running app count plus total and used system memory.</li>
<li>Single-instance: reopening the Task Manager while it is already visible raises and focuses the existing window rather than creating a new one.</li>
</ul> </ul>
<p>Single-instance: reopening the Task Manager while it is already visible raises and focuses the existing window rather than creating a new one.</p>
<h2>shellTaskMgrOpen</h2> <h2>shellTaskMgrOpen</h2>
<p>Open the Task Manager window, or raise it to the front if already open.</p> <p>Open the Task Manager window, or raise it to the front if already open.</p>
<pre><code>void shellTaskMgrOpen(AppContextT *ctx);</code></pre> <pre><code>void shellTaskMgrOpen(AppContextT *ctx);</code></pre>
@ -5075,8 +5075,8 @@ dvxSqlExec(db, sql);</code></pre>
<li>rs232 -- ISR-driven UART driver with ring buffers and flow control</li> <li>rs232 -- ISR-driven UART driver with ring buffers and flow control</li>
<li>packet -- HDLC framing, CRC-16, Go-Back-N ARQ (reliable delivery)</li> <li>packet -- HDLC framing, CRC-16, Go-Back-N ARQ (reliable delivery)</li>
<li>security -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR cipher, DRBG RNG</li> <li>security -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR cipher, DRBG RNG</li>
<li>secLink -- Convenience wrapper: channel multiplexing, per-packet encryption</li>
</ul> </ul>
<p>secLink -- Convenience wrapper: channel multiplexing, per-packet encryption</p>
<p>Loaded as: bin/libs/kpunch/serial/serial.lib</p> <p>Loaded as: bin/libs/kpunch/serial/serial.lib</p>
<h2>Layered Architecture</h2> <h2>Layered Architecture</h2>
<pre><code>+--------------------------------------------------+ <pre><code>+--------------------------------------------------+
@ -5410,15 +5410,15 @@ void secCipherDestroy(SecCipherT *c);</code></pre>
<ul> <ul>
<li>Creates a DH context via secDhCreate, generates 1024-bit keys via secDhGenerateKeys, and exports the 128-byte public key.</li> <li>Creates a DH context via secDhCreate, generates 1024-bit keys via secDhGenerateKeys, and exports the 128-byte public key.</li>
<li>Sends the public key as a single packet via pktSend (blocking).</li> <li>Sends the public key as a single packet via pktSend (blocking).</li>
<li>Polls pktPoll until the remote's public key arrives and the internal callback completes the handshake (computes the shared secret, derives directional TX and RX cipher keys, transitions to READY, destroys the DH context for forward secrecy).</li>
</ul> </ul>
<p>Polls pktPoll until the remote's public key arrives and the internal callback completes the handshake (computes the shared secret, derives directional TX and RX cipher keys, transitions to READY, destroys the DH context for forward secrecy).</p>
<p>Returns SECLINK_SUCCESS on success, SECLINK_ERR_PARAM on NULL link, SECLINK_ERR_ALLOC on DH context allocation failure, or SECLINK_ERR_HANDSHAKE for DH key generation failure, packet send failure, or serial disconnect during the exchange.</p> <p>Returns SECLINK_SUCCESS on success, SECLINK_ERR_PARAM on NULL link, SECLINK_ERR_ALLOC on DH context allocation failure, or SECLINK_ERR_HANDSHAKE for DH key generation failure, packet send failure, or serial disconnect during the exchange.</p>
<h3>Directional Key Derivation</h3> <h3>Directional Key Derivation</h3>
<p>To prevent CTR-mode keystream collision, the two sides never use the same TX key. After computing the shared secret, each side derives a master XTEA key via secDhDeriveKey and then XORs it with a direction byte:</p> <p>To prevent CTR-mode keystream collision, the two sides never use the same TX key. After computing the shared secret, each side derives a master XTEA key via secDhDeriveKey and then XORs it with a direction byte:</p>
<ul> <ul>
<li>The side with the lexicographically lower public key uses masterKey XOR 0xAA for TX and masterKey XOR 0x55 for RX.</li> <li>The side with the lexicographically lower public key uses masterKey XOR 0xAA for TX and masterKey XOR 0x55 for RX.</li>
<li>The other side uses the reverse assignment.</li>
</ul> </ul>
<p>The other side uses the reverse assignment.</p>
<p>Both sides initialize their cipher counters to zero and advance monotonically from there.</p> <p>Both sides initialize their cipher counters to zero and advance monotonically from there.</p>
<h2>secLinkSend</h2> <h2>secLinkSend</h2>
<p>Send data on a channel, optionally encrypted. Prepends a one-byte channel header (bit 7 = encrypt flag, bits 6..0 = channel number), then encrypts the payload only (never the header) when encrypt is true.</p> <p>Send data on a channel, optionally encrypted. Prepends a one-byte channel header (bit 7 = encrypt flag, bits 6..0 = channel number), then encrypts the payload only (never the header) when encrypt is true.</p>
@ -5526,8 +5526,8 @@ int main(void) {
<li>secLinkOpen internally calls rs232Open and pktOpen. secLinkClose tears them down in reverse order.</li> <li>secLinkOpen internally calls rs232Open and pktOpen. secLinkClose tears them down in reverse order.</li>
<li>The RNG must be seeded before secLinkHandshake. secRngGatherEntropy provides roughly 20 bits of hardware entropy; supplement with user interaction timing for cryptographic use.</li> <li>The RNG must be seeded before secLinkHandshake. secRngGatherEntropy provides roughly 20 bits of hardware entropy; supplement with user interaction timing for cryptographic use.</li>
<li>secLinkPoll must be called frequently to drain the RX ring buffer, process ACKs, and dispatch received packets to the callback.</li> <li>secLinkPoll must be called frequently to drain the RX ring buffer, process ACKs, and dispatch received packets to the callback.</li>
<li>For bulk transfers larger than SECLINK_MAX_PAYLOAD (254 bytes), use secLinkSendBuf which splits the data into chunks automatically.</li>
</ul> </ul>
<p>For bulk transfers larger than SECLINK_MAX_PAYLOAD (254 bytes), use secLinkSendBuf which splits the data into chunks automatically.</p>
</div> </div>
<div class="topic" id="lib.basrt"> <div class="topic" id="lib.basrt">
<h1>BASIC Runtime Library</h1> <h1>BASIC Runtime Library</h1>

View file

@ -512,8 +512,6 @@ static void buildVideoTab(WidgetT *page) {
// Build label strings // Build label strings
arrsetlen(sVideoLabels, 0); arrsetlen(sVideoLabels, 0);
arrfree(sLabelBufs);
sLabelBufs = NULL;
arrsetlen(sLabelBufs, sVideoCount); arrsetlen(sLabelBufs, sVideoCount);
for (int32_t i = 0; i < sVideoCount; i++) { for (int32_t i = 0; i < sVideoCount; i++) {
@ -554,10 +552,18 @@ static void buildVideoTab(WidgetT *page) {
static int32_t mapAccelName(const char *name) { static int32_t mapAccelName(const char *name) {
if (strcmp(name, "off") == 0) return MOUSE_ACCEL_OFF; if (strcmp(name, "off") == 0) {
if (strcmp(name, "low") == 0) return MOUSE_ACCEL_LOW; return MOUSE_ACCEL_OFF;
if (strcmp(name, "medium") == 0) return MOUSE_ACCEL_MEDIUM; }
if (strcmp(name, "high") == 0) return MOUSE_ACCEL_HIGH; if (strcmp(name, "low") == 0) {
return MOUSE_ACCEL_LOW;
}
if (strcmp(name, "medium") == 0) {
return MOUSE_ACCEL_MEDIUM;
}
if (strcmp(name, "high") == 0) {
return MOUSE_ACCEL_HIGH;
}
return MOUSE_ACCEL_MEDIUM; return MOUSE_ACCEL_MEDIUM;
} }

View file

@ -127,12 +127,12 @@ $(TEST_COMPACT): $(TEST_COMPACT_SRCS) | $(HOSTDIR)
# Host command-line compiler -- basstub.app is appended as a STUB # Host command-line compiler -- basstub.app is appended as a STUB
# resource so bascomp is self-contained (no BASSTUB.APP companion file). # resource so bascomp is self-contained (no BASSTUB.APP companion file).
$(BASCOMP_TARGET): $(BASCOMP_SRCS) $(STUB_TARGET) | $(HOSTDIR) $(BASCOMP_TARGET): $(BASCOMP_SRCS) ../../../tools/dvxResWrite.h $(STUB_TARGET) $(DVXRES) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -DBASCOMP_STANDALONE -I../../../tools -o $@ $(BASCOMP_SRCS) -lm $(HOSTCC) $(HOSTCFLAGS) -DBASCOMP_STANDALONE -I../../../tools -o $@ $(BASCOMP_SRCS) -lm
$(DVXRES) add $@ STUB binary @$(STUB_TARGET) $(DVXRES) add $@ STUB binary @$(STUB_TARGET)
# DOS command-line compiler (same STUB embed as the host build) # DOS command-line compiler (same STUB embed as the host build)
$(SYSTEMDIR)/BASCOMP.EXE: $(BASCOMP_SRCS) $(STUB_TARGET) | $(SYSTEMDIR) $(SYSTEMDIR)/BASCOMP.EXE: $(BASCOMP_SRCS) ../../../tools/dvxResWrite.h $(STUB_TARGET) $(DVXRES) | $(SYSTEMDIR)
$(DOSCC) $(DOSCFLAGS) -DBASCOMP_STANDALONE -I../../../tools -o $(SYSTEMDIR)/bascomp.exe $(BASCOMP_SRCS) -lm $(DOSCC) $(DOSCFLAGS) -DBASCOMP_STANDALONE -I../../../tools -o $(SYSTEMDIR)/bascomp.exe $(BASCOMP_SRCS) -lm
$(EXE2COFF) $(SYSTEMDIR)/bascomp.exe $(EXE2COFF) $(SYSTEMDIR)/bascomp.exe
cat $(CWSDSTUB) $(SYSTEMDIR)/bascomp > $@ cat $(CWSDSTUB) $(SYSTEMDIR)/bascomp > $@

View file

@ -65,15 +65,18 @@ static int32_t emitIcon(const char *path, const BasBuildSpecT *spec) {
// If a disk path was given, load it and embed. Otherwise use the // If a disk path was given, load it and embed. Otherwise use the
// pre-loaded bytes (used by the IDE for its "noicon" fallback). // pre-loaded bytes (used by the IDE for its "noicon" fallback).
if (spec->iconPath && spec->iconPath[0]) { if (spec->iconPath && spec->iconPath[0]) {
int32_t iconLen = 0; int32_t iconLen = 0;
char *iconData = platformReadFile(spec->iconPath, &iconLen); char *iconData = platformReadFile(spec->iconPath, &iconLen);
int32_t rc = 0;
if (iconData) { if (!iconData) {
rc = dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen); // A named icon that cannot be read is a build failure --
free(iconData); // returning success here would ship the app without its
// ICON32 resource and never tell anyone.
return -1;
} }
int32_t rc = dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen);
free(iconData);
return rc; return rc;
} }
@ -142,6 +145,15 @@ int32_t basBuildEmitResources(const char *outPath, const BasBuildSpecT *spec) {
continue; continue;
} }
// The stub's loader scans FORM0..FORM(BAS_MAX_FORM_RESOURCES-1)
// and stops there, so anything past that cap would be written but
// unreachable at runtime. Fail the build loudly instead of
// shipping forms the app can never load.
if (outIdx >= BAS_MAX_FORM_RESOURCES) {
rc |= -1;
break;
}
char resName[16]; char resName[16];
snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)outIdx); snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)outIdx);
rc |= dvxResAppend(outPath, resName, DVX_RES_BINARY, spec->formData[i], (uint32_t)spec->formLens[i]); rc |= dvxResAppend(outPath, resName, DVX_RES_BINARY, spec->formData[i], (uint32_t)spec->formLens[i]);

View file

@ -196,6 +196,13 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) {
cg->debugUdtDefs[i].fieldCount * sizeof(BasDebugFieldT)); cg->debugUdtDefs[i].fieldCount * sizeof(BasDebugFieldT));
} }
} }
// Keep count in sync with the pointer: on OOM the fields
// array stays NULL, so leaving the copied count nonzero
// would make consumers deref NULL.
if (mod->debugUdtDefs[i].fields == NULL) {
mod->debugUdtDefs[i].fieldCount = 0;
}
} }
mod->debugUdtDefCount = cg->debugUdtDefCount; mod->debugUdtDefCount = cg->debugUdtDefCount;
@ -283,6 +290,24 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
} }
} }
// Debug local variables carry a procIndex assigned in source-definition
// (code emission) order, while the symbol table lists procs in
// first-mention order (DECLARE lines included). Sort the table by code
// address so its indices match those procIndex values and the debugger
// attributes locals to the right procedure. Insertion sort: the table
// is small and usually already in order.
for (int32_t i = 1; i < idx; i++) {
BasProcEntryT key = mod->procs[i];
int32_t j = i - 1;
while (j >= 0 && mod->procs[j].codeAddr > key.codeAddr) {
mod->procs[j + 1] = mod->procs[j];
j--;
}
mod->procs[j + 1] = key;
}
mod->procCount = idx; mod->procCount = idx;
return mod; return mod;
} }

View file

@ -659,8 +659,9 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
maxDigit = 15; maxDigit = 15;
} }
int32_t idx = 0; bool overflow = false;
uint64_t value = 0; int32_t idx = 0;
uint64_t value = 0;
for (;;) { for (;;) {
if (atEnd(lex)) { if (atEnd(lex)) {
@ -688,6 +689,13 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
appendTokenChar(lex, &idx, c); appendTokenChar(lex, &idx, c);
value = (value << shift) | (uint64_t)digit; value = (value << shift) | (uint64_t)digit;
// Latch overflow instead of testing value after the loop: with
// enough digits the shifts wrap the uint64 back into range, which
// would hide the overflow.
if (value > UINT32_MAX) {
overflow = true;
}
} }
// No digits after the &H/&O/&B marker is a typo, not the integer 0. // No digits after the &H/&O/&B marker is a typo, not the integer 0.
@ -697,6 +705,14 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
return TOK_ERROR; return TOK_ERROR;
} }
// Runtime INTEGER and LONG are both 32-bit; silently truncating a
// wider literal would miscompile the constant, so reject it.
if (overflow) {
setError(lex, "Hexadecimal/octal/binary literal exceeds 32 bits");
lex->token.type = TOK_ERROR;
return TOK_ERROR;
}
lex->token.text[idx] = '\0'; lex->token.text[idx] = '\0';
lex->token.textLen = idx; lex->token.textLen = idx;

View file

@ -107,17 +107,14 @@ static const BuiltinFuncT builtinFuncs[] = {
{NULL, 0, 0, 0, 0} {NULL, 0, 0, 0, 0}
}; };
// Single source of truth for BASIC type-suffix characters. Both
// suffixToType (maps a trailing suffix to its BAS_TYPE_*) and
// nameHasTypeSuffix (reports whether a trailing suffix is present)
// are driven by this string so the two never disagree.
// '%' = INTEGER, '&' = LONG, '!' = SINGLE, '#' = DOUBLE, '$' = STRING
static const char suffixChars[] = "%&!#$";
// ============================================================ // ============================================================
// Module-scope declarations // Module-scope declarations
// ============================================================ // ============================================================
// Relative jump operands are 2 bytes; the VM takes the jump from the
// PC positioned just past the operand.
#define BAS_JUMP_OPERAND_SIZE 2
// ============================================================ // ============================================================
// Prototypes // Prototypes
// ============================================================ // ============================================================
@ -133,11 +130,14 @@ static bool check(BasParserT *p, BasTokenTypeE type);
static bool checkCtrlArrayAccess(BasParserT *p); static bool checkCtrlArrayAccess(BasParserT *p);
static bool checkKeyword(BasParserT *p, const char *kw); static bool checkKeyword(BasParserT *p, const char *kw);
static bool checkKeywordText(const char *text, const char *kw); static bool checkKeywordText(const char *text, const char *kw);
static bool clampArgCount(BasParserT *p, int32_t argc);
static bool clampParamCount(BasParserT *p, int32_t paramCount); static bool clampParamCount(BasParserT *p, int32_t paramCount);
static void closeSelectCase(BasParserT *p, int32_t **endJumps);
static void collectDebugGlobals(BasParserT *p); static void collectDebugGlobals(BasParserT *p);
static void collectDebugLocals(BasParserT *p, int32_t procIndex); static void collectDebugLocals(BasParserT *p, int32_t procIndex);
static void emitByRefArg(BasParserT *p); static void emitByRefArg(BasParserT *p);
static void emitFunctionCall(BasParserT *p, BasSymbolT *sym); static void emitFunctionCall(BasParserT *p, BasSymbolT *sym);
static void emitGotoWithSelectPops(BasParserT *p, const char *labelName);
static int32_t emitJump(BasParserT *p, uint8_t opcode); static int32_t emitJump(BasParserT *p, uint8_t opcode);
static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName); static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName);
static void emitLoad(BasParserT *p, BasSymbolT *sym); static void emitLoad(BasParserT *p, BasSymbolT *sym);
@ -234,6 +234,7 @@ static void parseXorExpr(BasParserT *p);
static void patchCallAddrs(BasParserT *p, BasSymbolT *sym); static void patchCallAddrs(BasParserT *p, BasSymbolT *sym);
static void patchJump(BasParserT *p, int32_t addr); static void patchJump(BasParserT *p, int32_t addr);
static void patchLabelRefs(BasParserT *p, BasSymbolT *sym); static void patchLabelRefs(BasParserT *p, BasSymbolT *sym);
static int16_t relJumpOffset(BasParserT *p, int32_t target, int32_t operandAddr);
static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName); static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName);
static uint8_t resolveTypeName(BasParserT *p); static uint8_t resolveTypeName(BasParserT *p);
static void shiftBackpatchAddrs(BasParserT *p, int32_t from, int32_t delta); static void shiftBackpatchAddrs(BasParserT *p, int32_t from, int32_t delta);
@ -306,10 +307,13 @@ bool basParse(BasParserT *p) {
error(p, "Too many string constants (constant pool index exceeds 16 bits)"); error(p, "Too many string constants (constant pool index exceeds 16 bits)");
} }
// Jumps and call addresses are 16-bit; a larger module would silently // OP_CALL operands are absolute 16-bit unsigned addresses, so code
// miscompile, so reject it cleanly instead. // beyond the uint16 range is unreachable by calls; reject cleanly
if (p->cg.codeLen > BAS_MAX_CODE_SIZE) { // instead of silently miscompiling. Relative jump spans are signed
error(p, "Module too large (code size exceeds 16-bit limit)"); // 16-bit and are range-checked individually in relJumpOffset, so
// modules between 32K and 64K stay compilable.
if (p->cg.codeLen > UINT16_MAX) {
error(p, "Module too large (code size exceeds 16-bit address range)");
} }
return !p->hasError; return !p->hasError;
@ -445,6 +449,22 @@ static bool checkKeywordText(const char *text, const char *kw) {
} }
// Guard against emitting a call with more than BAS_VM_MAX_CALL_ARGS
// arguments. The VM encodes the argument count in a single byte, so a
// call may not exceed that bound. Returns true (and reports an error)
// when argc exceeds the limit, signalling the caller to abort the
// current call emission.
static bool clampArgCount(BasParserT *p, int32_t argc) {
if (argc > BAS_VM_MAX_CALL_ARGS) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS);
error(p, buf);
return true;
}
return false;
}
// Guard against declaring more than BAS_MAX_PARAMS parameters. The // Guard against declaring more than BAS_MAX_PARAMS parameters. The
// typed param arrays on a symbol are fixed at BAS_MAX_PARAMS, so a // typed param arrays on a symbol are fixed at BAS_MAX_PARAMS, so a
// declaration may not exceed that bound. Returns true (and reports an // declaration may not exceed that bound. Returns true (and reports an
@ -461,6 +481,20 @@ static bool clampParamCount(BasParserT *p, int32_t paramCount) {
} }
// Emit the shared END SELECT epilogue: land every pending end-of-case jump on
// the OP_POP that discards the SELECT test value, free the jump list, and drop
// the select-depth marker. Every exit from parseSelectCase runs this so the
// test value is popped exactly once and selectDepth stays balanced.
static void closeSelectCase(BasParserT *p, int32_t **endJumps) {
for (int32_t i = 0; i < (int32_t)arrlen(*endJumps); i++) {
patchJump(p, (*endJumps)[i]);
}
basEmit8(&p->cg, OP_POP);
arrfree(*endJumps);
p->selectDepth--;
}
// Snapshot global variables for the debugger at the end of compilation. // Snapshot global variables for the debugger at the end of compilation.
static void collectDebugGlobals(BasParserT *p) { static void collectDebugGlobals(BasParserT *p) {
for (int32_t i = 0; i < p->sym.count; i++) { for (int32_t i = 0; i < p->sym.count; i++) {
@ -692,10 +726,7 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) {
// External library function: emit OP_CALL_EXTERN // External library function: emit OP_CALL_EXTERN
if (sym->isExtern) { if (sym->isExtern) {
if (argc > BAS_VM_MAX_CALL_ARGS) { if (clampArgCount(p, argc)) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS);
error(p, buf);
return; return;
} }
@ -724,6 +755,50 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) {
} }
// Emit a jump to a GOTO target, discarding the live test values of any
// SELECT CASE blocks the jump leaves. Shared by GOTO and the ON expr
// GOTO arms so both discard the same way.
static void emitGotoWithSelectPops(BasParserT *p, const char *labelName) {
// A GOTO out of one or more SELECT CASE blocks must discard their
// live test values -- but only for the blocks actually being left.
// A label may sit inside the same SELECT, so the number of pops is
// the GOTO site's depth minus the label's depth.
BasSymbolT *sym = basSymTabFind(&p->sym, labelName);
if (sym != NULL && sym->kind == SYM_LABEL && sym->isDefined) {
// Backward GOTO: the label's SELECT depth was recorded in
// sym->localCount when it was defined.
int32_t depthDiff = p->selectDepth - sym->localCount;
if (depthDiff > 0) {
emitSelectPops(p, depthDiff);
}
emitJumpToLabel(p, OP_JMP, labelName);
return;
}
// Forward GOTO: the label's depth is unknown until it is defined.
// Reserve one OP_NOP per currently-open SELECT ahead of the jump and
// encode this site's depth in the jump's placeholder operand;
// patchLabelRefs converts the needed number of NOPs to OP_POP once
// the label's depth is known. Unconverted NOPs execute harmlessly.
for (int32_t i = 0; i < p->selectDepth; i++) {
basEmit8(&p->cg, OP_NOP);
}
emitJumpToLabel(p, OP_JMP, labelName);
if (p->selectDepth > 0 && !p->hasError) {
sym = basSymTabFind(&p->sym, labelName);
if (sym != NULL && sym->patchCount > 0) {
basPatch16(&p->cg, sym->patchAddrs[sym->patchCount - 1], (int16_t)p->selectDepth);
}
}
}
static int32_t emitJump(BasParserT *p, uint8_t opcode) { static int32_t emitJump(BasParserT *p, uint8_t opcode) {
basEmit8(&p->cg, opcode); basEmit8(&p->cg, opcode);
int32_t addr = basCodePos(&p->cg); int32_t addr = basCodePos(&p->cg);
@ -740,7 +815,7 @@ static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName
// Label already defined -- emit jump to known address // Label already defined -- emit jump to known address
basEmit8(&p->cg, opcode); basEmit8(&p->cg, opcode);
int32_t here = basCodePos(&p->cg); int32_t here = basCodePos(&p->cg);
int16_t offset = (int16_t)(sym->codeAddr - (here + 2)); int16_t offset = relJumpOffset(p, sym->codeAddr, here);
basEmit16(&p->cg, offset); basEmit16(&p->cg, offset);
return; return;
} }
@ -802,6 +877,16 @@ static void emitLoad(BasParserT *p, BasSymbolT *sym) {
} }
// Emit OP_POP `count` times. Used to discard the live SELECT CASE test
// values from the eval stack when EXIT/GOTO jumps out of one or more
// enclosing SELECT constructs (see selectDepth).
static void emitSelectPops(BasParserT *p, int32_t count) {
for (int32_t i = 0; i < count; i++) {
basEmit8(&p->cg, OP_POP);
}
}
static void emitStore(BasParserT *p, BasSymbolT *sym) { static void emitStore(BasParserT *p, BasSymbolT *sym) {
// Storing into a CONST would emit OP_STORE_GLOBAL at its index 0 // Storing into a CONST would emit OP_STORE_GLOBAL at its index 0
// (CONSTs never allocate a slot), corrupting the first real global. // (CONSTs never allocate a slot), corrupting the first real global.
@ -832,16 +917,6 @@ static void emitStore(BasParserT *p, BasSymbolT *sym) {
} }
// Emit OP_POP `count` times. Used to discard the live SELECT CASE test
// values from the eval stack when EXIT/GOTO jumps out of one or more
// enclosing SELECT constructs (see selectDepth).
static void emitSelectPops(BasParserT *p, int32_t count) {
for (int32_t i = 0; i < count; i++) {
basEmit8(&p->cg, OP_POP);
}
}
// emitUdtInit -- emit code to initialize nested UDT fields after a UDT // emitUdtInit -- emit code to initialize nested UDT fields after a UDT
// has been created and is on top of the stack. For each field that is // has been created and is on top of the stack. For each field that is
// itself a UDT, we DUP the parent, allocate the child UDT, and store it // itself a UDT, we DUP the parent, allocate the child UDT, and store it
@ -980,7 +1055,7 @@ static void exitListPatch(ExitListT *el, BasParserT *p) {
int32_t n = (int32_t)arrlen(el->patchAddr); int32_t n = (int32_t)arrlen(el->patchAddr);
for (int32_t i = 0; i < n; i++) { for (int32_t i = 0; i < n; i++) {
int16_t offset = (int16_t)(target - (el->patchAddr[i] + 2)); int16_t offset = relJumpOffset(p, target, el->patchAddr[i]);
basPatch16(&p->cg, el->patchAddr[i], offset); basPatch16(&p->cg, el->patchAddr[i], offset);
} }
@ -1091,16 +1166,17 @@ static bool match(BasParserT *p, BasTokenTypeE type) {
} }
// Report whether a name carries an explicit BASIC type-suffix character // Report whether a name carries an explicit BASIC type-suffix character.
// (one of suffixChars). Lets callers distinguish an explicitly typed // Lets callers distinguish an explicitly typed name like "a!" from a bare
// name like "a!" from a bare "a", which suffixToType cannot do alone // "a", which suffixToType cannot do alone because it returns
// because it returns BAS_TYPE_SINGLE for both. // BAS_TYPE_SINGLE for both. Shares the lexer's basIsTypeSuffixChar so the
// suffix character set has a single source of truth.
static bool nameHasTypeSuffix(const char *name) { static bool nameHasTypeSuffix(const char *name) {
int32_t len = (int32_t)strlen(name); int32_t len = (int32_t)strlen(name);
if (len == 0) { if (len == 0) {
return false; return false;
} }
return strchr(suffixChars, name[len - 1]) != NULL; return basIsTypeSuffixChar(name[len - 1]);
} }
@ -1392,10 +1468,7 @@ static void parseAssignOrCall(BasParserT *p) {
argc++; argc++;
} }
if (argc > BAS_VM_MAX_CALL_ARGS) { if (clampArgCount(p, argc)) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS);
error(p, buf);
return; return;
} }
@ -1464,10 +1537,7 @@ static void parseAssignOrCall(BasParserT *p) {
argc++; argc++;
} }
if (argc > BAS_VM_MAX_CALL_ARGS) { if (clampArgCount(p, argc)) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS);
error(p, buf);
return; return;
} }
@ -1638,10 +1708,7 @@ static void parseAssignOrCall(BasParserT *p) {
// External library SUB: emit OP_CALL_EXTERN // External library SUB: emit OP_CALL_EXTERN
if (sym->isExtern) { if (sym->isExtern) {
if (argc > BAS_VM_MAX_CALL_ARGS) { if (clampArgCount(p, argc)) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS);
error(p, buf);
return; return;
} }
@ -2314,11 +2381,12 @@ static void parseDef(BasParserT *p) {
parseExpression(p); parseExpression(p);
basEmit8(&p->cg, OP_RET_VAL); basEmit8(&p->cg, OP_RET_VAL);
// Do NOT emit debug locals (or consume a debug proc index) for DEF FN. // A DEF FN occupies a proc-table slot (it is a defined FUNCTION
// The proc table orders DEF FNs at its tail (symbol-table order), while // symbol), and the proc table is sorted by code address to match the
// debugProcCount runs in source order; consuming an index here would // source-order debugProcCount indices -- so consume a debug proc index
// misattribute this DEF FN's -- and every later proc's -- debug locals. // here even though no debug locals are emitted for it. DEF FN params
// DEF FN params are tiny/transient, so skipping their debug info is safe. // are tiny/transient, so skipping their debug info is safe.
p->cg.debugProcCount++;
basSymTabLeaveLocal(&p->sym); basSymTabLeaveLocal(&p->sym);
uint8_t returnType = suffixToType(name); uint8_t returnType = suffixToType(name);
@ -2472,11 +2540,28 @@ static void parseDim(BasParserT *p) {
newScope = SCOPE_GLOBAL; newScope = SCOPE_GLOBAL;
} }
// Check for duplicate in the same scope only // Check for duplicates. Only a VARIABLE from an outer scope may be
BasSymbolT *existing = basSymTabFind(&p->sym, name); // shadowed; a CONST/label/type of the same name must be rejected
if (existing != NULL && existing->isDefined && existing->scope == newScope) { // regardless of scope, because local-first lookup would make every
// later use of that name silently bind to the new variable. A
// SUB/FUNCTION is rejected even when not yet defined: prescan
// registers every signature before bodies parse, so the name is
// already committed to a procedure (a shadowed FUNCTION call would
// otherwise compile as an array load on a scalar).
BasSymbolT *existing = basSymTabFind(&p->sym, name);
bool isDupSymbol = false;
if (existing != NULL) {
if (existing->kind == SYM_SUB || existing->kind == SYM_FUNCTION) {
isDupSymbol = true;
} else if (existing->isDefined && (existing->kind != SYM_VARIABLE || existing->scope == newScope)) {
isDupSymbol = true;
}
}
if (isDupSymbol) {
char buf[BAS_PARSE_ERR_SCRATCH]; char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Variable '%s' already declared", name); snprintf(buf, sizeof(buf), "'%s' already declared", name);
error(p, buf); error(p, buf);
return; return;
} }
@ -2637,19 +2722,19 @@ static void parseDo(BasParserT *p) {
parseExpression(p); parseExpression(p);
// Jump back to loopTop if condition is true // Jump back to loopTop if condition is true
basEmit8(&p->cg, OP_JMP_TRUE); basEmit8(&p->cg, OP_JMP_TRUE);
int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2)); int16_t backOffset = relJumpOffset(p, loopTop, basCodePos(&p->cg));
basEmit16(&p->cg, backOffset); basEmit16(&p->cg, backOffset);
} else if (check(p, TOK_UNTIL)) { } else if (check(p, TOK_UNTIL)) {
advance(p); advance(p);
parseExpression(p); parseExpression(p);
// Jump back to loopTop if condition is false // Jump back to loopTop if condition is false
basEmit8(&p->cg, OP_JMP_FALSE); basEmit8(&p->cg, OP_JMP_FALSE);
int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2)); int16_t backOffset = relJumpOffset(p, loopTop, basCodePos(&p->cg));
basEmit16(&p->cg, backOffset); basEmit16(&p->cg, backOffset);
} else { } else {
// Plain LOOP -- unconditional jump back // Plain LOOP -- unconditional jump back
basEmit8(&p->cg, OP_JMP); basEmit8(&p->cg, OP_JMP);
int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2)); int16_t backOffset = relJumpOffset(p, loopTop, basCodePos(&p->cg));
basEmit16(&p->cg, backOffset); basEmit16(&p->cg, backOffset);
} }
@ -2694,7 +2779,7 @@ static void parseEndForm(BasParserT *p) {
int32_t initLen = basCodePos(&p->cg) - p->formInitCodeStart; int32_t initLen = basCodePos(&p->cg) - p->formInitCodeStart;
// Patch the JMP to skip over the entire init block // Patch the JMP to skip over the entire init block
int16_t offset = (int16_t)(basCodePos(&p->cg) - (p->formInitJmpAddr + 2)); int16_t offset = relJumpOffset(p, basCodePos(&p->cg), p->formInitJmpAddr);
basPatch16(&p->cg, p->formInitJmpAddr, offset); basPatch16(&p->cg, p->formInitJmpAddr, offset);
p->formInitJmpAddr = -1; p->formInitJmpAddr = -1;
@ -2881,12 +2966,12 @@ static void parseFor(BasParserT *p) {
basEmit8(&p->cg, OP_FOR_NEXT); basEmit8(&p->cg, OP_FOR_NEXT);
basEmitU16(&p->cg, (uint16_t)loopVar->index); basEmitU16(&p->cg, (uint16_t)loopVar->index);
basEmit8(&p->cg, (uint8_t)loopVar->scope); basEmit8(&p->cg, (uint8_t)loopVar->scope);
int16_t backOffset = (int16_t)(loopBody - (basCodePos(&p->cg) + 2)); int16_t backOffset = relJumpOffset(p, loopBody, basCodePos(&p->cg));
basEmit16(&p->cg, backOffset); basEmit16(&p->cg, backOffset);
// Patch FOR_INIT's forward skip offset to point past FOR_NEXT. // Patch FOR_INIT's forward skip offset to point past FOR_NEXT.
int32_t loopEnd = basCodePos(&p->cg); int32_t loopEnd = basCodePos(&p->cg);
int16_t skipOffset = (int16_t)(loopEnd - (skipOffsetPos + 2)); int16_t skipOffset = relJumpOffset(p, loopEnd, skipOffsetPos);
p->cg.code[skipOffsetPos] = (uint8_t)(skipOffset & 0xFF); p->cg.code[skipOffsetPos] = (uint8_t)(skipOffset & 0xFF);
p->cg.code[skipOffsetPos + 1] = (uint8_t)((skipOffset >> 8) & 0xFF); p->cg.code[skipOffsetPos + 1] = (uint8_t)((skipOffset >> 8) & 0xFF);
@ -3185,13 +3270,17 @@ static void parseGoto(BasParserT *p) {
labelName[BAS_MAX_TOKEN_LEN - 1] = '\0'; labelName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p); advance(p);
// A GOTO out of one or more SELECT CASE blocks must discard their emitGotoWithSelectPops(p, labelName);
// live test values. Labels are resolved at the procedure level, so }
// a GOTO target is treated as the proc base (selectDepth 0); pop all
// currently-open SELECT values before jumping.
emitSelectPops(p, p->selectDepth);
emitJumpToLabel(p, OP_JMP, labelName);
static void parseIdivExpr(BasParserT *p) {
parseMulDivExpr(p);
while (!p->hasError && check(p, TOK_BACKSLASH)) {
advance(p);
parseMulDivExpr(p);
basEmit8(&p->cg, OP_IDIV_INT);
}
} }
@ -3539,6 +3628,17 @@ static void prescanSignatures(BasParserT *p) {
snprintf(savedErrMsg, sizeof(savedErrMsg), "%s", p->error); snprintf(savedErrMsg, sizeof(savedErrMsg), "%s", p->error);
while (!check(p, TOK_EOF)) { while (!check(p, TOK_EOF)) {
// Best-effort: clear any scan error so we continue to the next
// declaration. The main parse pass will re-surface real errors
// with full location info. This must happen on EVERY iteration:
// advance() refuses to move while hasError is set, so a lexer
// error (TOK_ERROR) left latched would spin this loop forever.
if (p->hasError) {
p->hasError = false;
p->errorLine = 0;
p->error[0] = '\0';
}
// "END SUB" / "END FUNCTION" consume the END and the following // "END SUB" / "END FUNCTION" consume the END and the following
// keyword as separate tokens; skip END so the next-iteration // keyword as separate tokens; skip END so the next-iteration
// SUB/FUNCTION check doesn't misinterpret it as a declaration. // SUB/FUNCTION check doesn't misinterpret it as a declaration.
@ -3686,15 +3786,6 @@ static void prescanSignatures(BasParserT *p) {
sym->isDefined = false; sym->isDefined = false;
sym->codeAddr = 0; sym->codeAddr = 0;
} }
// Best-effort: clear any scan error so we continue to the
// next declaration. The main parse pass will re-surface
// real errors with full location info.
if (p->hasError) {
p->hasError = false;
p->errorLine = 0;
p->error[0] = '\0';
}
} }
p->lex = savedLex; p->lex = savedLex;
@ -3749,16 +3840,6 @@ static void parseModule(BasParserT *p) {
// This makes 8 MOD 3 * 2 == 2 and 10 \ 2 * 3 == 1 (matching QB), and keeps // This makes 8 MOD 3 * 2 == 2 and 10 \ 2 * 3 == 1 (matching QB), and keeps
// -2^2 = -(2^2) = -4. // -2^2 = -(2^2) = -4.
static void parseIdivExpr(BasParserT *p) {
parseMulDivExpr(p);
while (!p->hasError && check(p, TOK_BACKSLASH)) {
advance(p);
parseMulDivExpr(p);
basEmit8(&p->cg, OP_IDIV_INT);
}
}
static void parseMulDivExpr(BasParserT *p) { static void parseMulDivExpr(BasParserT *p) {
parseUnaryExpr(p); parseUnaryExpr(p);
while (!p->hasError) { while (!p->hasError) {
@ -3892,8 +3973,9 @@ static void parseOn(BasParserT *p) {
// After GOSUB returns, jump to end of ON...GOSUB // After GOSUB returns, jump to end of ON...GOSUB
arrput(endJumps, emitJump(p, OP_JMP)); arrput(endJumps, emitJump(p, OP_JMP));
} else { } else {
// GOTO: just jump to the label // GOTO: jump to the label, discarding the live test values
emitJumpToLabel(p, OP_JMP, labelName); // of any SELECT CASE blocks being left (same as plain GOTO).
emitGotoWithSelectPops(p, labelName);
} }
// Patch the skip (no-match continues to next branch) // Patch the skip (no-match continues to next branch)
@ -3914,7 +3996,7 @@ static void parseOn(BasParserT *p) {
int32_t n = (int32_t)arrlen(endJumps); int32_t n = (int32_t)arrlen(endJumps);
for (int32_t i = 0; i < n; i++) { for (int32_t i = 0; i < n; i++) {
int16_t offset = (int16_t)(endTarget - (endJumps[i] + 2)); int16_t offset = relJumpOffset(p, endTarget, endJumps[i]);
basPatch16(&p->cg, endJumps[i], offset); basPatch16(&p->cg, endJumps[i], offset);
} }
@ -3960,7 +4042,7 @@ static void parseOnError(BasParserT *p) {
// Label already defined -- emit ON_ERROR with offset to handler // Label already defined -- emit ON_ERROR with offset to handler
basEmit8(&p->cg, OP_ON_ERROR); basEmit8(&p->cg, OP_ON_ERROR);
int32_t here = basCodePos(&p->cg); int32_t here = basCodePos(&p->cg);
int16_t offset = (int16_t)(sym->codeAddr - (here + 2)); int16_t offset = relJumpOffset(p, sym->codeAddr, here);
basEmit16(&p->cg, offset); basEmit16(&p->cg, offset);
} else { } else {
// Forward reference // Forward reference
@ -4770,10 +4852,7 @@ static void parsePrimary(BasParserT *p) {
} }
} }
expect(p, TOK_RPAREN); expect(p, TOK_RPAREN);
if (argc > BAS_VM_MAX_CALL_ARGS) { if (clampArgCount(p, argc)) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS);
error(p, buf);
return; return;
} }
basEmit8(&p->cg, OP_CALL_METHOD); basEmit8(&p->cg, OP_CALL_METHOD);
@ -5169,15 +5248,7 @@ static void parseSelectCase(BasParserT *p) {
advance(p); advance(p);
if (check(p, TOK_SELECT)) { if (check(p, TOK_SELECT)) {
advance(p); advance(p);
// Patch the per-case end jumps to land ON the pop (not closeSelectCase(p, &endJumps);
// after it) so every matched-case body pops the test
// value exactly once, then fall through to the pop.
for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) {
patchJump(p, endJumps[i]);
}
basEmit8(&p->cg, OP_POP); // pop test expression
arrfree(endJumps);
p->selectDepth--;
return; return;
} }
p->lex = savedLex; p->lex = savedLex;
@ -5205,12 +5276,7 @@ static void parseSelectCase(BasParserT *p) {
advance(p); advance(p);
if (check(p, TOK_SELECT)) { if (check(p, TOK_SELECT)) {
advance(p); advance(p);
for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) { closeSelectCase(p, &endJumps);
patchJump(p, endJumps[i]);
}
basEmit8(&p->cg, OP_POP);
arrfree(endJumps);
p->selectDepth--;
return; return;
} }
p->lex = savedLex; p->lex = savedLex;
@ -5321,12 +5387,7 @@ static void parseSelectCase(BasParserT *p) {
// Both the no-match fall-through (nextCaseJump) and // Both the no-match fall-through (nextCaseJump) and
// earlier matched bodies (endJumps) land ON the pop. // earlier matched bodies (endJumps) land ON the pop.
patchJump(p, nextCaseJump); patchJump(p, nextCaseJump);
for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) { closeSelectCase(p, &endJumps);
patchJump(p, endJumps[i]);
}
basEmit8(&p->cg, OP_POP);
arrfree(endJumps);
p->selectDepth--;
return; return;
} }
p->lex = savedLex; p->lex = savedLex;
@ -5344,15 +5405,7 @@ static void parseSelectCase(BasParserT *p) {
// Reached if EOF hit without END SELECT -- patch pending end jumps // Reached if EOF hit without END SELECT -- patch pending end jumps
// and clean up. End jumps land on the pop, then fall through to it. // and clean up. End jumps land on the pop, then fall through to it.
for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) { closeSelectCase(p, &endJumps);
patchJump(p, endJumps[i]);
}
basEmit8(&p->cg, OP_POP);
arrfree(endJumps);
p->selectDepth--;
if (!p->hasError) { if (!p->hasError) {
error(p, "Expected END SELECT"); error(p, "Expected END SELECT");
@ -5971,10 +6024,7 @@ static void parseStatement(BasParserT *p) {
parseExpression(p); parseExpression(p);
argc++; argc++;
} }
if (argc > BAS_VM_MAX_CALL_ARGS) { if (clampArgCount(p, argc)) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS);
error(p, buf);
return; return;
} }
basEmit8(&p->cg, OP_CALL_METHOD); basEmit8(&p->cg, OP_CALL_METHOD);
@ -6004,10 +6054,7 @@ static void parseStatement(BasParserT *p) {
parseExpression(p); parseExpression(p);
argc++; argc++;
} }
if (argc > BAS_VM_MAX_CALL_ARGS) { if (clampArgCount(p, argc)) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS);
error(p, buf);
return; return;
} }
basEmit8(&p->cg, OP_CALL_METHOD); basEmit8(&p->cg, OP_CALL_METHOD);
@ -6047,12 +6094,16 @@ static void parseStatement(BasParserT *p) {
if (check(p, TOK_COLON)) { if (check(p, TOK_COLON)) {
advance(p); // consume colon advance(p); // consume colon
// Record the label at the current code position // Record the label at the current code position. For
// SYM_LABEL, localCount is repurposed to hold the SELECT
// depth at the definition, so GOTO can pop only the test
// values of the SELECT blocks it actually leaves.
BasSymbolT *sym = basSymTabFind(&p->sym, labelName); BasSymbolT *sym = basSymTabFind(&p->sym, labelName);
if (sym != NULL && sym->kind == SYM_LABEL) { if (sym != NULL && sym->kind == SYM_LABEL) {
// Forward-declared label -- now define it // Forward-declared label -- now define it
sym->codeAddr = basCodePos(&p->cg); sym->codeAddr = basCodePos(&p->cg);
sym->isDefined = true; sym->isDefined = true;
sym->localCount = p->selectDepth;
patchLabelRefs(p, sym); patchLabelRefs(p, sym);
} else if (sym == NULL) { } else if (sym == NULL) {
sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0); sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0);
@ -6060,9 +6111,10 @@ static void parseStatement(BasParserT *p) {
error(p, "Symbol table full"); error(p, "Symbol table full");
break; break;
} }
sym->scope = SCOPE_GLOBAL; sym->scope = SCOPE_GLOBAL;
sym->isDefined = true; sym->isDefined = true;
sym->codeAddr = basCodePos(&p->cg); sym->codeAddr = basCodePos(&p->cg);
sym->localCount = p->selectDepth;
} else { } else {
char buf[BAS_PARSE_ERR_SCRATCH]; char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Name '%s' already used", labelName); snprintf(buf, sizeof(buf), "Name '%s' already used", labelName);
@ -6524,7 +6576,7 @@ static void parseWhile(BasParserT *p) {
// Jump back to loop top // Jump back to loop top
basEmit8(&p->cg, OP_JMP); basEmit8(&p->cg, OP_JMP);
int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2)); int16_t backOffset = relJumpOffset(p, loopTop, basCodePos(&p->cg));
basEmit16(&p->cg, backOffset); basEmit16(&p->cg, backOffset);
// Patch the false jump to exit // Patch the false jump to exit
@ -6612,7 +6664,7 @@ static void patchCallAddrs(BasParserT *p, BasSymbolT *sym) {
static void patchJump(BasParserT *p, int32_t addr) { static void patchJump(BasParserT *p, int32_t addr) {
int32_t target = basCodePos(&p->cg); int32_t target = basCodePos(&p->cg);
int16_t offset = (int16_t)(target - (addr + 2)); int16_t offset = relJumpOffset(p, target, addr);
basPatch16(&p->cg, addr, offset); basPatch16(&p->cg, addr, offset);
} }
@ -6623,7 +6675,36 @@ static void patchLabelRefs(BasParserT *p, BasSymbolT *sym) {
for (int32_t i = 0; i < sym->patchCount; i++) { for (int32_t i = 0; i < sym->patchCount; i++) {
int32_t patchAddr = sym->patchAddrs[i]; int32_t patchAddr = sym->patchAddrs[i];
int16_t offset = (int16_t)(target - (patchAddr + 2));
// A forward GOTO reserves one OP_NOP per SELECT open at its site
// directly before the jump opcode and stores that count in the
// placeholder operand (all other label refs leave it at 0). Now
// that the label's own SELECT depth is known (sym->localCount),
// convert one NOP to OP_POP per block the jump actually leaves.
if (patchAddr + 2 <= p->cg.codeLen) {
int16_t gotoDepth = 0;
memcpy(&gotoDepth, &p->cg.code[patchAddr], sizeof(gotoDepth));
if (gotoDepth > 0) {
int32_t pops = gotoDepth - sym->localCount;
if (pops > gotoDepth) {
pops = gotoDepth;
}
int32_t nopStart = patchAddr - 1 - gotoDepth;
if (nopStart >= 0) {
for (int32_t j = 0; j < pops; j++) {
if (p->cg.code[nopStart + j] == OP_NOP) {
p->cg.code[nopStart + j] = OP_POP;
}
}
}
}
}
int16_t offset = relJumpOffset(p, target, patchAddr);
basPatch16(&p->cg, patchAddr, offset); basPatch16(&p->cg, patchAddr, offset);
} }
@ -6631,6 +6712,21 @@ static void patchLabelRefs(BasParserT *p, BasSymbolT *sym) {
} }
// Compute the operand for a relative jump whose 2-byte operand lives at
// operandAddr. A span outside the signed 16-bit range would silently
// wrap and jump somewhere wild, so surface it as a compile error; the
// module-size check in basParse only bounds absolute call addresses.
static int16_t relJumpOffset(BasParserT *p, int32_t target, int32_t operandAddr) {
int32_t span = target - (operandAddr + BAS_JUMP_OPERAND_SIZE);
if (span < INT16_MIN || span > INT16_MAX) {
error(p, "Jump distance exceeds 16-bit limit");
}
return (int16_t)span;
}
static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName) { static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName) {
for (int32_t i = 0; i < typeSym->fieldCount; i++) { for (int32_t i = 0; i < typeSym->fieldCount; i++) {
const char *a = typeSym->fields[i].name; const char *a = typeSym->fields[i].name;

File diff suppressed because it is too large Load diff

View file

@ -56,7 +56,6 @@ typedef struct BasControlT BasControlT;
#define BAS_MAX_FORM_NAME 64 #define BAS_MAX_FORM_NAME 64
#define BAS_MAX_FRM_LINE_LEN 512 #define BAS_MAX_FRM_LINE_LEN 512
#define BAS_MAX_FRM_NESTING 16 #define BAS_MAX_FRM_NESTING 16
#define BAS_MAX_PENDING_UNLOAD 8
// ============================================================ // ============================================================
// Menu ID to name mapping for event dispatch // Menu ID to name mapping for event dispatch
@ -153,6 +152,11 @@ typedef struct BasFormT {
// cancelled) until the form is freed. Blocks re-entrant Unload of // cancelled) until the form is freed. Blocks re-entrant Unload of
// the same form from its own Unload/QueryUnload handlers. // the same form from its own Unload/QueryUnload handlers.
bool unloading; bool unloading;
// True once detachFormForUnload has run: the form is out of
// rt->forms, its window is hidden, and it is event-inert
// (fireCtrlEvent drops events for it). Distinct from `unloading`,
// which is also true while QueryUnload/Unload still fire.
bool detached;
// Synthetic control entry for the form itself, so that // Synthetic control entry for the form itself, so that
// FormName.Property works through the same getProp/setProp path. // FormName.Property works through the same getProp/setProp path.
BasControlT formCtrl; BasControlT formCtrl;
@ -199,16 +203,18 @@ typedef struct {
// were seeing when the dialog's event pump raced with other // were seeing when the dialog's event pump raced with other
// queued events. // queued events.
bool suppressErrorDialog; bool suppressErrorDialog;
// Deferred unload. While eventDepth > 0 a VM event handler is on // Deferred teardown. While eventDepth > 0 some frame on the stack
// the stack, so Unload must not free the form's vars and controls // still holds BASIC form/control pointers -- a VM event handler, or
// out from under it (the VM dereferences currentFormVars for every // a native bridge that reads them after firing (see the rule-N
// form-variable access, and fireCtrlEvent writes into the firing // brackets in formrt.c) -- so Unload/RemoveControl must not free
// control after the handler returns). QueryUnload/Unload events // form vars, BasControlT structs, or BasFormT out from under it.
// fire immediately; the frees are queued here and flushed when the // QueryUnload/Unload events fire immediately and the object is
// outermost handler returns (see rtEventLeave). // detached (unfindable, hidden, event-inert); only the frees are
// queued here, in stb_ds dynamic arrays with no cap, and flushed
// when the outermost frame returns (see rtEventLeave).
int32_t eventDepth; int32_t eventDepth;
BasFormT *pendingUnload[BAS_MAX_PENDING_UNLOAD]; BasFormT **pendingUnload; // stb_ds array of detached forms
int32_t pendingUnloadCount; BasControlT **pendingCtrlFree; // stb_ds array of removed controls
// Name of the most recent basFormRtFindCtrl lookup. The VM's // Name of the most recent basFormRtFindCtrl lookup. The VM's
// OP_FIND_CTRL opcode discards the name string after the lookup, // OP_FIND_CTRL opcode discards the name string after the lookup,
// so when a subsequent OP_CALL_METHOD / OP_SET_PROP sees a NULL // so when a subsequent OP_CALL_METHOD / OP_SET_PROP sees a NULL

View file

@ -76,7 +76,6 @@ static void dsgnLoad_onFormProp(void *userData, const char *key, const c
static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level); static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level);
static void dsgnLoad_onMenuEnd(void *userData); static void dsgnLoad_onMenuEnd(void *userData);
static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *value); static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *value);
static int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...);
static int32_t emitPad(char *buf, int32_t bufSize, int32_t pos, int32_t count); static int32_t emitPad(char *buf, int32_t bufSize, int32_t pos, int32_t count);
static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y); static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y);
static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y); static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y);
@ -267,14 +266,21 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
if (layout && layout[0] && strcasecmp(layout, "VBox") != 0) { if (layout && layout[0] && strcasecmp(layout, "VBox") != 0) {
// Check if we already created a content box inside. // Check if we already created a content box inside.
// pc is non-NULL, so a NULL firstChild->userData also // pc is non-NULL, so a NULL firstChild->userData also
// fails the != pc test -- no separate NULL check needed. // fails the == pc test -- no separate NULL check needed.
if (!parent->firstChild || if (parent->firstChild &&
parent->firstChild->userData != (void *)pc) { parent->firstChild->userData == (void *)pc) {
parent = parent->firstChild;
} else {
WidgetT *box = dsgnCreateContentBox(parent, layout); WidgetT *box = dsgnCreateContentBox(parent, layout);
box->userData = (void *)pc;
}
parent = parent->firstChild; // dsgnCreateContentBox returns the container itself
// when the layout type is not a loaded parent
// container; only tag and descend into a real box.
if (box != parent) {
box->userData = (void *)pc;
parent = box;
}
}
} }
break; break;
@ -318,7 +324,10 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
w->weight = ctrl->weight; w->weight = ctrl->weight;
wgtSetVisible(w, ctrl->visible); // Do not apply ctrl->visible here: hidden widgets get no layout
// geometry, which would make a Visible=False control invisible
// and unclickable on the design canvas. Visible is stored on the
// control and applied by the runtime loader instead.
wgtSetEnabled(w, ctrl->enabled); wgtSetEnabled(w, ctrl->enabled);
// Apply interface properties (Alignment, etc.) from FRM data // Apply interface properties (Alignment, etc.) from FRM data
@ -383,21 +392,7 @@ void dsgnFree(DsgnStateT *ds) {
// Timer's "Enabled" maps to its running state, VB-style) without producing a // Timer's "Enabled" maps to its running state, VB-style) without producing a
// duplicate grid row or a conflicting saved line. // duplicate grid row or a conflicting saved line.
bool dsgnIfaceHasProp(const char *typeName, const char *propName) { bool dsgnIfaceHasProp(const char *typeName, const char *propName) {
const char *wgtName = wgtFindByBasName(typeName); return findIfaceProp(typeName, propName) != NULL;
if (wgtName) {
const WgtIfaceT *iface = wgtGetIface(wgtName);
if (iface) {
for (int32_t i = 0; i < iface->propCount; i++) {
if (strcasecmp(iface->props[i].name, propName) == 0) {
return true;
}
}
}
}
return false;
} }
@ -583,9 +578,9 @@ static void dsgnLoad_onCtrlProp(void *userData, const char *key, const char *val
} else if (strcasecmp(key, "HelpTopic") == 0) { } else if (strcasecmp(key, "HelpTopic") == 0) {
snprintf(cc->helpTopic, DSGN_MAX_NAME, "%s", val); snprintf(cc->helpTopic, DSGN_MAX_NAME, "%s", val);
} else if (strcasecmp(key, "Visible") == 0 && !dsgnIfaceHasProp(cc->typeName, "Visible")) { } else if (strcasecmp(key, "Visible") == 0 && !dsgnIfaceHasProp(cc->typeName, "Visible")) {
cc->visible = (strcasecmp(val, "False") != 0); cc->visible = frmParseBool(val);
} else if (strcasecmp(key, "Enabled") == 0 && !dsgnIfaceHasProp(cc->typeName, "Enabled")) { } else if (strcasecmp(key, "Enabled") == 0 && !dsgnIfaceHasProp(cc->typeName, "Enabled")) {
cc->enabled = (strcasecmp(val, "False") != 0); cc->enabled = frmParseBool(val);
} else if (strcasecmp(key, "TabIndex") == 0) { } else if (strcasecmp(key, "TabIndex") == 0) {
// ignored -- DVX has no tab order // ignored -- DVX has no tab order
} else { } else {
@ -771,21 +766,25 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
// Delete key -- remove the selected control and any children // Delete key -- remove the selected control and any children
if (key == KEY_DELETE && ds->selectedIdx >= 0 && ds->selectedIdx < count) { if (key == KEY_DELETE && ds->selectedIdx >= 0 && ds->selectedIdx < count) {
// Build the full set of names to delete (the selected control and // Build the full set of controls to delete (the selected control and
// every descendant, transitively). parentName references only the // every descendant, transitively). parentName references only the
// immediate parent, so a single-level child pass would orphan // immediate parent, so a single-level child pass would orphan
// grandchildren -- saveControls then silently drops them because their // grandchildren -- saveControls then silently drops them because their
// parentName matches no surviving container. The names are wrapped in // parentName matches no surviving container. Controls are identified
// a struct so arrput (which assigns via =) is a legal struct copy; a // by name plus array index, because control array members share one
// bare char[] element type would not compile. // name and differ only by ->index. The keys are wrapped in a struct
// so arrput (which assigns via =) is a legal struct copy; a bare
// char[] element type would not compile.
typedef struct { typedef struct {
char name[DSGN_MAX_NAME]; char name[DSGN_MAX_NAME];
} DelNameT; int32_t index;
} DelKeyT;
DelNameT *delSet = NULL; DelKeyT *delSet = NULL;
DelNameT seed; DelKeyT seed;
snprintf(seed.name, DSGN_MAX_NAME, "%s", ds->form->controls[ds->selectedIdx]->name); snprintf(seed.name, DSGN_MAX_NAME, "%s", ds->form->controls[ds->selectedIdx]->name);
seed.index = ds->form->controls[ds->selectedIdx]->index;
arrput(delSet, seed); arrput(delSet, seed);
bool grew = true; bool grew = true;
@ -796,28 +795,49 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
const char *pName = ds->form->controls[i]->parentName; const char *pName = ds->form->controls[i]->parentName;
const char *cName = ds->form->controls[i]->name; const char *cName = ds->form->controls[i]->name;
int32_t cIdx = ds->form->controls[i]->index;
if (pName[0] == '\0') { if (pName[0] == '\0') {
continue; continue;
} }
bool parentInSet = false; bool selfInSet = false;
bool selfInSet = false;
for (int32_t s = 0; s < (int32_t)arrlen(delSet); s++) { for (int32_t s = 0; s < (int32_t)arrlen(delSet); s++) {
if (strcasecmp(delSet[s].name, pName) == 0) { if (strcasecmp(delSet[s].name, cName) == 0 && delSet[s].index == cIdx) {
parentInSet = true;
}
if (strcasecmp(delSet[s].name, cName) == 0) {
selfInSet = true; selfInSet = true;
break;
} }
} }
if (parentInSet && !selfInSet) { if (selfInSet) {
DelNameT add; continue;
}
// parentName cannot name a specific array member, so the
// parent counts as deleted only when every control bearing
// that name is already in the set -- a surviving member keeps
// the child's parent reference valid.
int32_t nameCount = 0;
int32_t setCount = 0;
for (int32_t j = 0; j < count; j++) {
if (strcasecmp(ds->form->controls[j]->name, pName) == 0) {
nameCount++;
}
}
for (int32_t s = 0; s < (int32_t)arrlen(delSet); s++) {
if (strcasecmp(delSet[s].name, pName) == 0) {
setCount++;
}
}
if (setCount > 0 && setCount == nameCount) {
DelKeyT add;
snprintf(add.name, DSGN_MAX_NAME, "%s", cName); snprintf(add.name, DSGN_MAX_NAME, "%s", cName);
add.index = cIdx;
arrput(delSet, add); arrput(delSet, add);
grew = true; grew = true;
} }
@ -830,7 +850,7 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
bool remove = false; bool remove = false;
for (int32_t s = 0; s < (int32_t)arrlen(delSet); s++) { for (int32_t s = 0; s < (int32_t)arrlen(delSet); s++) {
if (strcasecmp(delSet[s].name, ds->form->controls[i]->name) == 0) { if (strcasecmp(delSet[s].name, ds->form->controls[i]->name) == 0 && delSet[s].index == ds->form->controls[i]->index) {
remove = true; remove = true;
break; break;
} }
@ -1005,13 +1025,20 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
// Mirror dsgnCreateWidgets: non-VBox containers nest // Mirror dsgnCreateWidgets: non-VBox containers nest
// children inside a tagged content box so the live designer // children inside a tagged content box so the live designer
// layout matches the rebuilt/saved/runtime form. // layout matches the rebuilt/saved/runtime form.
if (!parentWidget->firstChild || !parentWidget->firstChild->userData || if (parentWidget->firstChild &&
parentWidget->firstChild->userData != (void *)pc) { parentWidget->firstChild->userData == (void *)pc) {
parentWidget = parentWidget->firstChild;
} else {
WidgetT *box = dsgnCreateContentBox(parentWidget, layout); WidgetT *box = dsgnCreateContentBox(parentWidget, layout);
box->userData = (void *)pc;
}
parentWidget = parentWidget->firstChild; // dsgnCreateContentBox returns the container itself
// when the layout type is not a loaded parent
// container; only tag and descend into a real box.
if (box != parentWidget) {
box->userData = (void *)pc;
parentWidget = box;
}
}
} }
break; break;
@ -1241,7 +1268,12 @@ const char *dsgnSelectedName(const DsgnStateT *ds) {
} }
static int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) { // Clamped formatted append into a fixed buffer. Returns pos unchanged once
// pos has reached bufSize; otherwise performs a size-clamped vsnprintf and
// returns the new position, capped at bufSize-1 on truncation. This avoids
// the snprintf-accumulation overrun where bufSize-pos wraps negative.
// Shared by ideDesigner.c and ideMain.c via ideDesigner.h.
int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) {
va_list args; va_list args;
int32_t avail; int32_t avail;
int32_t written; int32_t written;

View file

@ -215,10 +215,19 @@ void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t
// Check if a control type is a container (can hold children). // Check if a control type is a container (can hold children).
bool dsgnIsContainer(const char *typeName); bool dsgnIsContainer(const char *typeName);
// Look up a widget type's interface property descriptor by name, or NULL if
// the type has no such interface property. Shared IDE-internal helper.
const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName);
// True if the widget type registers an interface property of this name. The // True if the widget type registers an interface property of this name. The
// generic Visible/Enabled designer rows defer to such a property. // generic Visible/Enabled designer rows defer to such a property.
bool dsgnIfaceHasProp(const char *typeName, const char *propName); bool dsgnIfaceHasProp(const char *typeName, const char *propName);
// Clamped formatted append into a fixed buffer. Returns pos unchanged once
// pos has reached bufSize; otherwise performs a size-clamped vsnprintf and
// returns the new position, capped at bufSize-1 on truncation.
int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...);
// Free designer resources. // Free designer resources.
void dsgnFree(DsgnStateT *ds); void dsgnFree(DsgnStateT *ds);

View file

@ -213,10 +213,10 @@ static bool doEventsCallback(void *ctx);
static void dsgnCopySelected(void); static void dsgnCopySelected(void);
static void dsgnPasteControl(void); static void dsgnPasteControl(void);
static int32_t editorLineToCodeLine(int32_t editorLine); static int32_t editorLineToCodeLine(int32_t editorLine);
static int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...);
static void ensureProject(const char *filePath); static void ensureProject(const char *filePath);
static void evaluateImmediate(const char *expr); static void evaluateImmediate(const char *expr);
static bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize); static bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize);
static bool evtLabelMatches(const char *label, const char *evtName);
static char *extractNewProcs(const char *buf); static char *extractNewProcs(const char *buf);
static const BasDebugVarT *findDebugVar(const char *name); static const BasDebugVarT *findDebugVar(const char *name);
static bool findInProject(const char *needle, bool caseSensitive, bool forward); static bool findInProject(const char *needle, bool caseSensitive, bool forward);
@ -319,6 +319,7 @@ static void parseProcs(const char *source);
static void prefsUpdateColorSliders(void); static void prefsUpdateColorSliders(void);
static void prefsUpdateSwatch(void); static void prefsUpdateSwatch(void);
static void printCallback(void *ctx, const char *text, bool newline); static void printCallback(void *ctx, const char *text, bool newline);
static bool procBufContains(const char *hay, const char *needle, int32_t needleLen, bool caseSensitive);
static bool promptAndSave(void); static bool promptAndSave(void);
static bool readDebugVar(const BasDebugVarT *dv, BasValueT *outVal); static bool readDebugVar(const BasDebugVarT *dv, BasValueT *outVal);
static void recentAdd(const char *path); static void recentAdd(const char *path);
@ -1686,10 +1687,8 @@ static bool ideValidator_isPropValid(void *ctx, const char *wgtType, const char
return true; return true;
} }
for (int32_t i = 0; i < iface->propCount; i++) { if (wgtIfaceFindProp(iface, propName)) {
if (strcasecmp(iface->props[i].name, propName) == 0) { return true;
return true;
}
} }
return false; return false;
@ -2721,39 +2720,6 @@ static int32_t editorLineToCodeLine(int32_t editorLine) {
} }
// Clamped formatted append into a fixed buffer. Returns pos unchanged once
// pos has reached bufSize; otherwise performs a size-clamped vsnprintf and
// returns the new position, capped at bufSize-1 on truncation. This avoids
// the snprintf-accumulation overrun where bufSize-pos wraps negative.
static int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) {
va_list args;
int32_t avail;
int32_t written;
if (!buf || pos < 0 || pos >= bufSize) {
return pos;
}
avail = bufSize - pos;
va_start(args, fmt);
written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args);
va_end(args);
if (written < 0) {
return pos;
}
if (written >= avail) {
// Truncated: vsnprintf wrote avail-1 chars + NUL. Advance to
// the last writable byte before the NUL so we never exceed bufSize-1.
return bufSize - 1;
}
return pos + written;
}
// Auto-create an implicit project when opening a file without one. // Auto-create an implicit project when opening a file without one.
// Derives project name and directory from the file path. Also adds // Derives project name and directory from the file path. Also adds
// a matching .frm file if one exists alongside the .bas. // a matching .frm file if one exists alongside the .bas.
@ -3036,6 +3002,20 @@ static bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize) {
} }
// evtLabelMatches -- compare an Event dropdown label against an event
// name, ignoring the [] brackets that mark unimplemented events.
static bool evtLabelMatches(const char *label, const char *evtName) {
if (label[0] == '[') {
size_t nameLen = strlen(evtName);
return strncasecmp(label + 1, evtName, nameLen) == 0 && label[nameLen + 1] == ']' && label[nameLen + 2] == '\0';
}
return strcasecmp(label, evtName) == 0;
}
// extractNewProcs -- scan a buffer for Sub/Function declarations that // extractNewProcs -- scan a buffer for Sub/Function declarations that
// don't belong (e.g. user typed a new Sub in the General section). // don't belong (e.g. user typed a new Sub in the General section).
// Extracts them into new sProcBufs entries and removes them from the // Extracts them into new sProcBufs entries and removes them from the
@ -3232,20 +3212,6 @@ static const BasDebugVarT *findDebugVar(const char *name) {
} }
// procBufContains -- true if needle occurs anywhere in a proc buffer.
static bool procBufContains(const char *hay, const char *needle, int32_t needleLen, bool caseSensitive) {
if (!hay) {
return false;
}
if (caseSensitive) {
return strstr(hay, needle) != NULL;
}
return findSubstrNoCase(hay, needle, needleLen) != NULL;
}
static bool findInProject(const char *needle, bool caseSensitive, bool forward) { static bool findInProject(const char *needle, bool caseSensitive, bool forward) {
if (!needle || !needle[0] || sProject.fileCount == 0) { if (!needle || !needle[0] || sProject.fileCount == 0) {
return false; return false;
@ -3279,14 +3245,22 @@ static bool findInProject(const char *needle, bool caseSensitive, bool forward)
// procs before the current one (descending), then General, then // procs before the current one (descending), then General, then
// procs after it (descending). // procs after it (descending).
if (forward) { if (forward) {
for (int32_t p = sCurProcIdx + 1; p < procCount; p++) { // sCurProcIdx is -2 when stashCurrentFile discarded an empty
// skeleton proc, so clamp the scan start to the first proc.
int32_t firstProc = sCurProcIdx + 1;
if (firstProc < 0) {
firstProc = 0;
}
for (int32_t p = firstProc; p < procCount; p++) {
if (procBufContains(sProcBufs[p], needle, needleLen, caseSensitive)) { if (procBufContains(sProcBufs[p], needle, needleLen, caseSensitive)) {
showProcAndFind(p, needle, caseSensitive, forward); showProcAndFind(p, needle, caseSensitive, forward);
return true; return true;
} }
} }
if (sCurProcIdx >= 0 && procBufContains(sGeneralBuf, needle, needleLen, caseSensitive)) { if (sCurProcIdx != -1 && procBufContains(sGeneralBuf, needle, needleLen, caseSensitive)) {
showProcAndFind(-1, needle, caseSensitive, forward); showProcAndFind(-1, needle, caseSensitive, forward);
return true; return true;
} }
@ -6801,13 +6775,7 @@ static void onObjDropdownChange(WidgetT *w) {
// Find the default event in the list (it will be bracketed) // Find the default event in the list (it will be bracketed)
for (int32_t i = 0; i < evtCount; i++) { for (int32_t i = 0; i < evtCount; i++) {
const char *label = sEvtItems[i]; if (evtLabelMatches(sEvtItems[i], defEvt)) {
if (label[0] == '[') {
label++;
}
if (strcasecmp(label, defEvt) == 0) {
wgtDropdownSetSelected(sEvtDropdown, i); wgtDropdownSetSelected(sEvtDropdown, i);
onEvtDropdownChange(sEvtDropdown); onEvtDropdownChange(sEvtDropdown);
break; break;
@ -6906,6 +6874,8 @@ static void onReplaceAll(WidgetT *w) {
if (scope == ScopeFuncE && sEditor) { if (scope == ScopeFuncE && sEditor) {
totalCount = wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens); totalCount = wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
} else if (scope == ScopeProjE) { } else if (scope == ScopeProjE) {
int32_t needleLen = (int32_t)strlen(sFindText);
stashCurrentFile(); stashCurrentFile();
for (int32_t i = 0; i < sProject.fileCount; i++) { for (int32_t i = 0; i < sProject.fileCount; i++) {
@ -6914,10 +6884,18 @@ static void onReplaceAll(WidgetT *w) {
// wgtTextAreaReplaceAll only touches the proc currently shown // wgtTextAreaReplaceAll only touches the proc currently shown
// in the editor, so visit every proc of this file (General + // in the editor, so visit every proc of this file (General +
// each Sub/Function), exactly as the ScopeFileE path does -- // each Sub/Function), exactly as the ScopeFileE path does --
// otherwise only the (General) section gets replaced. // otherwise only the (General) section gets replaced. The
int32_t procCount = (int32_t)arrlen(sProcBufs); // buffers are in sync after stashCurrentFile/activateFile, so
// procs without a match are skipped instead of paying for a
// full editor reload on every proc. The bound is re-read each
// pass because showProc can discard an empty skeleton proc.
for (int32_t p = -1; p < (int32_t)arrlen(sProcBufs); p++) {
const char *procBuf = (p == -1) ? sGeneralBuf : sProcBufs[p];
if (!procBufContains(procBuf, sFindText, needleLen, caseSens)) {
continue;
}
for (int32_t p = -1; p < procCount; p++) {
showProc(p); showProc(p);
if (sEditor) { if (sEditor) {
@ -7392,6 +7370,20 @@ static void printCallback(void *ctx, const char *text, bool newline) {
} }
// procBufContains -- true if needle occurs anywhere in a proc buffer.
static bool procBufContains(const char *hay, const char *needle, int32_t needleLen, bool caseSensitive) {
if (!hay) {
return false;
}
if (caseSensitive) {
return strstr(hay, needle) != NULL;
}
return findSubstrNoCase(hay, needle, needleLen) != NULL;
}
// ============================================================ // ============================================================
// promptAndSave -- ask user to save, discard, or cancel // promptAndSave -- ask user to save, discard, or cancel
// ============================================================ // ============================================================
@ -8372,13 +8364,7 @@ static void selectDropdowns(const char *objName, const char *evtName) {
int32_t evtCount = (int32_t)arrlen(sEvtItems); int32_t evtCount = (int32_t)arrlen(sEvtItems);
for (int32_t i = 0; i < evtCount; i++) { for (int32_t i = 0; i < evtCount; i++) {
const char *label = sEvtItems[i]; if (evtLabelMatches(sEvtItems[i], evtName)) {
if (label[0] == '[') {
label++;
}
if (strcasecmp(label, evtName) == 0) {
wgtDropdownSetSelected(sEvtDropdown, i); wgtDropdownSetSelected(sEvtDropdown, i);
break; break;
} }

View file

@ -358,70 +358,6 @@ static void ppdOnOk(WidgetT *w) {
} }
// Set prj->projectDir to the directory containing dbpPath, or "." when
// dbpPath has no directory component.
static void prjDeriveDir(PrjStateT *prj, const char *dbpPath) {
snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath);
char *sep = platformPathDirEnd(prj->projectDir);
if (sep) {
*sep = '\0';
} else {
prj->projectDir[0] = '.';
prj->projectDir[1] = '\0';
}
}
// Load File0, File1, ... entries from an INI section, registering each as a
// project file. The loop ends at the first missing key (no fixed cap).
static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, const char *section, bool isForm) {
for (int32_t i = 0; ; i++) {
char key[PRJ_INI_KEY_LEN];
snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)i);
const char *val = prefsGetString(h, section, key, NULL);
if (!val) {
break;
}
prjAddFile(prj, val, isForm);
}
}
// Return path relative to prj->projectDir when path lives under it, else NULL
// (callers apply their own fallback).
static const char *prjMakeRelative(const PrjStateT *prj, const char *path) {
int32_t dirLen = (int32_t)strlen(prj->projectDir);
if (strncasecmp(path, prj->projectDir, dirLen) == 0 &&
(path[dirLen] == '/' || path[dirLen] == '\\')) {
return path + dirLen + 1;
}
return NULL;
}
// Write the project files whose isForm flag matches into an INI section as
// File0, File1, ... plus a Count entry.
static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char *section, bool isForm) {
int32_t idx = 0;
for (int32_t i = 0; i < prj->fileCount; i++) {
if (prj->files[i].isForm == isForm) {
char key[PRJ_INI_KEY_LEN];
snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)idx++);
prefsSetString(h, section, key, prj->files[i].path);
}
}
prefsSetInt(h, section, PRJ_COUNT_KEY, idx);
}
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm) { int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm) {
PrjFileT entry; PrjFileT entry;
memset(&entry, 0, sizeof(entry)); memset(&entry, 0, sizeof(entry));
@ -468,6 +404,21 @@ WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onCli
} }
// Set prj->projectDir to the directory containing dbpPath, or "." when
// dbpPath has no directory component.
static void prjDeriveDir(PrjStateT *prj, const char *dbpPath) {
snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath);
char *sep = platformPathDirEnd(prj->projectDir);
if (sep) {
*sep = '\0';
} else {
prj->projectDir[0] = '.';
prj->projectDir[1] = '\0';
}
}
void prjDestroyWindow(AppContextT *ctx, WindowT *win) { void prjDestroyWindow(AppContextT *ctx, WindowT *win) {
if (win) { if (win) {
dvxDestroyWindow(ctx, win); dvxDestroyWindow(ctx, win);
@ -600,24 +551,7 @@ void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) {
// Extract form name from .frm files // Extract form name from .frm files
if (prj->files[i].isForm) { if (prj->files[i].isForm) {
const char *pos = buf; basExtractFormName(buf, prj->files[i].formName, PRJ_MAX_NAME);
while (*pos) {
pos = dvxSkipWs(pos);
if (strncasecmp(pos, "Begin Form ", sizeof("Begin Form ") - 1) == 0) {
const char *np = dvxSkipWs(pos + sizeof("Begin Form ") - 1);
int32_t n = 0;
while (*np && *np != ' ' && *np != '\t' && *np != '\r' && *np != '\n' && n < PRJ_MAX_NAME - 1) {
prj->files[i].formName[n++] = *np++;
}
prj->files[i].formName[n] = '\0';
break;
}
while (*pos && *pos != '\n') { pos++; }
if (*pos == '\n') { pos++; }
}
} }
// Yield between files to keep the UI responsive // Yield between files to keep the UI responsive
@ -628,6 +562,38 @@ void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) {
} }
// Load File0, File1, ... entries from an INI section, registering each as a
// project file. The loop ends at the first missing key (no fixed cap).
static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, const char *section, bool isForm) {
for (int32_t i = 0; ; i++) {
char key[PRJ_INI_KEY_LEN];
snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)i);
const char *val = prefsGetString(h, section, key, NULL);
if (!val) {
break;
}
prjAddFile(prj, val, isForm);
}
}
// Return path relative to prj->projectDir when path lives under it, else NULL
// (callers apply their own fallback).
static const char *prjMakeRelative(const PrjStateT *prj, const char *path) {
int32_t dirLen = (int32_t)strlen(prj->projectDir);
if (strncasecmp(path, prj->projectDir, dirLen) == 0 &&
(path[dirLen] == '/' || path[dirLen] == '\\')) {
return path + dirLen + 1;
}
return NULL;
}
bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine) { bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine) {
for (int32_t i = 0; i < prj->sourceMapCount; i++) { for (int32_t i = 0; i < prj->sourceMapCount; i++) {
const PrjSourceMapT *m = &prj->sourceMap[i]; const PrjSourceMapT *m = &prj->sourceMap[i];
@ -999,6 +965,23 @@ bool prjSaveAs(PrjStateT *prj, const char *dbpPath) {
} }
// Write the project files whose isForm flag matches into an INI section as
// File0, File1, ... plus a Count entry.
static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char *section, bool isForm) {
int32_t idx = 0;
for (int32_t i = 0; i < prj->fileCount; i++) {
if (prj->files[i].isForm == isForm) {
char key[PRJ_INI_KEY_LEN];
snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)idx++);
prefsSetString(h, section, key, prj->files[i].path);
}
}
prefsSetInt(h, section, PRJ_COUNT_KEY, idx);
}
// validateIcon -- check that an image file is a valid 32x32 icon. // validateIcon -- check that an image file is a valid 32x32 icon.
// Returns true if valid. Shows an error dialog and returns false if not. // Returns true if valid. Shows an error dialog and returns false if not.
static bool validateIcon(const char *fullPath, bool showErrors) { static bool validateIcon(const char *fullPath, bool showErrors) {

View file

@ -96,6 +96,7 @@ static char **sTreeLabels = NULL; // stb_ds array of strdup'd strings
#define PRP_CELL_COLUMNS 2 // property grid: name column + value column #define PRP_CELL_COLUMNS 2 // property grid: name column + value column
#define PRP_MAX_LAYOUT_NAMES 32 // designer dropdown: max layout container types #define PRP_MAX_LAYOUT_NAMES 32 // designer dropdown: max layout container types
#define PRP_MAX_DATA_NAMES 16 // designer dropdown: max Data controls (excludes "(none)") #define PRP_MAX_DATA_NAMES 16 // designer dropdown: max Data controls (excludes "(none)")
#define PRP_MAX_NEST_DEPTH 32 // container recursion cap; matches DSGN_MAX_NEST_DEPTH in ideDesigner.c
static char **sCellData = NULL; // stb_ds array of strdup'd strings static char **sCellData = NULL; // stb_ds array of strdup'd strings
@ -104,9 +105,8 @@ static char **sCellData = NULL; // stb_ds array of strdup'd strings
// ============================================================ // ============================================================
static void addPropRow(const char *name, const char *value); static void addPropRow(const char *name, const char *value);
static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visible, bool enabled); static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visible, bool enabled, int32_t depth);
static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, const char *parentName); static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, const char *parentName);
static const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName);
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index); static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index);
static void freeCellData(void); static void freeCellData(void);
static void freeTreeLabels(void); static void freeTreeLabels(void);
@ -131,7 +131,7 @@ static void addPropRow(const char *name, const char *value) {
// Recursively apply Visible or Enabled to all descendants of a // Recursively apply Visible or Enabled to all descendants of a
// container control. // container control.
static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visible, bool enabled) { static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visible, bool enabled, int32_t depth) {
int32_t count = (int32_t)arrlen(ds->form->controls); int32_t count = (int32_t)arrlen(ds->form->controls);
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
@ -146,9 +146,13 @@ static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visib
wgtSetEnabled(child->widget, enabled); wgtSetEnabled(child->widget, enabled);
} }
// Recurse into nested containers // Recurse into nested containers. Guard against a self-parenting
if (dsgnIsContainer(child->typeName)) { // container and runaway cycles (A->B->A) via a depth cap so the
cascadeToChildren(ds, child->name, visible, enabled); // recursion always terminates.
if (dsgnIsContainer(child->typeName) &&
strcasecmp(child->name, parentName) != 0 &&
depth < PRP_MAX_NEST_DEPTH) {
cascadeToChildren(ds, child->name, visible, enabled, depth + 1);
} }
} }
} }
@ -185,7 +189,9 @@ static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t src
} }
static const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName) { // Shared IDE-internal helper (declared in ideDesigner.h); also wrapped by
// dsgnIfaceHasProp.
const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName) {
if (!typeName || !typeName[0]) { if (!typeName || !typeName[0]) {
return NULL; return NULL;
} }
@ -198,41 +204,11 @@ static const WgtPropDescT *findIfaceProp(const char *typeName, const char *propN
const WgtIfaceT *iface = wgtGetIface(wgtName); const WgtIfaceT *iface = wgtGetIface(wgtName);
if (!iface) { return wgtIfaceFindProp(iface, propName);
return NULL;
}
for (int32_t i = 0; i < iface->propCount; i++) {
if (strcasecmp(iface->props[i].name, propName) == 0) {
return &iface->props[i];
}
}
return NULL;
} }
// Walk tree items recursively to find the one matching a control name. // Walk tree items recursively to find the one matching a control name.
// Parse a tree label "name (Type)" or "name(idx) (Type)" into the bare name
// and, when present, the array index used to disambiguate control-array
// members (which all share one name but have distinct ->index).
static void parseTreeLabel(const char *label, char *outName, int32_t outNameSize, int32_t *outIndex) {
int32_t ni = 0;
while (label[ni] && label[ni] != ' ' && label[ni] != '(' && ni < outNameSize - 1) {
outName[ni] = label[ni];
ni++;
}
outName[ni] = '\0';
*outIndex = DSGN_NO_INDEX;
if (label[ni] == '(') {
*outIndex = (int32_t)atoi(&label[ni + 1]);
}
}
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index) { static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index) {
for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) { for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
const char *label = (const char *)item->userData; const char *label = (const char *)item->userData;
@ -427,14 +403,11 @@ static uint8_t getPropType(const char *propName, const char *typeName) {
const char *wgtName = wgtFindByBasName(typeName); const char *wgtName = wgtFindByBasName(typeName);
if (wgtName) { if (wgtName) {
const WgtIfaceT *iface = wgtGetIface(wgtName); const WgtIfaceT *iface = wgtGetIface(wgtName);
const WgtPropDescT *p = wgtIfaceFindProp(iface, propName);
if (iface) { if (p) {
for (int32_t i = 0; i < iface->propCount; i++) { return p->type;
if (strcasecmp(iface->props[i].name, propName) == 0) {
return iface->props[i].type;
}
}
} }
} }
} }
@ -872,7 +845,7 @@ static void onPropDblClick(WidgetT *w) {
} }
if (dsgnIsContainer(ctrl->typeName)) { if (dsgnIsContainer(ctrl->typeName)) {
cascadeToChildren(sDs, ctrl->name, val, ctrl->enabled); cascadeToChildren(sDs, ctrl->name, val, ctrl->enabled, 0);
} }
} else if (strcasecmp(propName, "Enabled") == 0 && !dsgnIfaceHasProp(ctrl->typeName, "Enabled")) { } else if (strcasecmp(propName, "Enabled") == 0 && !dsgnIfaceHasProp(ctrl->typeName, "Enabled")) {
bool val = frmParseBool(newValue); bool val = frmParseBool(newValue);
@ -883,7 +856,7 @@ static void onPropDblClick(WidgetT *w) {
} }
if (dsgnIsContainer(ctrl->typeName)) { if (dsgnIsContainer(ctrl->typeName)) {
cascadeToChildren(sDs, ctrl->name, ctrl->visible, val); cascadeToChildren(sDs, ctrl->name, ctrl->visible, val, 0);
} }
} else if (strcasecmp(propName, "HelpTopic") == 0) { } else if (strcasecmp(propName, "HelpTopic") == 0) {
snprintf(ctrl->helpTopic, DSGN_MAX_NAME, "%s", newValue); snprintf(ctrl->helpTopic, DSGN_MAX_NAME, "%s", newValue);
@ -898,13 +871,9 @@ static void onPropDblClick(WidgetT *w) {
const WgtIfaceT *iface = wgtGetIface(wgtName); const WgtIfaceT *iface = wgtGetIface(wgtName);
if (iface) { if (iface) {
for (int32_t i = 0; i < iface->propCount; i++) { const WgtPropDescT *p = wgtIfaceFindProp(iface, propName);
const WgtPropDescT *p = &iface->props[i];
if (strcasecmp(p->name, propName) != 0 || !p->setFn) {
continue;
}
if (p && p->setFn) {
if (p->type == WGT_IFACE_STRING) { if (p->type == WGT_IFACE_STRING) {
// Strings must outlive this function, so the // Strings must outlive this function, so the
// ctrl->props[] copy is what we pass to setFn // ctrl->props[] copy is what we pass to setFn
@ -945,7 +914,6 @@ static void onPropDblClick(WidgetT *w) {
} }
ifaceHandled = true; ifaceHandled = true;
break;
} }
} }
} }
@ -1173,6 +1141,81 @@ static void onTreeItemClick(WidgetT *w) {
} }
// Parse a tree label "name (Type)" or "name(idx) (Type)" into the bare name
// and, when present, the array index used to disambiguate control-array
// members (which all share one name but have distinct ->index).
static void parseTreeLabel(const char *label, char *outName, int32_t outNameSize, int32_t *outIndex) {
int32_t ni = 0;
while (label[ni] && label[ni] != ' ' && label[ni] != '(' && ni < outNameSize - 1) {
outName[ni] = label[ni];
ni++;
}
outName[ni] = '\0';
*outIndex = DSGN_NO_INDEX;
if (label[ni] == '(') {
*outIndex = (int32_t)atoi(&label[ni + 1]);
}
}
// propDropdownOrInput -- shared editor for the DataField / RecordSource
// properties. When count <= 0 it falls back to a free-text input box;
// otherwise it offers a "(none)"-prefixed dropdown of names, preselecting
// the entry matching curValue. Writes the chosen value (empty for "(none)")
// into newValue. Returns false if the user cancelled.
static bool propDropdownOrInput(AppContextT *ctx, const char *propName, const char *selectPrompt, char (*names)[DSGN_MAX_NAME], int32_t count, const char *curValue, char *newValue, int32_t newValueSize) {
if (count <= 0) {
// No names available -- fall back to text input
char prompt[PRP_PROMPT_BUF];
snprintf(prompt, sizeof(prompt), "%s:", propName);
snprintf(newValue, newValueSize, "%s", curValue);
return dvxInputBox(ctx, "Edit Property", prompt, curValue, newValue, newValueSize);
}
const char **ptrs = (const char **)calloc((size_t)count + 1, sizeof(const char *));
if (!ptrs) {
return false;
}
ptrs[0] = "(none)";
for (int32_t i = 0; i < count; i++) {
ptrs[i + 1] = names[i];
}
int32_t defIdx = 0;
for (int32_t i = 0; i < count; i++) {
if (strcasecmp(names[i], curValue) == 0) {
defIdx = i + 1;
break;
}
}
int32_t chosenIdx = 0;
bool ok = dvxChoiceDialog(ctx, propName, selectPrompt, ptrs, count + 1, defIdx, &chosenIdx);
free(ptrs);
if (!ok) {
return false;
}
if (chosenIdx == 0) {
newValue[0] = '\0';
} else {
snprintf(newValue, newValueSize, "%s", names[chosenIdx - 1]);
}
return true;
}
WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) { WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) {
sDs = ds; sDs = ds;
sPrpCtx = ctx; sPrpCtx = ctx;
@ -1459,61 +1502,6 @@ void prpRefresh(DsgnStateT *ds) {
} }
// propDropdownOrInput -- shared editor for the DataField / RecordSource
// properties. When count <= 0 it falls back to a free-text input box;
// otherwise it offers a "(none)"-prefixed dropdown of names, preselecting
// the entry matching curValue. Writes the chosen value (empty for "(none)")
// into newValue. Returns false if the user cancelled.
static bool propDropdownOrInput(AppContextT *ctx, const char *propName, const char *selectPrompt, char (*names)[DSGN_MAX_NAME], int32_t count, const char *curValue, char *newValue, int32_t newValueSize) {
if (count <= 0) {
// No names available -- fall back to text input
char prompt[PRP_PROMPT_BUF];
snprintf(prompt, sizeof(prompt), "%s:", propName);
snprintf(newValue, newValueSize, "%s", curValue);
return dvxInputBox(ctx, "Edit Property", prompt, curValue, newValue, newValueSize);
}
const char **ptrs = (const char **)calloc((size_t)count + 1, sizeof(const char *));
if (!ptrs) {
return false;
}
ptrs[0] = "(none)";
for (int32_t i = 0; i < count; i++) {
ptrs[i + 1] = names[i];
}
int32_t defIdx = 0;
for (int32_t i = 0; i < count; i++) {
if (strcasecmp(names[i], curValue) == 0) {
defIdx = i + 1;
break;
}
}
int32_t chosenIdx = 0;
bool ok = dvxChoiceDialog(ctx, propName, selectPrompt, ptrs, count + 1, defIdx, &chosenIdx);
free(ptrs);
if (!ok) {
return false;
}
if (chosenIdx == 0) {
newValue[0] = '\0';
} else {
snprintf(newValue, newValueSize, "%s", names[chosenIdx - 1]);
}
return true;
}
// resolveDbPath -- resolve a DatabaseName against the project directory // resolveDbPath -- resolve a DatabaseName against the project directory
static void resolveDbPath(const char *dbName, char *out, int32_t outSize) { static void resolveDbPath(const char *dbName, char *out, int32_t outSize) {
// If it's already an absolute path (starts with drive letter or /), use as-is // If it's already an absolute path (starts with drive letter or /), use as-is

View file

@ -114,10 +114,10 @@ static void bufWriteI32(BufT *b, int32_t v);
static void bufWriteStr(BufT *b, const char *s); static void bufWriteStr(BufT *b, const char *s);
static void bufWriteU16(BufT *b, uint16_t v); static void bufWriteU16(BufT *b, uint16_t v);
static void bufWriteU8(BufT *b, uint8_t v); static void bufWriteU8(BufT *b, uint8_t v);
static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry);
static double rF64(ReaderT *r); static double rF64(ReaderT *r);
static int32_t rI32(ReaderT *r); static int32_t rI32(ReaderT *r);
static bool rOk(ReaderT *r, int32_t need); static bool rOk(ReaderT *r, int32_t need);
static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry);
static char *rStr(ReaderT *r); static char *rStr(ReaderT *r);
static void rStrInto(ReaderT *r, char *dst, int32_t cap); static void rStrInto(ReaderT *r, char *dst, int32_t cap);
static uint16_t rU16(ReaderT *r); static uint16_t rU16(ReaderT *r);
@ -231,7 +231,9 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
// The constCount/dataCount/procCount counts live in the fixed header, // The constCount/dataCount/procCount counts live in the fixed header,
// ahead of the code blob, so their remaining-bytes upper bound cannot // ahead of the code blob, so their remaining-bytes upper bound cannot
// be checked until the code has been consumed. Reject negatives now. // be checked until the code has been consumed. Reject negatives now.
if (r.err || mod->codeLen < 0 || mod->constCount < 0 || mod->dataCount < 0 || mod->procCount < 0) { // globalCount has no payload here, but a negative value would defeat
// the VM's frame-0 local-index bounds check, so reject it too.
if (r.err || mod->globalCount < 0 || mod->codeLen < 0 || mod->constCount < 0 || mod->dataCount < 0 || mod->procCount < 0) {
basModuleFree(mod); basModuleFree(mod);
return NULL; return NULL;
} }
@ -659,6 +661,30 @@ static void bufWriteU8(BufT *b, uint8_t v) {
} }
// Read an int32 count and validate it is non-negative and that the
// declared minimum per-entry payload still fits in the remaining bytes.
// Uses division, not multiplication, to avoid overflow.
static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry) {
int32_t count = rI32(r);
if (r->err || count < 0) {
r->err = true;
return 0;
}
if (minBytesPerEntry > 0) {
int32_t remaining = r->len - r->pos;
if (count > remaining / minBytesPerEntry) {
r->err = true;
return 0;
}
}
return count;
}
static double rF64(ReaderT *r) { static double rF64(ReaderT *r) {
if (!rOk(r, sizeof(double))) { if (!rOk(r, sizeof(double))) {
return 0.0; return 0.0;
@ -696,30 +722,6 @@ static bool rOk(ReaderT *r, int32_t need) {
} }
// Read an int32 count and validate it is non-negative and that the
// declared minimum per-entry payload still fits in the remaining bytes.
// Uses division, not multiplication, to avoid overflow.
static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry) {
int32_t count = rI32(r);
if (r->err || count < 0) {
r->err = true;
return 0;
}
if (minBytesPerEntry > 0) {
int32_t remaining = r->len - r->pos;
if (count > remaining / minBytesPerEntry) {
r->err = true;
return 0;
}
}
return count;
}
// Read a length-prefixed string into a malloc'd buffer. // Read a length-prefixed string into a malloc'd buffer.
static char *rStr(ReaderT *r) { static char *rStr(ReaderT *r) {
uint16_t len = rU16(r); uint16_t len = rU16(r);

View file

@ -57,7 +57,6 @@ BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t
BasArrayT *basArrayRef(BasArrayT *arr); BasArrayT *basArrayRef(BasArrayT *arr);
void basArrayUnref(BasArrayT *arr); void basArrayUnref(BasArrayT *arr);
static int32_t basClampToRange(double n, int32_t lo, int32_t hi); static int32_t basClampToRange(double n, int32_t lo, int32_t hi);
static int32_t basValCompareImpl(BasValueT a, BasValueT b, bool caseInsensitive);
BasStringT *basStringAlloc(int32_t cap); BasStringT *basStringAlloc(int32_t cap);
int32_t basStringCompare(const BasStringT *a, const BasStringT *b); int32_t basStringCompare(const BasStringT *a, const BasStringT *b);
int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b); int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b);
@ -75,6 +74,7 @@ void basUdtUnref(BasUdtT *udt);
BasValueT basValBool(bool v); BasValueT basValBool(bool v);
int32_t basValCompare(BasValueT a, BasValueT b); int32_t basValCompare(BasValueT a, BasValueT b);
int32_t basValCompareCI(BasValueT a, BasValueT b); int32_t basValCompareCI(BasValueT a, BasValueT b);
static int32_t basValCompareImpl(BasValueT a, BasValueT b, bool caseInsensitive);
BasValueT basValCopy(BasValueT v); BasValueT basValCopy(BasValueT v);
BasValueT basValDouble(double v); BasValueT basValDouble(double v);
BasStringT *basValFormatString(BasValueT v); BasStringT *basValFormatString(BasValueT v);

View file

@ -90,7 +90,8 @@ static BasCallFrameT *currentFrame(BasVmT *vm);
static void defaultPrint(void *ctx, const char *text, bool newline); static void defaultPrint(void *ctx, const char *text, bool newline);
static void dirClose(BasVmT *vm); static void dirClose(BasVmT *vm);
static const char *dirNext(BasVmT *vm); static const char *dirNext(BasVmT *vm);
static void formatNumber(double n, const BasNumFormatT *f, char *out, size_t outSize); static int32_t forStackFloor(BasVmT *vm);
static void forStackTrim(BasVmT *vm, int32_t newDepth);
static BasVmResultE execArith(BasVmT *vm, uint8_t op); static BasVmResultE execArith(BasVmT *vm, uint8_t op);
static BasVmResultE execCompare(BasVmT *vm, uint8_t op); static BasVmResultE execCompare(BasVmT *vm, uint8_t op);
static BasVmResultE execFileOp(BasVmT *vm, uint8_t op); static BasVmResultE execFileOp(BasVmT *vm, uint8_t op);
@ -99,11 +100,16 @@ static BasVmResultE execLogical(BasVmT *vm, uint8_t op);
static BasVmResultE execMath(BasVmT *vm, uint8_t op); static BasVmResultE execMath(BasVmT *vm, uint8_t op);
static BasVmResultE execPrint(BasVmT *vm); static BasVmResultE execPrint(BasVmT *vm);
static BasVmResultE execStringOp(BasVmT *vm, uint8_t op); static BasVmResultE execStringOp(BasVmT *vm, uint8_t op);
static void formatNumber(double n, const BasNumFormatT *f, char *out, size_t outSize);
static int32_t nextStatementPc(BasVmT *vm);
static int32_t opcodeOperandSize(uint8_t op);
static bool operandFits(BasVmT *vm, int32_t size); static bool operandFits(BasVmT *vm, int32_t size);
static bool pop(BasVmT *vm, BasValueT *val); static bool pop(BasVmT *vm, BasValueT *val);
static void popCallFrame(BasVmT *vm); static void popCallFrame(BasVmT *vm);
static BasVmResultE popFileChannel(BasVmT *vm, int32_t *outChannel, bool requireOpen); static BasVmResultE popFileChannel(BasVmT *vm, int32_t *outChannel, bool requireOpen);
static void primeModuleFrame(BasVmT *vm);
static bool push(BasVmT *vm, BasValueT val); static bool push(BasVmT *vm, BasValueT val);
static void putBounded(char *out, int32_t *idx, int32_t limit, char c);
static int16_t readInt16(BasVmT *vm); static int16_t readInt16(BasVmT *vm);
static uint16_t readUint16(BasVmT *vm); static uint16_t readUint16(BasVmT *vm);
static uint8_t readUint8(BasVmT *vm); static uint8_t readUint8(BasVmT *vm);
@ -148,9 +154,12 @@ bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr, const BasValueT *args
bool savedRunning = vm->running; bool savedRunning = vm->running;
BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
frame->returnPc = savedPc; frame->returnPc = savedPc;
frame->localCount = BAS_VM_MAX_LOCALS; frame->localCount = BAS_VM_MAX_LOCALS;
frame->errorHandler = 0; frame->errorHandler = 0;
frame->savedForDepth = vm->forDepth;
frame->savedStmtPc = vm->stmtPc;
frame->savedStmtSp = vm->stmtSp;
memset(frame->locals, 0, sizeof(frame->locals)); memset(frame->locals, 0, sizeof(frame->locals));
for (int32_t i = 0; i < argCount && i < BAS_VM_MAX_LOCALS; i++) { for (int32_t i = 0; i < argCount && i < BAS_VM_MAX_LOCALS; i++) {
@ -260,20 +269,11 @@ void basVmLoadModule(BasVmT *vm, BasModuleT *module) {
} }
} }
// Prime the implicit main (module-level) frame. Module-level code // Prime the implicit main (module-level) frame. The IDE and test
// runs in callStack[0]; without callDepth >= 1 the error dispatcher's // harness used to set this by hand; doing it here makes every entry
// 'while (callDepth > 0)' unwind never inspects that frame, so a // path (including compiled apps via the stub) consistent.
// module-level ON ERROR GOTO never traps. The IDE and test harness primeModuleFrame(vm);
// set this by hand; doing it here makes every entry path (including vm->stmtPc = -1;
// compiled apps via the stub) consistent. localCount lets RET and
// frame-local cleanup see the right slot count.
vm->callDepth = 1;
vm->callStack[0].localCount = module->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : module->globalCount;
vm->callStack[0].errorHandler = 0;
// Module-level vars are globals, so frame-0 locals are unused, but
// clear them so a future re-load into a used VM can't release stale
// values when the error dispatcher or teardown walks localCount.
memset(vm->callStack[0].locals, 0, sizeof(vm->callStack[0].locals));
} }
@ -294,8 +294,26 @@ void basVmReset(BasVmT *vm) {
vm->sp = 0; vm->sp = 0;
vm->stmtSp = 0; vm->stmtSp = 0;
vm->stmtPc = -1;
vm->callDepth = 0; vm->callDepth = 0;
vm->forDepth = 0; vm->forDepth = 0;
// Re-establish the implicit module frame that basVmLoadModule primes.
// Leaving callDepth at 0 would silently kill module-level ON ERROR
// trapping after a reset: the error dispatcher only walks frames while
// callDepth > 0, so it would never inspect frame 0's handler.
if (vm->module) {
primeModuleFrame(vm);
} else {
// No module yet: hosts that re-prime callDepth = 1 by hand rely on
// frame 0's saved context being sane; clear it so a stale value
// from a previous run cannot leak into the FOR-stack floor or
// error-resume logic.
vm->callStack[0].savedForDepth = 0;
vm->callStack[0].savedStmtPc = -1;
vm->callStack[0].savedStmtSp = 0;
}
vm->outArgs = NULL; vm->outArgs = NULL;
vm->outArgCount = 0; vm->outArgCount = 0;
vm->outArgFrame = -1; vm->outArgFrame = -1;
@ -340,7 +358,8 @@ BasVmResultE basVmRun(BasVmT *vm) {
// frame and resume after the call site (effectively RESUME // frame and resume after the call site (effectively RESUME
// NEXT semantics), which is not what ON ERROR promises. // NEXT semantics), which is not what ON ERROR promises.
if (!vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) { if (!vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) {
int32_t target = 0; int32_t target = 0;
int32_t resumePc = -1;
while (vm->callDepth > 0) { while (vm->callDepth > 0) {
BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1]; BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1];
@ -356,6 +375,15 @@ BasVmResultE basVmRun(BasVmT *vm) {
basValRelease(&frame->locals[li]); basValRelease(&frame->locals[li]);
} }
// Restore the caller's context: drop the callee's FOR
// frames and bring back the statement boundary of the
// calling statement. RESUME must land in the handler
// frame's code, never in the discarded callee's.
forStackTrim(vm, frame->savedForDepth);
vm->stmtPc = frame->savedStmtPc;
vm->stmtSp = frame->savedStmtSp;
resumePc = frame->returnPc;
vm->callDepth--; vm->callDepth--;
} }
@ -372,8 +400,18 @@ BasVmResultE basVmRun(BasVmT *vm) {
vm->sp = vm->stmtSp; vm->sp = vm->stmtSp;
} }
vm->errorPc = savedPc; // The eval stack is now at the failing statement's
vm->errorNextPc = vm->pc; // boundary, so RESUME must re-run that statement from
// its start and RESUME NEXT must continue at the next
// statement -- resuming mid-statement would pop values
// from beneath the truncated boundary. Without
// statement info (OP_LINE stripped by release
// compaction) fall back to the call site after an
// unwind, or to the failing instruction.
int32_t nextPc = nextStatementPc(vm);
vm->errorPc = (vm->stmtPc >= 0) ? vm->stmtPc : ((resumePc >= 0) ? resumePc : savedPc);
vm->errorNextPc = (nextPc >= 0) ? nextPc : ((resumePc >= 0) ? resumePc : vm->pc);
vm->inErrorHandler = true; vm->inErrorHandler = true;
vm->errorHandler = target; vm->errorHandler = target;
vm->pc = target; vm->pc = target;
@ -507,7 +545,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
case OP_PUSH_INT32: { case OP_PUSH_INT32: {
int16_t lo = readInt16(vm); int16_t lo = readInt16(vm);
int16_t hi = readInt16(vm); int16_t hi = readInt16(vm);
int32_t val = ((int32_t)hi << 16) | (uint16_t)lo; // Reconstruct through unsigned math: left-shifting a negative
// sign-extended high half would be signed-overflow UB.
int32_t val = (int32_t)(((uint32_t)(uint16_t)hi << 16) | (uint16_t)lo);
if (!push(vm, basValLong(val))) { if (!push(vm, basValLong(val))) {
return BAS_VM_STACK_OVERFLOW; return BAS_VM_STACK_OVERFLOW;
@ -922,14 +962,34 @@ BasVmResultE basVmStep(BasVmT *vm) {
uint8_t argc = readUint8(vm); uint8_t argc = readUint8(vm);
uint8_t baseSlot = readUint8(vm); uint8_t baseSlot = readUint8(vm);
// Guard the locals window: baseSlot and argc come straight from
// bytecode, and a corrupt module could otherwise make the arg
// copy below write past locals[]. Drain the pushed args first
// so the eval stack stays balanced.
if ((int32_t)baseSlot + (int32_t)argc > BAS_VM_MAX_LOCALS) {
for (int32_t i = 0; i < (int32_t)argc; i++) {
BasValueT tmp;
if (pop(vm, &tmp)) {
basValRelease(&tmp);
}
}
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "CALL argument slots out of range");
return BAS_VM_ERROR;
}
if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE) { if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE) {
return BAS_VM_CALL_OVERFLOW; return BAS_VM_CALL_OVERFLOW;
} }
BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
frame->returnPc = vm->pc; frame->returnPc = vm->pc;
frame->localCount = BAS_VM_MAX_LOCALS; frame->localCount = BAS_VM_MAX_LOCALS;
frame->errorHandler = 0; frame->errorHandler = 0;
frame->savedForDepth = vm->forDepth;
frame->savedStmtPc = vm->stmtPc;
frame->savedStmtSp = vm->stmtSp;
// Zero all local slots // Zero all local slots
memset(frame->locals, 0, sizeof(frame->locals)); memset(frame->locals, 0, sizeof(frame->locals));
@ -1006,6 +1066,20 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW; return BAS_VM_STACK_UNDERFLOW;
} }
// Re-entering a FOR whose frame is still live (a GOTO back to
// the FOR statement, or a jump that skipped its NEXT) must not
// stack a second frame: discard the old context and everything
// nested inside it, as classic BASIC does. Only frames owned
// by the current call frame are considered.
int32_t forFloor = forStackFloor(vm);
for (int32_t k = vm->forDepth - 1; k >= forFloor; k--) {
if (vm->forStack[k].varIdx == (int32_t)varIdx && vm->forStack[k].scopeTag == scopeTag) {
forStackTrim(vm, k);
break;
}
}
if (vm->forDepth >= BAS_VM_MAX_FOR_DEPTH) { if (vm->forDepth >= BAS_VM_MAX_FOR_DEPTH) {
basValRelease(&stepVal); basValRelease(&stepVal);
basValRelease(&limitVal); basValRelease(&limitVal);
@ -1078,9 +1152,28 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasForStateT *fs = &vm->forStack[vm->forDepth - 1]; BasForStateT *fs = &vm->forStack[vm->forDepth - 1];
if (fs->varIdx != (int32_t)varIdx) { if (fs->varIdx != (int32_t)varIdx || fs->scopeTag != scopeTag) {
runtimeError(vm, BAS_ERR_NEXT_WITHOUT_FOR, "NEXT variable mismatch"); // A jump out of an inner FOR (EXIT DO, GOTO) leaves its
return BAS_VM_ERROR; // frame on the FOR stack. Search down to this call frame's
// floor for the loop this NEXT belongs to and discard the
// stale frames above it, matching classic BASIC semantics.
int32_t forFloor = forStackFloor(vm);
int32_t matchIdx = -1;
for (int32_t k = vm->forDepth - 2; k >= forFloor; k--) {
if (vm->forStack[k].varIdx == (int32_t)varIdx && vm->forStack[k].scopeTag == scopeTag) {
matchIdx = k;
break;
}
}
if (matchIdx < 0) {
runtimeError(vm, BAS_ERR_NEXT_WITHOUT_FOR, "NEXT variable mismatch");
return BAS_VM_ERROR;
}
forStackTrim(vm, matchIdx + 1);
fs = &vm->forStack[matchIdx];
} }
// Get pointer to the loop variable // Get pointer to the loop variable
@ -2823,7 +2916,19 @@ BasVmResultE basVmStep(BasVmT *vm) {
} }
} }
if (!pop(vm, &methodNameVal) || !pop(vm, &ctrlRefVal)) { if (!pop(vm, &methodNameVal)) {
for (int32_t i = 0; i < argCount; i++) {
basValRelease(&args[i]);
}
return BAS_VM_STACK_UNDERFLOW;
}
if (!pop(vm, &ctrlRefVal)) {
// methodNameVal was already popped -- release it too or
// its reference leaks on this underflow path.
basValRelease(&methodNameVal);
for (int32_t i = 0; i < argCount; i++) { for (int32_t i = 0; i < argCount; i++) {
basValRelease(&args[i]); basValRelease(&args[i]);
} }
@ -3368,8 +3473,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
// Snapshot the eval-stack depth at this statement boundary so an // Snapshot the eval-stack depth at this statement boundary so an
// ON ERROR GOTO dispatch can release any operands left half-pushed // ON ERROR GOTO dispatch can release any operands left half-pushed
// by an error mid-expression and reset sp to here. // by an error mid-expression and reset sp to here. Also remember
// where the statement starts: RESUME re-executes it from here and
// RESUME NEXT scans from here for the following statement.
vm->stmtSp = vm->sp; vm->stmtSp = vm->sp;
vm->stmtPc = vm->currentOpPc;
// Step into: break at any OP_LINE // Step into: break at any OP_LINE
if (vm->debugBreak) { if (vm->debugBreak) {
@ -3688,189 +3796,27 @@ static const char *dirNext(BasVmT *vm) {
} }
// Shared numeric formatter for OP_PRINT_USING (numeric branch) and OP_FORMAT // Lowest forStack index the currently-executing call frame may unwind
// (non-PERCENT branch). Renders n into out per the flags in f. Every cursor // to. FOR frames below this belong to callers and must never be
// write is bounded against outSize, and every intermediate buffer is sized to // touched by NEXT mismatch recovery or FOR re-entry cleanup.
// BAS_FORMAT_BUF_SIZE and clamped, so a pathological format string or numeric static int32_t forStackFloor(BasVmT *vm) {
// magnitude cannot overflow the caller's stack-resident buffer. if (vm->callDepth > 0) {
static void formatNumber(double n, const BasNumFormatT *f, char *out, size_t outSize) { return vm->callStack[vm->callDepth - 1].savedForDepth;
bool isNeg = (n < 0);
double absN = isNeg ? -n : n;
int32_t decimals = f->hasDecimal ? f->digitsAfter : 0;
int32_t limit = (int32_t)outSize - 1;
int32_t idx = 0;
char numBuf[BAS_FORMAT_BUF_SIZE];
snprintf(numBuf, sizeof(numBuf), "%.*f", (int)decimals, absN);
// Split into integer and decimal parts.
char intPart[BAS_FORMAT_BUF_SIZE];
char decPart[BAS_FORMAT_BUF_SIZE];
char *dot = strchr(numBuf, '.');
intPart[0] = '\0';
decPart[0] = '\0';
if (dot) {
int32_t intLen = (int32_t)(dot - numBuf);
if (intLen > (int32_t)sizeof(intPart) - 1) {
intLen = (int32_t)sizeof(intPart) - 1;
}
memcpy(intPart, numBuf, intLen);
intPart[intLen] = '\0';
strncpy(decPart, dot + 1, sizeof(decPart) - 1);
decPart[sizeof(decPart) - 1] = '\0';
} else {
strncpy(intPart, numBuf, sizeof(intPart) - 1);
intPart[sizeof(intPart) - 1] = '\0';
} }
// Apply thousands separator. return 0;
char fmtIntPart[BAS_FORMAT_BUF_SIZE]; }
if (f->hasComma) {
int32_t srcLen = (int32_t)strlen(intPart);
int32_t dstIdx = 0;
for (int32_t i = 0; i < srcLen; i++) { // Release FOR-stack entries above newDepth and truncate the stack to it.
if (i > 0 && (srcLen - i) % 3 == 0) { // Used when returning from a call frame, unwinding to an error handler,
if (dstIdx < (int32_t)sizeof(fmtIntPart) - 1) { // and discarding stale frames left by EXIT DO / GOTO jumps out of FORs.
fmtIntPart[dstIdx++] = ','; static void forStackTrim(BasVmT *vm, int32_t newDepth) {
} while (vm->forDepth > newDepth) {
} BasForStateT *fs = &vm->forStack[--vm->forDepth];
basValRelease(&fs->limit);
if (dstIdx < (int32_t)sizeof(fmtIntPart) - 1) { basValRelease(&fs->step);
fmtIntPart[dstIdx++] = intPart[i];
}
}
fmtIntPart[dstIdx] = '\0';
} else {
strncpy(fmtIntPart, intPart, sizeof(fmtIntPart) - 1);
fmtIntPart[sizeof(fmtIntPart) - 1] = '\0';
} }
// Sign prefix. FORMAT$ (formatPad) and PRINT USING differ here, so each
// call site's original rule is reproduced exactly.
if (f->formatPad) {
// FORMAT$: leading '-' for negatives whenever a sign position exists,
// leading '+' only for plusAtStart positives.
if (f->plusAtStart || f->plusAtEnd) {
if (isNeg) {
if (idx < limit) {
out[idx++] = '-';
}
} else if (f->plusAtStart) {
if (idx < limit) {
out[idx++] = '+';
}
}
} else if (isNeg) {
if (idx < limit) {
out[idx++] = '-';
}
}
} else {
// PRINT USING: plusAtStart always shows a sign; otherwise a leading
// '-' for negatives unless the minus is deferred to the end.
if (f->plusAtStart) {
if (idx < limit) {
out[idx++] = isNeg ? '-' : '+';
}
} else if (isNeg && !f->minusAtEnd) {
if (idx < limit) {
out[idx++] = '-';
}
}
}
// Dollar sign (PRINT USING $$ only).
if (f->dollarFloat) {
if (idx < limit) {
out[idx++] = '$';
}
}
// Pad leading. PRINT USING fills the whole pad region with fillChar
// (space or '*'). FORMAT$ (formatPad) fills the positions before the
// number with '0' when a zero digit precedes (zeroPad) else ' ', and the
// remaining positions with '0'.
int32_t fmtIntLen = (int32_t)strlen(fmtIntPart);
int32_t intRawLen = (int32_t)strlen(intPart);
int32_t padNeeded = f->digitsBefore - fmtIntLen;
char fillChar = f->asteriskFill ? '*' : ' ';
for (int32_t i = 0; i < padNeeded; i++) {
char padChar;
if (f->formatPad) {
if (i < padNeeded - intRawLen) {
padChar = f->zeroPad ? '0' : ' ';
} else {
padChar = '0';
}
} else {
padChar = fillChar;
}
if (idx < limit) {
out[idx++] = padChar;
}
}
// Integer part.
for (int32_t i = 0; fmtIntPart[i]; i++) {
if (idx < limit) {
out[idx++] = fmtIntPart[i];
}
}
// Decimal part. Missing digits fill with '0' (PRINT USING behavior);
// FORMAT$ always has exactly 'decimals' digits so this never triggers.
if (f->hasDecimal) {
if (idx < limit) {
out[idx++] = '.';
}
for (int32_t i = 0; i < decimals; i++) {
if (idx < limit) {
out[idx++] = decPart[i] ? decPart[i] : '0';
}
}
}
// Trailing sign. FORMAT$ already emitted the leading '-' for negatives,
// so its trailing plusEnd path only shows '+' for positives. PRINT USING
// defers the sign to the end, so its plusEnd path shows '-' for negatives.
if (f->formatPad) {
if (f->plusAtEnd && !isNeg) {
if (idx < limit) {
out[idx++] = '+';
}
} else if (f->minusAtEnd && isNeg) {
if (idx < limit) {
out[idx++] = '-';
}
} else if (f->minusAtEnd && !isNeg) {
if (idx < limit) {
out[idx++] = ' ';
}
}
} else {
if (f->plusAtEnd) {
if (idx < limit) {
out[idx++] = isNeg ? '-' : '+';
}
} else if (f->minusAtEnd) {
if (idx < limit) {
out[idx++] = isNeg ? '-' : ' ';
}
}
}
out[idx] = '\0';
} }
@ -4114,6 +4060,16 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
BasVmResultE chResult = popFileChannel(vm, &channel, false); BasVmResultE chResult = popFileChannel(vm, &channel, false);
if (chResult != BAS_VM_OK) { if (chResult != BAS_VM_OK) {
// popFileChannel consumed only the channel; the filename
// operand below it must come off too or the untrapped error
// path strands it on the eval stack. A channel underflow
// means the stack was empty, so this pop is a no-op there.
BasValueT discardVal;
if (pop(vm, &discardVal)) {
basValRelease(&discardVal);
}
return chResult; return chResult;
} }
@ -4632,6 +4588,14 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
BasVmResultE chResult = popFileChannel(vm, &channel, true); BasVmResultE chResult = popFileChannel(vm, &channel, true);
if (chResult != BAS_VM_OK) { if (chResult != BAS_VM_OK) {
// Same shape as OP_FILE_OPEN: the count operand below the
// channel must be drained on the error path.
BasValueT discardVal;
if (pop(vm, &discardVal)) {
basValRelease(&discardVal);
}
return chResult; return chResult;
} }
@ -5646,6 +5610,335 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
} }
// Shared numeric formatter for OP_PRINT_USING (numeric branch) and OP_FORMAT
// (non-PERCENT branch). Renders n into out per the flags in f. Every cursor
// write is bounded against outSize, and every intermediate buffer is sized to
// BAS_FORMAT_BUF_SIZE and clamped, so a pathological format string or numeric
// magnitude cannot overflow the caller's stack-resident buffer.
static void formatNumber(double n, const BasNumFormatT *f, char *out, size_t outSize) {
bool isNeg = (n < 0);
double absN = isNeg ? -n : n;
int32_t decimals = f->hasDecimal ? f->digitsAfter : 0;
int32_t limit = (int32_t)outSize - 1;
int32_t idx = 0;
char numBuf[BAS_FORMAT_BUF_SIZE];
snprintf(numBuf, sizeof(numBuf), "%.*f", (int)decimals, absN);
// Split into integer and decimal parts.
char intPart[BAS_FORMAT_BUF_SIZE];
char decPart[BAS_FORMAT_BUF_SIZE];
char *dot = strchr(numBuf, '.');
intPart[0] = '\0';
decPart[0] = '\0';
if (dot) {
int32_t intLen = (int32_t)(dot - numBuf);
if (intLen > (int32_t)sizeof(intPart) - 1) {
intLen = (int32_t)sizeof(intPart) - 1;
}
memcpy(intPart, numBuf, intLen);
intPart[intLen] = '\0';
strncpy(decPart, dot + 1, sizeof(decPart) - 1);
decPart[sizeof(decPart) - 1] = '\0';
} else {
strncpy(intPart, numBuf, sizeof(intPart) - 1);
intPart[sizeof(intPart) - 1] = '\0';
}
// Apply thousands separator.
char fmtIntPart[BAS_FORMAT_BUF_SIZE];
if (f->hasComma) {
int32_t srcLen = (int32_t)strlen(intPart);
int32_t dstIdx = 0;
for (int32_t i = 0; i < srcLen; i++) {
if (i > 0 && (srcLen - i) % 3 == 0) {
putBounded(fmtIntPart, &dstIdx, (int32_t)sizeof(fmtIntPart) - 1, ',');
}
putBounded(fmtIntPart, &dstIdx, (int32_t)sizeof(fmtIntPart) - 1, intPart[i]);
}
fmtIntPart[dstIdx] = '\0';
} else {
strncpy(fmtIntPart, intPart, sizeof(fmtIntPart) - 1);
fmtIntPart[sizeof(fmtIntPart) - 1] = '\0';
}
// Sign prefix. FORMAT$ (formatPad) and PRINT USING differ here, so each
// call site's original rule is reproduced exactly.
if (f->formatPad) {
// FORMAT$: leading '-' for negatives whenever a sign position exists,
// leading '+' only for plusAtStart positives.
if (f->plusAtStart || f->plusAtEnd) {
if (isNeg) {
putBounded(out, &idx, limit, '-');
} else if (f->plusAtStart) {
putBounded(out, &idx, limit, '+');
}
} else if (isNeg) {
putBounded(out, &idx, limit, '-');
}
} else {
// PRINT USING: plusAtStart always shows a sign; otherwise a leading
// '-' for negatives unless the minus is deferred to the end.
if (f->plusAtStart) {
putBounded(out, &idx, limit, isNeg ? '-' : '+');
} else if (isNeg && !f->minusAtEnd) {
putBounded(out, &idx, limit, '-');
}
}
// Dollar sign (PRINT USING $$ only).
if (f->dollarFloat) {
putBounded(out, &idx, limit, '$');
}
// Pad leading. PRINT USING fills the whole pad region with fillChar
// (space or '*'). FORMAT$ (formatPad) fills the positions before the
// number with '0' when a zero digit precedes (zeroPad) else ' ', and the
// remaining positions with '0'.
int32_t fmtIntLen = (int32_t)strlen(fmtIntPart);
int32_t intRawLen = (int32_t)strlen(intPart);
int32_t padNeeded = f->digitsBefore - fmtIntLen;
char fillChar = f->asteriskFill ? '*' : ' ';
for (int32_t i = 0; i < padNeeded; i++) {
char padChar;
if (f->formatPad) {
if (i < padNeeded - intRawLen) {
padChar = f->zeroPad ? '0' : ' ';
} else {
padChar = '0';
}
} else {
padChar = fillChar;
}
putBounded(out, &idx, limit, padChar);
}
// Integer part.
for (int32_t i = 0; fmtIntPart[i]; i++) {
putBounded(out, &idx, limit, fmtIntPart[i]);
}
// Decimal part. Missing digits fill with '0' (PRINT USING behavior);
// FORMAT$ always has exactly 'decimals' digits so this never triggers.
if (f->hasDecimal) {
putBounded(out, &idx, limit, '.');
for (int32_t i = 0; i < decimals; i++) {
putBounded(out, &idx, limit, decPart[i] ? decPart[i] : '0');
}
}
// Trailing sign. FORMAT$ already emitted the leading '-' for negatives,
// so its trailing plusEnd path only shows '+' for positives. PRINT USING
// defers the sign to the end, so its plusEnd path shows '-' for negatives.
if (f->formatPad) {
if (f->plusAtEnd && !isNeg) {
putBounded(out, &idx, limit, '+');
} else if (f->minusAtEnd && isNeg) {
putBounded(out, &idx, limit, '-');
} else if (f->minusAtEnd && !isNeg) {
putBounded(out, &idx, limit, ' ');
}
} else {
if (f->plusAtEnd) {
putBounded(out, &idx, limit, isNeg ? '-' : '+');
} else if (f->minusAtEnd) {
putBounded(out, &idx, limit, isNeg ? '-' : ' ');
}
}
out[idx] = '\0';
}
// Returns the bytecode address the error dispatcher should record for
// RESUME NEXT: the statement that follows the failing one. Walks the
// bytecode instruction by instruction from the failing statement's
// OP_LINE (vm->stmtPc). The walk lands on the next OP_LINE or on a
// structural opcode that IS what runs after the statement (a loop-closing
// OP_JMP/OP_FOR_NEXT, a SUB's OP_RET epilogue, OP_END/OP_HALT); a GOSUB's
// PUSH_INT32+JMP pair is stepped over as a unit so the walk continues at
// its return address. Returns -1 when no statement boundary is known
// (OP_LINE stripped by release compaction) or when the walk reaches an
// opcode that pops values the trap already released (conditional jumps,
// OP_RET_VAL); the caller then falls back to a coarser resume target.
static int32_t nextStatementPc(BasVmT *vm) {
if (vm->stmtPc < 0 || !vm->module) {
return -1;
}
const uint8_t *code = vm->module->code;
int32_t codeLen = vm->module->codeLen;
int32_t pc = vm->stmtPc;
while (pc < codeLen) {
uint8_t op = code[pc];
if (pc > vm->stmtPc) {
// GOSUB pattern: OP_PUSH_INT32 <pc+8> OP_JMP <offset>. Step
// over both so a failed GOSUB statement resumes after the call.
if (op == OP_PUSH_INT32 && pc + 8 <= codeLen && code[pc + 5] == OP_JMP) {
int32_t retAddr = (int32_t)((uint32_t)code[pc + 1] | ((uint32_t)code[pc + 2] << 8) | ((uint32_t)code[pc + 3] << 16) | ((uint32_t)code[pc + 4] << 24));
if (retAddr == pc + 8) {
pc += 8;
continue;
}
}
if (op == OP_LINE || op == OP_JMP || op == OP_FOR_NEXT || op == OP_RET || op == OP_END || op == OP_HALT) {
return pc;
}
if (op == OP_JMP_TRUE || op == OP_JMP_FALSE || op == OP_RET_VAL || op == OP_GOSUB_RET) {
return -1;
}
}
int32_t operand = opcodeOperandSize(op);
if (operand < 0) {
return -1;
}
pc += 1 + operand;
}
return -1;
}
// Operand byte count for each opcode (excluding the 1-byte opcode), or -1
// if unknown. Used only to walk instruction boundaries for RESUME NEXT.
// Keep in sync with opOperandSize in compiler/compact.c (the compiler-side
// copy) and the operand comments in compiler/opcodes.h.
static int32_t opcodeOperandSize(uint8_t op) {
switch (op) {
// No operand bytes
case OP_NOP:
case OP_PUSH_TRUE: case OP_PUSH_FALSE:
case OP_POP: case OP_DUP:
case OP_LOAD_REF: case OP_STORE_REF:
case OP_ADD_INT: case OP_SUB_INT: case OP_MUL_INT:
case OP_IDIV_INT: case OP_MOD_INT: case OP_NEG_INT:
case OP_ADD_FLT: case OP_SUB_FLT: case OP_MUL_FLT:
case OP_DIV_FLT: case OP_NEG_FLT: case OP_POW:
case OP_STR_CONCAT: case OP_STR_LEFT: case OP_STR_RIGHT:
case OP_STR_MID: case OP_STR_MID2: case OP_STR_LEN:
case OP_STR_INSTR: case OP_STR_INSTR3:
case OP_STR_UCASE: case OP_STR_LCASE:
case OP_STR_TRIM: case OP_STR_LTRIM: case OP_STR_RTRIM:
case OP_STR_CHR: case OP_STR_ASC: case OP_STR_SPACE:
case OP_CMP_EQ: case OP_CMP_NE: case OP_CMP_LT:
case OP_CMP_GT: case OP_CMP_LE: case OP_CMP_GE:
case OP_AND: case OP_OR: case OP_NOT:
case OP_XOR: case OP_EQV: case OP_IMP:
case OP_GOSUB_RET: case OP_RET: case OP_RET_VAL:
case OP_FOR_POP:
case OP_CONV_INT_FLT: case OP_CONV_FLT_INT:
case OP_CONV_INT_STR: case OP_CONV_STR_INT:
case OP_CONV_FLT_STR: case OP_CONV_STR_FLT:
case OP_CONV_INT_LONG: case OP_CONV_LONG_INT:
case OP_PRINT: case OP_PRINT_NL: case OP_PRINT_TAB:
case OP_INPUT:
case OP_FILE_CLOSE: case OP_FILE_PRINT: case OP_FILE_INPUT:
case OP_FILE_EOF: case OP_FILE_LINE_INPUT:
case OP_LOAD_PROP: case OP_STORE_PROP:
case OP_LOAD_FORM: case OP_UNLOAD_FORM:
case OP_HIDE_FORM: case OP_DO_EVENTS:
case OP_MSGBOX: case OP_INPUTBOX: case OP_ME_REF:
case OP_CREATE_CTRL: case OP_FIND_CTRL: case OP_FIND_CTRL_IDX:
case OP_CREATE_CTRL_EX:
case OP_ERASE:
case OP_RESUME: case OP_RESUME_NEXT:
case OP_RAISE_ERR: case OP_ERR_NUM: case OP_ERR_CLEAR:
case OP_MATH_ABS: case OP_MATH_INT: case OP_MATH_FIX:
case OP_MATH_SGN: case OP_MATH_SQR: case OP_MATH_SIN:
case OP_MATH_COS: case OP_MATH_TAN: case OP_MATH_ATN:
case OP_MATH_LOG: case OP_MATH_EXP: case OP_MATH_RND:
case OP_MATH_RANDOMIZE:
case OP_RGB:
case OP_GET_RED: case OP_GET_GREEN: case OP_GET_BLUE:
case OP_STR_VAL: case OP_STR_STRF: case OP_STR_HEX:
case OP_STR_STRING: case OP_STR_OCT: case OP_CONV_BOOL:
case OP_MATH_TIMER: case OP_DATE_STR: case OP_TIME_STR:
case OP_SLEEP: case OP_ENVIRON:
case OP_READ_DATA: case OP_RESTORE:
case OP_FILE_WRITE: case OP_FILE_WRITE_SEP: case OP_FILE_WRITE_NL:
case OP_FILE_GET: case OP_FILE_PUT: case OP_FILE_SEEK:
case OP_FILE_LOF: case OP_FILE_LOC: case OP_FILE_FREEFILE:
case OP_FILE_INPUT_N:
case OP_STR_MID_ASGN: case OP_PRINT_USING:
case OP_PRINT_TAB_N: case OP_PRINT_SPC_N:
case OP_FORMAT: case OP_SHELL:
case OP_APP_PATH: case OP_APP_CONFIG: case OP_APP_DATA:
case OP_INI_READ: case OP_INI_WRITE:
case OP_FS_KILL: case OP_FS_NAME: case OP_FS_FILECOPY:
case OP_FS_MKDIR: case OP_FS_RMDIR: case OP_FS_CHDIR:
case OP_FS_CHDRIVE: case OP_FS_CURDIR: case OP_FS_DIR:
case OP_FS_DIR_NEXT: case OP_FS_FILELEN:
case OP_FS_GETATTR: case OP_FS_SETATTR:
case OP_CREATE_FORM: case OP_SET_EVENT: case OP_REMOVE_CTRL:
case OP_END: case OP_HALT:
return 0;
case OP_LOAD_ARRAY: case OP_STORE_ARRAY:
case OP_PUSH_ARR_ADDR:
case OP_PRINT_SPC: case OP_FILE_OPEN:
case OP_CALL_METHOD: case OP_SHOW_FORM:
case OP_LBOUND: case OP_UBOUND:
case OP_COMPARE_MODE:
return 1;
case OP_PUSH_INT16: case OP_PUSH_STR:
case OP_LOAD_LOCAL: case OP_STORE_LOCAL:
case OP_LOAD_GLOBAL: case OP_STORE_GLOBAL:
case OP_LOAD_FIELD: case OP_STORE_FIELD:
case OP_PUSH_LOCAL_ADDR: case OP_PUSH_GLOBAL_ADDR:
case OP_JMP: case OP_JMP_TRUE: case OP_JMP_FALSE:
case OP_CTRL_REF:
case OP_LOAD_FORM_VAR: case OP_STORE_FORM_VAR:
case OP_PUSH_FORM_ADDR:
case OP_DIM_ARRAY: case OP_REDIM:
case OP_ON_ERROR:
case OP_STR_FIXLEN:
case OP_LINE:
return 2;
case OP_STORE_ARRAY_FIELD:
return 3;
case OP_PUSH_INT32: case OP_PUSH_FLT32:
case OP_CALL:
return 4;
case OP_FOR_INIT:
case OP_FOR_NEXT:
return 5;
case OP_CALL_EXTERN:
return 6;
case OP_PUSH_FLT64:
return 8;
default:
return -1;
}
}
// Returns whether `size` bytes can be read at vm->pc without running past // Returns whether `size` bytes can be read at vm->pc without running past
// the end of code[]. On overflow it flags the VM and clamps pc to codeLen so // the end of code[]. On overflow it flags the VM and clamps pc to codeLen so
// the next basVmStep entry raises BAS_VM_BAD_OPCODE. Compared as // the next basVmStep entry raises BAS_VM_BAD_OPCODE. Compared as
@ -5693,6 +5986,13 @@ static void popCallFrame(BasVmT *vm) {
basValRelease(&frame->locals[i]); basValRelease(&frame->locals[i]);
} }
// Discard any FOR frames the callee left open (EXIT SUB/FUNCTION
// from inside a FOR body jumps straight to this epilogue) and
// restore the caller's statement boundary for error dispatch.
forStackTrim(vm, frame->savedForDepth);
vm->stmtPc = frame->savedStmtPc;
vm->stmtSp = frame->savedStmtSp;
bool hadHandler = (frame->errorHandler != 0); bool hadHandler = (frame->errorHandler != 0);
frame->errorHandler = 0; frame->errorHandler = 0;
vm->pc = frame->returnPc; vm->pc = frame->returnPc;
@ -5733,6 +6033,26 @@ static BasVmResultE popFileChannel(BasVmT *vm, int32_t *outChannel, bool require
} }
// Primes callStack[0] as the implicit main (module-level) frame. Module
// code runs in frame 0; without callDepth >= 1 the error dispatcher's
// 'while (callDepth > 0)' unwind never inspects that frame, so a
// module-level ON ERROR GOTO never traps. Shared by basVmLoadModule and
// basVmReset so both entry paths keep the same invariant. localCount
// lets RET and frame-local cleanup see the right slot count.
static void primeModuleFrame(BasVmT *vm) {
vm->callDepth = 1;
vm->callStack[0].localCount = vm->module->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : vm->module->globalCount;
vm->callStack[0].errorHandler = 0;
vm->callStack[0].savedForDepth = 0;
vm->callStack[0].savedStmtPc = -1;
vm->callStack[0].savedStmtSp = 0;
// Module-level vars are globals, so frame-0 locals are unused, but
// clear them so re-priming a used VM can't release stale values when
// the error dispatcher or teardown walks localCount.
memset(vm->callStack[0].locals, 0, sizeof(vm->callStack[0].locals));
}
static bool push(BasVmT *vm, BasValueT val) { static bool push(BasVmT *vm, BasValueT val) {
if (vm->sp >= BAS_VM_STACK_SIZE) { if (vm->sp >= BAS_VM_STACK_SIZE) {
return false; return false;
@ -5743,6 +6063,16 @@ static bool push(BasVmT *vm, BasValueT val) {
} }
// Bounded single-character append used throughout formatNumber: writes c at
// out[*idx] and advances *idx only while it stays under limit, so no formatter
// cursor write can overflow the caller's buffer.
static void putBounded(char *out, int32_t *idx, int32_t limit, char c) {
if (*idx < limit) {
out[(*idx)++] = c;
}
}
// memcpy with constant size is folded to a single load by the compiler and // memcpy with constant size is folded to a single load by the compiler and
// is alignment-safe (bytecode operands aren't guaranteed 2-byte aligned). // is alignment-safe (bytecode operands aren't guaranteed 2-byte aligned).
static inline int16_t readInt16(BasVmT *vm) { static inline int16_t readInt16(BasVmT *vm) {
@ -5860,7 +6190,8 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
// so event handlers (fired via basVmCallSub) behave like // so event handlers (fired via basVmCallSub) behave like
// module-level code when it comes to error trapping. // module-level code when it comes to error trapping.
if (!vm->inErrorHandler && result != BAS_VM_BAD_OPCODE) { if (!vm->inErrorHandler && result != BAS_VM_BAD_OPCODE) {
int32_t target = 0; int32_t target = 0;
int32_t resumePc = -1;
while (vm->callDepth > savedCallDepth) { while (vm->callDepth > savedCallDepth) {
BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1]; BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1];
@ -5874,6 +6205,13 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
basValRelease(&frame->locals[li]); basValRelease(&frame->locals[li]);
} }
// Restore the caller's context -- see the basVmRun
// dispatcher for the rationale.
forStackTrim(vm, frame->savedForDepth);
vm->stmtPc = frame->savedStmtPc;
vm->stmtSp = frame->savedStmtSp;
resumePc = frame->returnPc;
vm->callDepth--; vm->callDepth--;
} }
@ -5890,8 +6228,12 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
vm->sp = vm->stmtSp; vm->sp = vm->stmtSp;
} }
vm->errorPc = stepPc; // Statement-granular resume targets -- see the basVmRun
vm->errorNextPc = vm->pc; // dispatcher for the rationale.
int32_t nextPc = nextStatementPc(vm);
vm->errorPc = (vm->stmtPc >= 0) ? vm->stmtPc : ((resumePc >= 0) ? resumePc : stepPc);
vm->errorNextPc = (nextPc >= 0) ? nextPc : ((resumePc >= 0) ? resumePc : vm->pc);
vm->inErrorHandler = true; vm->inErrorHandler = true;
vm->errorHandler = target; vm->errorHandler = target;
vm->pc = target; vm->pc = target;
@ -5900,9 +6242,28 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
} }
} }
vm->pc = savedPc; // Frames the dispatcher did not unwind (error raised inside a
vm->callDepth = savedCallDepth; // handler, bad opcode) still hold refcounted locals; discard
vm->running = savedRunning; // them the same way so force-restoring callDepth cannot orphan
// them above releaseVmState's reach. No out-arg copy-back and
// no popCallFrame here: this is the failure path, and
// popCallFrame would clear the error state the host reads.
while (vm->callDepth > savedCallDepth) {
BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1];
for (int32_t li = 0; li < frame->localCount; li++) {
basValRelease(&frame->locals[li]);
}
forStackTrim(vm, frame->savedForDepth);
vm->stmtPc = frame->savedStmtPc;
vm->stmtSp = frame->savedStmtSp;
vm->callDepth--;
}
vm->pc = savedPc;
vm->running = savedRunning;
return false; return false;
} }
@ -5919,9 +6280,18 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
} }
} }
vm->pc = savedPc; // END/OP_HALT and doEvents aborts break out of the loop with the
vm->callDepth = savedCallDepth; // callee's frames still pushed (OP_RET never ran for them). Pop them
vm->running = savedRunning; // properly -- out-arg copy-back, local release, FOR-stack trim --
// instead of force-lowering callDepth, which would strand the frames'
// refcounted locals above callDepth where releaseVmState never walks,
// and would drop ByRef out-args (e.g. Cancel = 1 set before END).
while (vm->callDepth > savedCallDepth) {
popCallFrame(vm);
}
vm->pc = savedPc;
vm->running = savedRunning;
// If we paused at a breakpoint during this sub, notify the host // If we paused at a breakpoint during this sub, notify the host
// so it can pause the event loop before any new event fires. // so it can pause the event loop before any new event fires.

View file

@ -265,6 +265,9 @@ typedef struct {
int32_t baseSlot; // base index in locals array int32_t baseSlot; // base index in locals array
int32_t localCount; // number of locals in this frame int32_t localCount; // number of locals in this frame
int32_t errorHandler; // ON ERROR GOTO target in this SUB (0 = none) int32_t errorHandler; // ON ERROR GOTO target in this SUB (0 = none)
int32_t savedForDepth; // vm->forDepth when this frame was pushed (restored on return so EXIT SUB/FUNCTION inside FOR bodies cannot leak FOR frames)
int32_t savedStmtPc; // caller's vm->stmtPc at call time (restored on return)
int32_t savedStmtSp; // caller's vm->stmtSp at call time (restored on return)
BasValueT locals[BAS_VM_MAX_LOCALS]; BasValueT locals[BAS_VM_MAX_LOCALS];
} BasCallFrameT; } BasCallFrameT;
@ -407,6 +410,7 @@ typedef struct {
BasValueT stack[BAS_VM_STACK_SIZE]; BasValueT stack[BAS_VM_STACK_SIZE];
int32_t sp; // stack pointer (index of next free slot) int32_t sp; // stack pointer (index of next free slot)
int32_t stmtSp; // eval-stack depth snapshot at last OP_LINE (statement boundary) int32_t stmtSp; // eval-stack depth snapshot at last OP_LINE (statement boundary)
int32_t stmtPc; // PC of the OP_LINE that opened the current statement (-1 = none seen, e.g. compacted release bytecode)
// Call stack // Call stack
BasCallFrameT callStack[BAS_VM_CALL_STACK_SIZE]; BasCallFrameT callStack[BAS_VM_CALL_STACK_SIZE];

View file

@ -210,10 +210,11 @@ int32_t appMain(DxeAppContextT *ctx) {
char *frmText = (char *)dvxResRead(res, resName, &frmSize); char *frmText = (char *)dvxResRead(res, resName, &frmSize);
if (!frmText) { if (!frmText) {
// Skip, don't stop: a numbering gap (from an empty form on // Stop at the first missing index: basBuild.c numbers the FORMn
// the writer side) must not drop every later form. The loop // resources densely (outIdx), so there is never a gap and the
// is bounded by BAS_MAX_FORM_RESOURCES. // first miss means there are no more forms. Probing all
continue; // BAS_MAX_FORM_RESOURCES indices would just waste startup lookups.
break;
} }
// dvxResRead returns exactly frmSize bytes with no terminator; the // dvxResRead returns exactly frmSize bytes with no terminator; the

View file

@ -37,17 +37,9 @@
static int32_t gFailCount = 0; static int32_t gFailCount = 0;
// Function prototypes // Function prototypes
static void primeMainFrame(BasVmT *vm, const BasModuleT *mod);
static void runProgram(const char *name, const char *source); static void runProgram(const char *name, const char *source);
// Set up callStack[0] as the implicit main frame for module-level code.
static void primeMainFrame(BasVmT *vm, const BasModuleT *mod) {
vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
vm->callDepth = 1;
}
static void runProgram(const char *name, const char *source) { static void runProgram(const char *name, const char *source) {
printf("=== %s ===\n", name); printf("=== %s ===\n", name);
@ -75,9 +67,6 @@ static void runProgram(const char *name, const char *source) {
BasVmT *vm = basVmCreate(); BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod); basVmLoadModule(vm, mod);
// Module-level code uses callStack[0] as implicit main frame
primeMainFrame(vm, mod);
BasVmResultE result = basVmRun(vm); BasVmResultE result = basVmRun(vm);
if (result != BAS_VM_HALTED && result != BAS_VM_OK) { if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
@ -907,7 +896,6 @@ int main(void) {
if (mod) { if (mod) {
BasVmT *vm = basVmCreate(); BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod); basVmLoadModule(vm, mod);
primeMainFrame(vm, mod);
basVmRun(vm); basVmRun(vm);
basVmDestroy(vm); basVmDestroy(vm);
basModuleFree(mod); basModuleFree(mod);
@ -1109,7 +1097,6 @@ int main(void) {
// Test basVmCallSub // Test basVmCallSub
BasVmT *vm = basVmCreate(); BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod); basVmLoadModule(vm, mod);
primeMainFrame(vm, mod);
p = basModuleFindProc(mod, "Command1_Click"); p = basModuleFindProc(mod, "Command1_Click");
@ -1907,7 +1894,6 @@ int main(void) {
} else { } else {
BasVmT *vm = basVmCreate(); BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod); basVmLoadModule(vm, mod);
primeMainFrame(vm, mod);
// Test: find and call Form1_Load (no args) // Test: find and call Form1_Load (no args)
const BasProcEntryT *loadProc = basModuleFindProc(mod, "Form1_Load"); const BasProcEntryT *loadProc = basModuleFindProc(mod, "Form1_Load");
@ -2002,7 +1988,6 @@ int main(void) {
if (mod) { if (mod) {
BasVmT *vm = basVmCreate(); BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod); basVmLoadModule(vm, mod);
primeMainFrame(vm, mod);
// Call NoParams with no args // Call NoParams with no args
const BasProcEntryT *p = basModuleFindProc(mod, "NoParams"); const BasProcEntryT *p = basModuleFindProc(mod, "NoParams");
@ -2099,7 +2084,6 @@ int main(void) {
if (mod) { if (mod) {
BasVmT *vm = basVmCreate(); BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod); basVmLoadModule(vm, mod);
primeMainFrame(vm, mod);
// Fire both events // Fire both events
const BasProcEntryT *p1 = basModuleFindProc(mod, "Form1_Load"); const BasProcEntryT *p1 = basModuleFindProc(mod, "Form1_Load");

View file

@ -453,6 +453,12 @@ typedef struct {
static MiniFormT *miniCreateForm(MiniRtT *rt, const char *name, int32_t formVarCount) { static MiniFormT *miniCreateForm(MiniRtT *rt, const char *name, int32_t formVarCount) {
MiniFormT *f = (MiniFormT *)calloc(1, sizeof(MiniFormT)); MiniFormT *f = (MiniFormT *)calloc(1, sizeof(MiniFormT));
if (!f) {
fprintf(stderr, "FATAL: out of memory allocating MiniFormT\n");
exit(EXIT_FAILURE);
}
snprintf(f->name, MINI_MAX_NAME, "%s", name); snprintf(f->name, MINI_MAX_NAME, "%s", name);
f->ctrls = NULL; f->ctrls = NULL;
@ -470,6 +476,12 @@ static MiniCtrlT *miniAddCtrl(MiniFormT *f, const char *name, const char *typeNa
// Store stable heap pointers: arrput may realloc f->ctrls, so returning // Store stable heap pointers: arrput may realloc f->ctrls, so returning
// an interior address would dangle once a second control is added. // an interior address would dangle once a second control is added.
MiniCtrlT *c = (MiniCtrlT *)calloc(1, sizeof(MiniCtrlT)); MiniCtrlT *c = (MiniCtrlT *)calloc(1, sizeof(MiniCtrlT));
if (!c) {
fprintf(stderr, "FATAL: out of memory allocating MiniCtrlT\n");
exit(EXIT_FAILURE);
}
snprintf(c->name, MINI_MAX_NAME, "%s", name); snprintf(c->name, MINI_MAX_NAME, "%s", name);
snprintf(c->typeName, MINI_MAX_NAME, "%s", typeName ? typeName : ""); snprintf(c->typeName, MINI_MAX_NAME, "%s", typeName ? typeName : "");
c->form = f; c->form = f;

View file

@ -55,6 +55,7 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <strings.h>
#include "dvxMem.h" #include "dvxMem.h"
@ -82,7 +83,6 @@
#define HELP_RULE_H 5 // horizontal-rule widget min height #define HELP_RULE_H 5 // horizontal-rule widget min height
#define HELP_RULE_LINE_Y 2 // rule line offset within the widget #define HELP_RULE_LINE_Y 2 // rule line offset within the widget
#define HELP_TITLE_MAX 300 // window-title buffer length #define HELP_TITLE_MAX 300 // window-title buffer length
#define HELP_CASE_FOLD ('a' - 'A') // ASCII upper->lower fold delta
// MAX_HISTORY -- fixed back-button history depth. 64 entries is plenty for a // MAX_HISTORY -- fixed back-button history depth. 64 entries is plenty for a
// help viewer; the oldest is dropped when exceeded (see the memmove below). // help viewer; the oldest is dropped when exceeded (see the memmove below).
@ -420,7 +420,11 @@ static void closeHelpFile(void) {
static int32_t countLines(const char *text) { static int32_t countLines(const char *text) {
int32_t count = 1; int32_t count = 1;
for (const char *p = text; p && *p; p++) { if (!text) {
return count;
}
for (const char *p = text; *p; p++) {
if (*p == '\n') { if (*p == '\n') {
count++; count++;
} }
@ -446,6 +450,10 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
td->text = dvxStrdup(payload); td->text = dvxStrdup(payload);
td->wrapWidth = -1; td->wrapWidth = -1;
w->data = td; w->data = td;
} else {
// The class handlers deref w->data unconditionally, so a
// data-less widget must not survive into the next layout.
widgetAllocRollback(w);
} }
} }
@ -464,6 +472,8 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
hd->text = dvxStrdup(payload); hd->text = dvxStrdup(payload);
hd->level = hdr->type - HLP_REC_HEADING1 + 1; hd->level = hdr->type - HLP_REC_HEADING1 + 1;
w->data = hd; w->data = hd;
} else {
widgetAllocRollback(w);
} }
} }
@ -474,14 +484,23 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
// Payload: target topic ID \0 display text \0 // Payload: target topic ID \0 display text \0
size_t targetLen = strlen(payload); size_t targetLen = strlen(payload);
// Reject a payload with no separator/display text within bounds so // Reject only a payload with no terminator within bounds so
// displayText doesn't point past the allocation. // displayText doesn't point past the allocation. A payload of
if (targetLen + 1 >= hdr->length) { // exactly targetLen + 1 bytes is the writer's legal encoding of
// a link with empty display text (displayText then points at the
// NUL the reader appended at payload[length]).
if (targetLen + 1 > hdr->length) {
break; break;
} }
const char *targetTopicId = payload; const char *targetTopicId = payload;
const char *displayText = payload + targetLen + 1; const char *displayText = payload + targetLen + 1;
// Empty display text: fall back to the target ID, matching the
// compiler's own HTML emitter.
if (displayText[0] == '\0') {
displayText = targetTopicId;
}
WidgetT *w = widgetAlloc(sContentBox, sHelpLinkTypeId); WidgetT *w = widgetAlloc(sContentBox, sHelpLinkTypeId);
if (w) { if (w) {
@ -491,6 +510,8 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
ld->displayText = dvxStrdup(displayText); ld->displayText = dvxStrdup(displayText);
ld->targetTopicId = dvxStrdup(targetTopicId); ld->targetTopicId = dvxStrdup(targetTopicId);
w->data = ld; w->data = ld;
} else {
widgetAllocRollback(w);
} }
} }
@ -532,14 +553,13 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
} }
} }
// wgtImage takes ownership of pixels even on
// failure (it frees them itself before returning
// NULL), so no cleanup is needed here.
WidgetT *imgWgt = wgtImage(parent, pixels, imgW, imgH, imgP); WidgetT *imgWgt = wgtImage(parent, pixels, imgW, imgH, imgP);
if (imgWgt && hasAlpha) { if (imgWgt && hasAlpha) {
wgtImageSetTransparent(imgWgt, true, keyColor); wgtImageSetTransparent(imgWgt, true, keyColor);
} else if (!imgWgt) {
// wgtImage takes ownership only on success;
// free the decoded pixels if it failed.
dvxFreeImage(pixels);
} }
} }
} }
@ -573,6 +593,8 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
ld->wrapWidth = -1; ld->wrapWidth = -1;
w->data = ld; w->data = ld;
} else {
widgetAllocRollback(w);
} }
} }
@ -595,6 +617,8 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
nd->wrapWidth = -1; nd->wrapWidth = -1;
nd->noteType = hdr->flags; nd->noteType = hdr->flags;
w->data = nd; w->data = nd;
} else {
widgetAllocRollback(w);
} }
} }
@ -613,6 +637,8 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
cd->text = dvxStrdup(payload); cd->text = dvxStrdup(payload);
cd->lineCount = countLines(cd->text); cd->lineCount = countLines(cd->text);
w->data = cd; w->data = cd;
} else {
widgetAllocRollback(w);
} }
} }
@ -1174,45 +1200,17 @@ static void onSearch(WidgetT *w) {
// Linear scan of keyword index for case-insensitive substring match // Linear scan of keyword index for case-insensitive substring match
int32_t firstMatch = -1; int32_t firstMatch = -1;
int32_t needleLen = strlen(searchBuf);
for (uint32_t i = 0; i < sHeader.indexCount; i++) { for (uint32_t i = 0; i < sHeader.indexCount; i++) {
const char *keyword = hlpString(sIndexEntries[i].keywordStr); const char *keyword = hlpString(sIndexEntries[i].keywordStr);
// Case-insensitive substring search bool found = false;
const char *kp = keyword;
const char *sp = searchBuf;
bool found = false;
while (*kp && !found) { for (const char *kp = keyword; *kp; kp++) {
const char *k = kp; if (strncasecmp(kp, searchBuf, needleLen) == 0) {
const char *s = sp;
bool match = true;
while (*s && *k) {
char kc = *k;
char sc = *s;
if (kc >= 'A' && kc <= 'Z') {
kc += HELP_CASE_FOLD;
}
if (sc >= 'A' && sc <= 'Z') {
sc += HELP_CASE_FOLD;
}
if (kc != sc) {
match = false;
break;
}
k++;
s++;
}
if (match && *s == '\0') {
found = true; found = true;
break;
} }
kp++;
} }
if (found) { if (found) {

View file

@ -42,10 +42,13 @@
// Module state // Module state
// ============================================================ // ============================================================
// Dynamic app slot table (stb_ds array). App IDs are array indices. // Dynamic app slot index (stb_ds array of pointers). App IDs are array
// Slot 0 is reserved (represents the shell itself); apps use slots 1+. // indices. Slot 0 is reserved (represents the shell itself); apps use
// New slots are appended as needed; freed slots are recycled. // slots 1+. New slots are appended as needed; freed slots are recycled.
static ShellAppT *sApps = NULL; // Each ShellAppT is heap-allocated once and only its pointer lives here,
// so a slot's address never moves when the index array grows -- held
// ShellAppT pointers (and app->name) stay valid across later app loads.
static ShellAppT **sApps = NULL;
// Ctrl+Esc handler -- set by taskmgr DXE constructor, NULL if not loaded // Ctrl+Esc handler -- set by taskmgr DXE constructor, NULL if not loaded
void (*shellCtrlEscFn)(AppContextT *ctx) = NULL; void (*shellCtrlEscFn)(AppContextT *ctx) = NULL;
@ -58,8 +61,11 @@ static int32_t allocSlot(void);
static void appTaskWrapper(void *arg); static void appTaskWrapper(void *arg);
static void cleanupTempFile(ShellAppT *app); static void cleanupTempFile(ShellAppT *app);
static int32_t copyFile(const char *src, const char *dst); static int32_t copyFile(const char *src, const char *dst);
static void destroyAppWindows(AppContextT *ctx, int32_t appId);
static ShellAppT *findLoadedPath(const char *path); static ShellAppT *findLoadedPath(const char *path);
static void killAppTask(ShellAppT *app);
static void makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize); static void makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize);
static void releaseAppResources(ShellAppT *app);
void shellAppInit(void); void shellAppInit(void);
int32_t shellAppSlotCount(void); int32_t shellAppSlotCount(void);
void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize); void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize);
@ -69,6 +75,7 @@ ShellAppT *shellGetApp(int32_t appId);
int32_t shellLoadApp(AppContextT *ctx, const char *path); int32_t shellLoadApp(AppContextT *ctx, const char *path);
static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const char *args); static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const char *args);
int32_t shellLoadAppWithArgs(AppContextT *ctx, const char *path, const char *args); int32_t shellLoadAppWithArgs(AppContextT *ctx, const char *path, const char *args);
static int32_t shellLoadFail(void *handle, const char *tempPath);
void shellReapApp(AppContextT *ctx, ShellAppT *app); void shellReapApp(AppContextT *ctx, ShellAppT *app);
bool shellReapApps(AppContextT *ctx); bool shellReapApps(AppContextT *ctx);
int32_t shellRunningAppCount(void); int32_t shellRunningAppCount(void);
@ -76,18 +83,25 @@ void shellTerminateAllApps(AppContextT *ctx);
// Find the first free slot, starting at 1 (slot 0 is the shell). // Find the first free slot, starting at 1 (slot 0 is the shell).
// Returns the slot index which also serves as the app's unique ID. // Returns the slot index which also serves as the app's unique ID, or -1
// If no free slot exists, appends a new one. // if a new slot was needed but could not be allocated.
// If no free slot exists, appends a new heap-allocated one.
static int32_t allocSlot(void) { static int32_t allocSlot(void) {
for (int32_t i = 1; i < arrlen(sApps); i++) { for (int32_t i = 1; i < arrlen(sApps); i++) {
if (sApps[i].state == AppStateFreeE) { if (sApps[i]->state == AppStateFreeE) {
return i; return i;
} }
} }
// No free slot -- grow the array // No free slot -- allocate a new one. The ShellAppT lives on the heap
ShellAppT newSlot; // and only its pointer is appended to the index, so its address stays
memset(&newSlot, 0, sizeof(newSlot)); // stable even when the index array reallocs.
ShellAppT *newSlot = (ShellAppT *)calloc(1, sizeof(ShellAppT));
if (!newSlot) {
return -1;
}
arrput(sApps, newSlot); arrput(sApps, newSlot);
return arrlen(sApps) - 1; return arrlen(sApps) - 1;
@ -104,20 +118,15 @@ static int32_t allocSlot(void) {
// this function never completes -- the task is killed externally via // this function never completes -- the task is killed externally via
// shellForceKillApp + tsKill. // shellForceKillApp + tsKill.
static void appTaskWrapper(void *arg) { static void appTaskWrapper(void *arg) {
// Look up the app by ID, not by pointer. The sApps array may have // The task's arg is the app's slot ID. Resolve it to the app's stable
// been reallocated between tsCreate and the first time this task runs // heap pointer once -- slots never move, so the pointer stays valid for
// (e.g. another app was loaded in the meantime), which would // the whole task lifetime (even across other app loads) without any
// invalidate a direct ShellAppT pointer. // need to re-resolve after the app's main loop returns.
int32_t appId = (int32_t)(intptr_t)arg; int32_t appId = (int32_t)(intptr_t)arg;
ShellAppT *app = &sApps[appId]; ShellAppT *app = sApps[appId];
app->dxeCtx->shellCtx->currentAppId = app->appId; app->dxeCtx->shellCtx->currentAppId = app->appId;
app->entryFn(app->dxeCtx); app->entryFn(app->dxeCtx);
// Re-resolve the slot: the app's main loop may have run for a long
// time, and any app loaded meanwhile can grow (realloc, move) sApps
// -- writing through the pointer captured above would corrupt freed
// memory and the app would never reach Terminating to be reaped.
app = &sApps[appId];
app->dxeCtx->shellCtx->currentAppId = 0; app->dxeCtx->shellCtx->currentAppId = 0;
// App returned from its main loop -- mark for reaping. This task is // App returned from its main loop -- mark for reaping. This task is
@ -178,12 +187,67 @@ static int32_t copyFile(const char *src, const char *dst) {
} }
// Destroy every window belonging to an app SYNCHRONOUSLY, before its DXE
// is unmapped -- a deferred destroy would leave live timer/poll/window
// callbacks pointing into unmapped code for the rest of the dispatch pass.
//
// First pass: flag every window of the app destroyPending (and hide it).
// This makes all of them inert at once, so the successor-focus selection
// inside each destroy can only land on OTHER apps' windows -- focus
// callbacks (and BASIC Activate events) can never fire into the app being
// torn down. The flush hint is incremented alongside the flag so
// dvxDestroyWindowNow's decrement stays balanced.
//
// Second pass: rescan-until-none-left rather than a single index walk,
// because callbacks fired mid-destroy (another app's onFocus) can mutate
// the window stack and a plain walk could skip an entry.
static void destroyAppWindows(AppContextT *ctx, int32_t appId) {
for (int32_t i = 0; i < ctx->stack.count; i++) {
WindowT *win = ctx->stack.windows[i];
if (win->appId == appId && !win->destroyPending) {
win->destroyPending = true;
ctx->pendingDestroyCount++;
if (win->visible) {
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
win->visible = false;
}
// Silent focus drop (mirrors deferDestroyWindow): with
// focusedIdx pre-cleared, the wmSetFocus that follows the
// first destroy sees no old window and fires only the
// successor's onFocus -- never onBlur/Deactivate into the
// dying app.
if (win->focused) {
win->focused = false;
ctx->stack.focusedIdx = -1;
}
}
}
bool found = true;
while (found) {
found = false;
for (int32_t i = ctx->stack.count - 1; i >= 0; i--) {
if (ctx->stack.windows[i]->appId == appId) {
dvxDestroyWindowNow(ctx, ctx->stack.windows[i]);
found = true;
break;
}
}
}
}
// Find an active slot with the given DXE path. Returns the slot pointer, // Find an active slot with the given DXE path. Returns the slot pointer,
// or NULL if no running app was loaded from this path. // or NULL if no running app was loaded from this path.
static ShellAppT *findLoadedPath(const char *path) { static ShellAppT *findLoadedPath(const char *path) {
for (int32_t i = 1; i < arrlen(sApps); i++) { for (int32_t i = 1; i < arrlen(sApps); i++) {
if (sApps[i].state != AppStateFreeE && strcmp(sApps[i].path, path) == 0) { if (sApps[i]->state != AppStateFreeE && strcmp(sApps[i]->path, path) == 0) {
return &sApps[i]; return sApps[i];
} }
} }
@ -191,6 +255,17 @@ static ShellAppT *findLoadedPath(const char *path) {
} }
// Kill an app's cooperative task if it has one and it is still alive.
// Shared by shellForceKillApp and shellReapApp.
static void killAppTask(ShellAppT *app) {
if (app->hasMainLoop && app->mainTaskId > 0) {
if (tsGetState(app->mainTaskId) != TaskStateTerminated) {
tsKill(app->mainTaskId);
}
}
}
// Build a temp path for a multi-instance copy. Uses the TEMP or TMP // Build a temp path for a multi-instance copy. Uses the TEMP or TMP
// environment variable if set, otherwise falls back to the current directory. // environment variable if set, otherwise falls back to the current directory.
// The slot ID is embedded in the filename to ensure uniqueness. // The slot ID is embedded in the filename to ensure uniqueness.
@ -218,10 +293,28 @@ static void makeTempPath(const char *origPath, int32_t id, char *out, int32_t ou
} }
// Shared teardown tail for both reap and force-kill: unmap the DXE, delete
// any multi-instance temp copy, release the heap app context, and reset the
// app's memory accounting. Callers handle the parts that differ between the
// two paths (app-code shutdown or its deliberate omission, callback purging,
// window destruction, and the final state/log line).
static void releaseAppResources(ShellAppT *app) {
if (app->dxeHandle) {
dlclose(app->dxeHandle);
app->dxeHandle = NULL;
}
cleanupTempFile(app);
free(app->dxeCtx);
app->dxeCtx = NULL;
dvxMemResetApp(app->appId);
}
void shellAppInit(void) { void shellAppInit(void) {
// Seed slot 0 (reserved for the shell) // Seed slot 0 (reserved for the shell). Slots are heap-allocated so
ShellAppT slot0; // their addresses stay stable as the sApps index array grows.
memset(&slot0, 0, sizeof(slot0)); ShellAppT *slot0 = (ShellAppT *)calloc(1, sizeof(ShellAppT));
arrput(sApps, slot0); arrput(sApps, slot0);
} }
@ -241,58 +334,40 @@ int32_t shellEnsureConfigDir(const DxeAppContextT *ctx) {
} }
// Forcible kill -- the shutdownFn IS still called (so the app can unregister // Forcible kill -- reclaims an app WITHOUT running any of the app's own code.
// shell callbacks that would otherwise dangle), but defensively: currentAppId // End Task's primary target is a HUNG or FAULTING app: its shutdownFn would
// is cleared first so a re-fault during shutdown lands in shell-level crash // re-enter the same broken state (re-hang the single-threaded cooperative
// recovery rather than re-entering this function. Used for crashed apps and // shell, or re-fault and longjmp past all cleanup), so the shutdownFn is
// for "End Task". // deliberately NOT called here. Instead the shell itself purges every
// Cleanup order matters: windows first (removes them from the compositor), // callback the app registered (desktop-update hooks, idle/serial pollers)
// then the task (frees the stack), then the DXE handle (unmaps the code). // via shellPurgeAppCallbacks, so nothing dangles once the DXE is unmapped.
// If we closed the DXE first, destroying windows could call into unmapped // Used for crashed apps, "End Task", and shell shutdown.
// callback code and crash the shell. // Cleanup order matters: purge shell callbacks and destroy windows (removes
// them from the compositor) first, then the task (frees the stack), then the
// DXE handle (unmaps the code). If we closed the DXE first, destroying
// windows could call into unmapped callback code and crash the shell.
void shellForceKillApp(AppContextT *ctx, ShellAppT *app) { void shellForceKillApp(AppContextT *ctx, ShellAppT *app) {
if (!app || app->state == AppStateFreeE) { if (!app || app->state == AppStateFreeE) {
return; return;
} }
// Call shutdown hook so the app can unregister shell callbacks (e.g. // Purge every shell-side callback this app registered (desktop-update and
// shellRegisterDesktopUpdate) before its DXE is unmapped; otherwise the // idle handlers) BEFORE its DXE is unmapped. A force kill never runs the
// dangling function pointers crash the next shellDesktopUpdate(). // app's shutdownFn -- the app is being killed precisely because its own
// currentAppId is cleared BEFORE the call so that if a crashed app // code cannot be trusted to run -- so the shell, not the app, is
// re-faults inside its own shutdownFn, the longjmp returns to crash // responsible for unhooking these; otherwise the dangling pointers crash
// recovery (shellMain.c) with currentAppId == 0 -- recovery then treats // the next shellDesktopUpdate() or idle frame.
// it as a shell-level fault and does NOT re-enter shellForceKillApp, shellPurgeAppCallbacks(app->appId);
// breaking the otherwise-infinite kill loop.
if (app->shutdownFn) {
ctx->currentAppId = 0;
app->shutdownFn();
}
// Destroy all windows belonging to this app. Walk backwards because // Destroy all windows belonging to this app -- synchronously, even if
// dvxDestroyWindow removes the window from the stack, shifting indices. // End Task ran inside a widget callback (dispatchDepth > 0), so no
for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { // timer/poll/window callback can point into the DXE once it unmaps.
if (ctx->stack.windows[i]->appId == app->appId) { // Also reclaims windows the app had already self-deferred.
dvxDestroyWindow(ctx, ctx->stack.windows[i]); destroyAppWindows(ctx, app->appId);
}
}
// Kill the task if it has one // Kill the task, then unmap the DXE and release the app's resources.
if (app->hasMainLoop && app->mainTaskId > 0) { killAppTask(app);
if (tsGetState(app->mainTaskId) != TaskStateTerminated) { releaseAppResources(app);
tsKill(app->mainTaskId);
}
}
// Close the DXE
if (app->dxeHandle) {
dlclose(app->dxeHandle);
app->dxeHandle = NULL;
}
cleanupTempFile(app);
free(app->dxeCtx);
app->dxeCtx = NULL;
dvxMemResetApp(app->appId);
app->state = AppStateFreeE; app->state = AppStateFreeE;
dvxLog("Shell: force-killed app '%s'", app->name); dvxLog("Shell: force-killed app '%s'", app->name);
} }
@ -303,11 +378,11 @@ ShellAppT *shellGetApp(int32_t appId) {
return NULL; return NULL;
} }
if (sApps[appId].state == AppStateFreeE) { if (sApps[appId]->state == AppStateFreeE) {
return NULL; return NULL;
} }
return &sApps[appId]; return sApps[appId];
} }
@ -333,7 +408,10 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
// Allocate a slot // Allocate a slot
int32_t id = allocSlot(); int32_t id = allocSlot();
// allocSlot grows the dynamic slot array on demand, so it never fails. if (id < 0) {
dvxErrorBox(ctx, NULL, "Out of memory: cannot allocate an application slot.");
return -1;
}
// Check if this DXE is already loaded. If so, check whether the app // Check if this DXE is already loaded. If so, check whether the app
// allows multiple instances. We read the descriptor from the existing // allows multiple instances. We read the descriptor from the existing
@ -382,12 +460,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
dvxLog("DXE load failed: %s", msg); dvxLog("DXE load failed: %s", msg);
dvxSetBusy(ctx, false); dvxSetBusy(ctx, false);
dvxErrorBox(ctx, NULL, msg); dvxErrorBox(ctx, NULL, msg);
return shellLoadFail(handle, tempPath);
if (tempPath[0]) {
remove(tempPath);
}
return -1;
} }
// Look up required symbols // Look up required symbols
@ -399,13 +472,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
dvxLog("DXE symbol error: %s", msg); dvxLog("DXE symbol error: %s", msg);
dvxSetBusy(ctx, false); dvxSetBusy(ctx, false);
dvxErrorBox(ctx, NULL, msg); dvxErrorBox(ctx, NULL, msg);
dlclose(handle); return shellLoadFail(handle, tempPath);
if (tempPath[0]) {
remove(tempPath);
}
return -1;
} }
int32_t (*entry)(DxeAppContextT *) = (int32_t (*)(DxeAppContextT *))dlsym(handle, "_appMain"); int32_t (*entry)(DxeAppContextT *) = (int32_t (*)(DxeAppContextT *))dlsym(handle, "_appMain");
@ -415,19 +482,13 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
snprintf(msg, sizeof(msg), "%s: missing appMain", platformPathBaseName(path)); snprintf(msg, sizeof(msg), "%s: missing appMain", platformPathBaseName(path));
dvxSetBusy(ctx, false); dvxSetBusy(ctx, false);
dvxErrorBox(ctx, NULL, msg); dvxErrorBox(ctx, NULL, msg);
dlclose(handle); return shellLoadFail(handle, tempPath);
if (tempPath[0]) {
remove(tempPath);
}
return -1;
} }
void (*shutdown)(void) = (void (*)(void))dlsym(handle, "_appShutdown"); void (*shutdown)(void) = (void (*)(void))dlsym(handle, "_appShutdown");
// Fill in the app slot // Fill in the app slot
ShellAppT *app = &sApps[id]; ShellAppT *app = sApps[id];
memset(app, 0, sizeof(*app)); memset(app, 0, sizeof(*app));
app->appId = id; app->appId = id;
@ -447,14 +508,8 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
if (!app->dxeCtx) { if (!app->dxeCtx) {
dvxLog("Shell: failed to allocate app context for %s", app->name); dvxLog("Shell: failed to allocate app context for %s", app->name);
dvxSetBusy(ctx, false); dvxSetBusy(ctx, false);
dlclose(handle);
app->state = AppStateFreeE; app->state = AppStateFreeE;
return shellLoadFail(handle, tempPath);
if (tempPath[0]) {
remove(tempPath);
}
return -1;
} }
app->dxeCtx->shellCtx = ctx; app->dxeCtx->shellCtx = ctx;
@ -508,16 +563,10 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
ctx->currentAppId = 0; ctx->currentAppId = 0;
dvxSetBusy(ctx, false); dvxSetBusy(ctx, false);
dvxErrorBox(ctx, NULL, "Failed to create task for application."); dvxErrorBox(ctx, NULL, "Failed to create task for application.");
dlclose(handle);
free(app->dxeCtx); free(app->dxeCtx);
app->dxeCtx = NULL; app->dxeCtx = NULL;
app->state = AppStateFreeE; app->state = AppStateFreeE;
return shellLoadFail(handle, tempPath);
if (tempPath[0]) {
remove(tempPath);
}
return -1;
} }
app->mainTaskId = (uint32_t)taskId; app->mainTaskId = (uint32_t)taskId;
@ -529,6 +578,9 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
app->entryFn(app->dxeCtx); app->entryFn(app->dxeCtx);
} }
// The slot pointer is stable across the callback-only entry point above
// (which can pump dvxUpdate and yield to other app loads), so no
// re-resolve is needed.
ctx->currentAppId = 0; ctx->currentAppId = 0;
app->state = AppStateRunningE; app->state = AppStateRunningE;
@ -544,6 +596,25 @@ int32_t shellLoadAppWithArgs(AppContextT *ctx, const char *path, const char *arg
} }
// Shared load-failure cleanup for shellLoadAppInternal: unmap the DXE (if
// dlopen got far enough to return a handle) and delete any multi-instance
// temp copy. Touches only these locals, never the slot, so it is safe from
// every failure branch regardless of how far the load progressed. The busy
// hourglass, the error dialog, and any slot-state reset are handled by the
// caller before this runs. Always returns -1 for the caller to propagate.
static int32_t shellLoadFail(void *handle, const char *tempPath) {
if (handle) {
dlclose(handle);
}
if (tempPath[0]) {
remove(tempPath);
}
return -1;
}
// Graceful reap -- called from shellReapApps when an app has reached // Graceful reap -- called from shellReapApps when an app has reached
// the Terminating state. Unlike forceKill, this calls the app's // the Terminating state. Unlike forceKill, this calls the app's
// shutdown hook (if provided) giving it a chance to save state, close // shutdown hook (if provided) giving it a chance to save state, close
@ -561,30 +632,20 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) {
ctx->currentAppId = 0; ctx->currentAppId = 0;
} }
// Destroy all windows belonging to this app // Destroy all windows belonging to this app, synchronously (see
for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { // destroyAppWindows) -- they must be gone before dlclose unmaps the
if (ctx->stack.windows[i]->appId == app->appId) { // callback code they point into.
dvxDestroyWindow(ctx, ctx->stack.windows[i]); destroyAppWindows(ctx, app->appId);
}
}
// Kill the task if it has one and it's still alive // Kill the task if it has one and it's still alive.
if (app->hasMainLoop && app->mainTaskId > 0) { killAppTask(app);
if (tsGetState(app->mainTaskId) != TaskStateTerminated) {
tsKill(app->mainTaskId);
}
}
// Close the DXE // Safety net: a well-behaved shutdownFn already unregistered its shell
if (app->dxeHandle) { // callbacks above, but the shell owns teardown regardless of app
dlclose(app->dxeHandle); // cooperation -- drop any the app forgot before the DXE is unmapped.
app->dxeHandle = NULL; shellPurgeAppCallbacks(app->appId);
}
cleanupTempFile(app); releaseAppResources(app);
free(app->dxeCtx);
app->dxeCtx = NULL;
dvxMemResetApp(app->appId);
dvxLog("Shell: reaped app '%s'", app->name); dvxLog("Shell: reaped app '%s'", app->name);
app->state = AppStateFreeE; app->state = AppStateFreeE;
} }
@ -599,8 +660,8 @@ bool shellReapApps(AppContextT *ctx) {
bool reaped = false; bool reaped = false;
for (int32_t i = 1; i < arrlen(sApps); i++) { for (int32_t i = 1; i < arrlen(sApps); i++) {
if (sApps[i].state == AppStateTerminatingE) { if (sApps[i]->state == AppStateTerminatingE) {
shellReapApp(ctx, &sApps[i]); shellReapApp(ctx, sApps[i]);
reaped = true; reaped = true;
continue; continue;
} }
@ -608,18 +669,18 @@ bool shellReapApps(AppContextT *ctx) {
// Callback-only apps terminate when their last window closes. // Callback-only apps terminate when their last window closes.
// They have no main loop to set AppStateTerminatingE, so we // They have no main loop to set AppStateTerminatingE, so we
// detect termination by checking for zero remaining windows. // detect termination by checking for zero remaining windows.
if (sApps[i].state == AppStateRunningE && !sApps[i].hasMainLoop) { if (sApps[i]->state == AppStateRunningE && !sApps[i]->hasMainLoop) {
bool hasWindow = false; bool hasWindow = false;
for (int32_t w = 0; w < ctx->stack.count; w++) { for (int32_t w = 0; w < ctx->stack.count; w++) {
if (ctx->stack.windows[w]->appId == sApps[i].appId) { if (ctx->stack.windows[w]->appId == sApps[i]->appId) {
hasWindow = true; hasWindow = true;
break; break;
} }
} }
if (!hasWindow) { if (!hasWindow) {
shellReapApp(ctx, &sApps[i]); shellReapApp(ctx, sApps[i]);
reaped = true; reaped = true;
} }
} }
@ -633,7 +694,7 @@ int32_t shellRunningAppCount(void) {
int32_t count = 0; int32_t count = 0;
for (int32_t i = 1; i < arrlen(sApps); i++) { for (int32_t i = 1; i < arrlen(sApps); i++) {
if (sApps[i].state == AppStateRunningE || sApps[i].state == AppStateLoadedE) { if (sApps[i]->state == AppStateRunningE || sApps[i]->state == AppStateLoadedE) {
count++; count++;
} }
} }
@ -644,8 +705,8 @@ int32_t shellRunningAppCount(void) {
void shellTerminateAllApps(AppContextT *ctx) { void shellTerminateAllApps(AppContextT *ctx) {
for (int32_t i = 1; i < arrlen(sApps); i++) { for (int32_t i = 1; i < arrlen(sApps); i++) {
if (sApps[i].state != AppStateFreeE) { if (sApps[i]->state != AppStateFreeE) {
shellForceKillApp(ctx, &sApps[i]); shellForceKillApp(ctx, sApps[i]);
} }
} }
} }

View file

@ -164,10 +164,12 @@ bool shellReapApps(AppContextT *ctx);
// destroys windows, kills task, closes DXE handle. // destroys windows, kills task, closes DXE handle.
void shellReapApp(AppContextT *ctx, ShellAppT *app); void shellReapApp(AppContextT *ctx, ShellAppT *app);
// Forcibly kill an app (Task Manager "End Task"). Still calls shutdownFn so // Forcibly kill an app (Task Manager "End Task", crash recovery, shell
// the app can unregister shell callbacks (which would otherwise dangle after // shutdown). Does NOT run the app's shutdownFn -- a force kill targets a
// dlclose), but currentAppId is cleared first so a re-fault during shutdown // hung or faulting app whose own code cannot be trusted to run (it would
// is caught by shell-level crash recovery rather than looping back here. // re-hang the cooperative shell or re-fault past cleanup). The shell purges
// the callbacks the app registered itself, via shellPurgeAppCallbacks, so
// nothing dangles after dlclose.
void shellForceKillApp(AppContextT *ctx, ShellAppT *app); void shellForceKillApp(AppContextT *ctx, ShellAppT *app);
// Terminate all running apps (shell shutdown) // Terminate all running apps (shell shutdown)
@ -221,6 +223,13 @@ void shellRegisterIdle(void (*fn)(void *ctx), void *ctx);
// double-unregister is harmless. // double-unregister is harmless.
void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx); void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx);
// Drop every idle and desktop-update callback an app registered, keyed by the
// appId captured at registration time. shellForceKillApp and shellReapApp
// call this before dlclose so a force or crash kill can reclaim an app WITHOUT
// running the app's own shutdownFn. appId < 1 (shell-owned and
// persistent-DXE registrations) is left untouched.
void shellPurgeAppCallbacks(int32_t appId);
// ============================================================ // ============================================================
// Ctrl+Esc handler (set by taskmgr library) // Ctrl+Esc handler (set by taskmgr library)
// ============================================================ // ============================================================

View file

@ -76,18 +76,27 @@ static jmp_buf sCrashJmp;
// Volatile because it's written from a signal handler context. Tells // Volatile because it's written from a signal handler context. Tells
// the recovery code which signal fired (for logging/diagnostics). // the recovery code which signal fired (for logging/diagnostics).
static volatile int sCrashSignal = 0; static volatile int sCrashSignal = 0;
// Desktop update callback list (dynamic, managed via stb_ds arrput/arrdel) // Desktop update callback list (dynamic, managed via stb_ds arrput/arrdel).
typedef void (*DesktopUpdateFnT)(void); // Each entry records the appId that registered it (captured from
static DesktopUpdateFnT *sDesktopUpdateFns = NULL; // sCtx.currentAppId at registration time) so shellPurgeAppCallbacks can drop
// a force-killed app's callbacks without depending on the app's shutdownFn.
typedef struct {
void (*fn)(void);
int32_t appId;
} DesktopUpdateHandlerT;
static DesktopUpdateHandlerT *sDesktopUpdateFns = NULL;
// Idle handler list (dynamic, managed via stb_ds arrput/arrdel). Walked // Idle handler list (dynamic, managed via stb_ds arrput/arrdel). Walked
// every idle frame by shellIdleDispatch. Serial/secLink pollers register // every idle frame by shellIdleDispatch. Serial/secLink pollers register
// here instead of overwriting the context's single idleCallback slot, so a // here instead of overwriting the context's single idleCallback slot, so a
// background connection keeps pumping regardless of which app is foreground // background connection keeps pumping regardless of which app is foreground
// and the shell's cooperative yield can never be clobbered. // and the shell's cooperative yield can never be clobbered. appId is the
// owner captured at registration so shellPurgeAppCallbacks can reclaim it.
typedef struct { typedef struct {
void (*fn)(void *ctx); void (*fn)(void *ctx);
void *ctx; void *ctx;
int32_t appId;
} IdleHandlerT; } IdleHandlerT;
static IdleHandlerT *sIdleHandlers = NULL; static IdleHandlerT *sIdleHandlers = NULL;
@ -104,6 +113,7 @@ static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData);
void shellDesktopUpdate(void); void shellDesktopUpdate(void);
static void shellIdleDispatch(void *ctx); static void shellIdleDispatch(void *ctx);
int shellMain(int argc, char *argv[]); int shellMain(int argc, char *argv[]);
void shellPurgeAppCallbacks(int32_t appId);
void shellRegisterDesktopUpdate(void (*updateFn)(void)); void shellRegisterDesktopUpdate(void (*updateFn)(void));
void shellRegisterIdle(void (*fn)(void *ctx), void *ctx); void shellRegisterIdle(void (*fn)(void *ctx), void *ctx);
void shellUnregisterDesktopUpdate(void (*updateFn)(void)); void shellUnregisterDesktopUpdate(void (*updateFn)(void));
@ -181,7 +191,7 @@ static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) {
void shellDesktopUpdate(void) { void shellDesktopUpdate(void) {
for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) { for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) {
sDesktopUpdateFns[i](); sDesktopUpdateFns[i].fn();
} }
} }
@ -210,8 +220,40 @@ static void shellIdleDispatch(void *ctx) {
} }
// Drop every idle and desktop-update callback registered by the given app,
// matched on the appId captured when each was registered. shellForceKillApp
// (and, defensively, shellReapApp) calls this before dlclose so a force or
// crash kill reclaims an app's shell-side registrations WITHOUT running the
// app's own -- possibly hung or faulting -- shutdownFn. Otherwise the
// pointers would dangle into unmapped DXE code and the next shellDesktopUpdate
// or idle frame would jump into it. appId < 1 (the shell's own and
// persistent-DXE registrations) is never purged. Both lists are walked
// back-to-front so arrdel's index shifts cannot skip a matching entry.
void shellPurgeAppCallbacks(int32_t appId) {
if (appId < 1) {
return;
}
for (int32_t i = arrlen(sDesktopUpdateFns) - 1; i >= 0; i--) {
if (sDesktopUpdateFns[i].appId == appId) {
arrdel(sDesktopUpdateFns, i);
}
}
for (int32_t i = arrlen(sIdleHandlers) - 1; i >= 0; i--) {
if (sIdleHandlers[i].appId == appId) {
arrdel(sIdleHandlers, i);
}
}
}
void shellRegisterDesktopUpdate(void (*updateFn)(void)) { void shellRegisterDesktopUpdate(void (*updateFn)(void)) {
arrput(sDesktopUpdateFns, updateFn); DesktopUpdateHandlerT handler;
handler.fn = updateFn;
handler.appId = sCtx.currentAppId;
arrput(sDesktopUpdateFns, handler);
} }
@ -226,15 +268,16 @@ void shellRegisterIdle(void (*fn)(void *ctx), void *ctx) {
} }
} }
handler.fn = fn; handler.fn = fn;
handler.ctx = ctx; handler.ctx = ctx;
handler.appId = sCtx.currentAppId;
arrput(sIdleHandlers, handler); arrput(sIdleHandlers, handler);
} }
void shellUnregisterDesktopUpdate(void (*updateFn)(void)) { void shellUnregisterDesktopUpdate(void (*updateFn)(void)) {
for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) { for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) {
if (sDesktopUpdateFns[i] == updateFn) { if (sDesktopUpdateFns[i].fn == updateFn) {
arrdel(sDesktopUpdateFns, i); arrdel(sDesktopUpdateFns, i);
return; return;
} }
@ -415,16 +458,27 @@ int shellMain(int argc, char *argv[]) {
// safe to call any shell function. // safe to call any shell function.
if (setjmp(sCrashJmp) != 0) { if (setjmp(sCrashJmp) != 0) {
// Returned here from crash handler via longjmp. // Returned here from crash handler via longjmp.
// Capture the crashed task's app id BEFORE tsRecoverToMain, which
// restores the main task's context value (currentAppId back to 0).
int32_t crashedAppId = sCtx.currentAppId;
// The task switcher's currentIdx still points to the crashed task. // The task switcher's currentIdx still points to the crashed task.
// Fix it before doing anything else so the scheduler is consistent. // Fix it before doing anything else so the scheduler is consistent.
tsRecoverToMain(); tsRecoverToMain();
// The longjmp discarded every in-flight dvxUpdate/dispatch frame
// before its dispatchDepth-- could run, so reset the counter here.
// Otherwise dvxDestroyWindow would defer forever (including the
// crashed app's own windows below, past its DXE being closed) and
// flushPendingDestroys would never fire again.
sCtx.dispatchDepth = 0;
// Platform handler already logged signal name and register dump. // Platform handler already logged signal name and register dump.
// Log app-specific info here. // Log app-specific info here.
dvxLog("Current app ID: %ld", (long)sCtx.currentAppId); dvxLog("Current app ID: %ld", (long)crashedAppId);
if (sCtx.currentAppId > 0) { if (crashedAppId > 0) {
ShellAppT *crashedApp = shellGetApp(sCtx.currentAppId); ShellAppT *crashedApp = shellGetApp(crashedAppId);
if (crashedApp) { if (crashedApp) {
dvxLog("App name: %s", crashedApp->name); dvxLog("App name: %s", crashedApp->name);
@ -436,13 +490,13 @@ int shellMain(int argc, char *argv[]) {
dvxLog("Crashed in shell (task 0)"); dvxLog("Crashed in shell (task 0)");
} }
dvxLog("Recovering from crash, killing app %ld", (long)sCtx.currentAppId); dvxLog("Recovering from crash, killing app %ld", (long)crashedAppId);
// Clear busy cursor so the fault dialog is interactive // Clear busy cursor so the fault dialog is interactive
dvxSetBusy(&sCtx, false); dvxSetBusy(&sCtx, false);
if (sCtx.currentAppId > 0) { if (crashedAppId > 0) {
ShellAppT *app = shellGetApp(sCtx.currentAppId); ShellAppT *app = shellGetApp(crashedAppId);
if (app) { if (app) {
char msg[256]; char msg[256];

File diff suppressed because it is too large Load diff

View file

@ -45,12 +45,6 @@
#include <time.h> #include <time.h>
// Maximum windows that can be queued for deferred destruction during a
// single dispatch pass. On overflow the destroy happens immediately,
// which risks the callback use-after-free this mechanism prevents -- the
// limit is far above what any real dispatch produces.
#define DVX_PENDING_DESTROY_MAX 32
// ============================================================ // ============================================================
// Application context // Application context
// ============================================================ // ============================================================
@ -114,10 +108,12 @@ typedef struct AppContextT {
// user callback (onClick, timer tick, BASIC event handler) would // user callback (onClick, timer tick, BASIC event handler) would
// free widget memory the dispatch code still touches after the // free widget memory the dispatch code still touches after the
// callback returns. While dispatchDepth > 0, dvxDestroyWindow // callback returns. While dispatchDepth > 0, dvxDestroyWindow
// hides the window and queues its id here; the queue is flushed in // hides the window and flags it destroyPending; the flags are
// dvxUpdate once the dispatch stack has fully unwound. // flushed in dvxUpdate once the dispatch stack has fully unwound.
// pendingDestroyCount is only a fast-path hint that a flush scan is
// needed -- the per-window destroyPending flag is the single source
// of truth for "queued".
int32_t dispatchDepth; int32_t dispatchDepth;
int32_t pendingDestroyIds[DVX_PENDING_DESTROY_MAX];
int32_t pendingDestroyCount; int32_t pendingDestroyCount;
void (*onCtrlEsc)(void *ctx); // system-wide Ctrl+Esc handler (e.g. task manager) void (*onCtrlEsc)(void *ctx); // system-wide Ctrl+Esc handler (e.g. task manager)
void *ctrlEscCtx; void *ctrlEscCtx;
@ -145,6 +141,8 @@ typedef struct AppContextT {
int32_t wheelDirection; // 1 = normal, -1 = reversed int32_t wheelDirection; // 1 = normal, -1 = reversed
int32_t wheelStep; // lines per wheel notch (1-10, default 3) int32_t wheelStep; // lines per wheel notch (1-10, default 3)
clock_t dblClickTicks; // double-click speed in clock() ticks clock_t dblClickTicks; // double-click speed in clock() ticks
int32_t accelThreshold; // last applied pointer-accel threshold
int32_t mickeyRatio; // last applied mickeys-to-pixels ratio
// Color scheme source RGB values (unpacked, for theme save/get) // Color scheme source RGB values (unpacked, for theme save/get)
uint8_t colorRgb[ColorCountE][3]; uint8_t colorRgb[ColorCountE][3];
// Available video modes (enumerated once at init) // Available video modes (enumerated once at init)
@ -245,8 +243,19 @@ WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t
WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, int32_t h, bool resizable); WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, int32_t h, bool resizable);
// Destroy a window, free all its resources, and dirty its former region. // Destroy a window, free all its resources, and dirty its former region.
// While a user callback is on the stack (dispatchDepth > 0) the teardown
// is deferred: the window is hidden and made inert, and its memory is
// freed once the dispatch stack unwinds.
void dvxDestroyWindow(AppContextT *ctx, WindowT *win); void dvxDestroyWindow(AppContextT *ctx, WindowT *win);
// Destroy a window SYNCHRONOUSLY, even mid-dispatch, clearing any pending
// deferred-destroy state first. For teardown that must complete before
// code or data becomes invalid (e.g. the shell destroying an app's windows
// before dlclose unmaps its DXE). Safe mid-dispatch for windows the
// current dispatch is not executing inside: wgtDestroy bumps sWidgetGen
// and every dispatch site re-checks it before touching cached widgets.
void dvxDestroyWindowNow(AppContextT *ctx, WindowT *win);
// Raise a window to the top of the z-order and give it focus. // Raise a window to the top of the z-order and give it focus.
void dvxRaiseWindow(AppContextT *ctx, WindowT *win); void dvxRaiseWindow(AppContextT *ctx, WindowT *win);
@ -272,6 +281,11 @@ void dvxResizeWindow(AppContextT *ctx, WindowT *win, int32_t newW, int32_t newH)
// the dirty area. // the dirty area.
void dvxInvalidateRect(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h); void dvxInvalidateRect(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h);
// Hide the on-screen tooltip if it borrows `text` (NULL = hide any).
// ctx->tooltipText borrows the widget's tooltip string while displayed,
// so any code that frees or replaces such a string must call this first.
void dvxInvalidateTooltip(AppContextT *ctx, const char *text);
// Mark the entire window content area as dirty. // Mark the entire window content area as dirty.
void dvxInvalidateWindow(AppContextT *ctx, WindowT *win); void dvxInvalidateWindow(AppContextT *ctx, WindowT *win);

View file

@ -233,6 +233,7 @@ static void iibOnOk(WidgetT *w);
static void onButtonClick(WidgetT *w); static void onButtonClick(WidgetT *w);
static void onMsgBoxClose(WindowT *win); static void onMsgBoxClose(WindowT *win);
static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea); static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea);
static void restoreModal(AppContextT *ctx, WindowT *prevModal);
static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t maxW, uint32_t fg, uint32_t bg); static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t maxW, uint32_t fg, uint32_t bg);
static void wordWrapDrawLine(const char *line, int32_t lineLen, int32_t lineIndex, void *userData); static void wordWrapDrawLine(const char *line, int32_t lineLen, int32_t lineIndex, void *userData);
static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW); static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW);
@ -261,12 +262,15 @@ static void cbOnDblClick(WidgetT *w) {
static void cbOnOk(WidgetT *w) { static void cbOnOk(WidgetT *w) {
(void)w; (void)w;
// Accept only when the selection was actually written -- if the
// listbox failed to create, returning true would hand the caller
// an unwritten *outIdx.
if (sChoiceBox.listBox && sChoiceBox.outIdx) { if (sChoiceBox.listBox && sChoiceBox.outIdx) {
*sChoiceBox.outIdx = wgtListBoxGetSelected(sChoiceBox.listBox); *sChoiceBox.outIdx = wgtListBoxGetSelected(sChoiceBox.listBox);
sChoiceBox.accepted = true;
} }
sChoiceBox.accepted = true; sChoiceBox.done = true;
sChoiceBox.done = true;
} }
@ -443,7 +447,7 @@ bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, co
dvxUpdate(ctx); dvxUpdate(ctx);
} }
ctx->modalWindow = prevModal; restoreModal(ctx, prevModal);
dvxDestroyWindow(ctx, win); dvxDestroyWindow(ctx, win);
sChoiceBox.listBox = NULL; sChoiceBox.listBox = NULL;
@ -599,7 +603,7 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
dvxUpdate(ctx); dvxUpdate(ctx);
} }
ctx->modalWindow = prevModal; restoreModal(ctx, prevModal);
// Build result path // Build result path
bool result = false; bool result = false;
@ -725,7 +729,7 @@ bool dvxInputBox(AppContextT *ctx, const char *title, const char *prompt, const
dvxUpdate(ctx); dvxUpdate(ctx);
} }
ctx->modalWindow = prevModal; restoreModal(ctx, prevModal);
dvxDestroyWindow(ctx, win); dvxDestroyWindow(ctx, win);
sInputBox.input = NULL; sInputBox.input = NULL;
@ -817,7 +821,7 @@ bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int
dvxUpdate(ctx); dvxUpdate(ctx);
} }
ctx->modalWindow = prevModal; restoreModal(ctx, prevModal);
dvxDestroyWindow(ctx, win); dvxDestroyWindow(ctx, win);
sIntBox.spinner = NULL; sIntBox.spinner = NULL;
@ -1021,7 +1025,7 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message,
dvxUpdate(ctx); dvxUpdate(ctx);
} }
ctx->modalWindow = prevModal; restoreModal(ctx, prevModal);
dvxDestroyWindow(ctx, win); dvxDestroyWindow(ctx, win);
return sMsgBox.result; return sMsgBox.result;
@ -1300,6 +1304,14 @@ static void fdLoadDir(void) {
sFd.entryNames[idx] = strdup(ent->d_name); sFd.entryNames[idx] = strdup(ent->d_name);
} }
// Don't commit a NULL name -- entryCount is not yet incremented,
// so on break the arrays stay consistent and the sort comparator
// and listbox never see a NULL entry name.
if (!sFd.entryNames[idx]) {
dvxLog("Dialog: failed to strdup entry name");
break;
}
sFd.entryCount++; sFd.entryCount++;
} }
@ -1669,13 +1681,16 @@ static void ibOnClose(WindowT *win) {
static void ibOnOk(WidgetT *w) { static void ibOnOk(WidgetT *w) {
(void)w; (void)w;
// Accept only when the text was actually written -- if the input
// failed to create, returning true would hand the caller an
// unwritten outBuf.
if (sInputBox.input && sInputBox.outBuf) { if (sInputBox.input && sInputBox.outBuf) {
const char *text = wgtGetText(sInputBox.input); const char *text = wgtGetText(sInputBox.input);
snprintf(sInputBox.outBuf, sInputBox.outBufSize, "%s", text ? text : ""); snprintf(sInputBox.outBuf, sInputBox.outBufSize, "%s", text ? text : "");
sInputBox.accepted = true;
} }
sInputBox.accepted = true; sInputBox.done = true;
sInputBox.done = true;
} }
@ -1696,12 +1711,15 @@ static void iibOnClose(WindowT *win) {
static void iibOnOk(WidgetT *w) { static void iibOnOk(WidgetT *w) {
(void)w; (void)w;
// Accept only when the value was actually written -- if the spinner
// failed to create, returning true would hand the caller an
// unwritten *outVal.
if (sIntBox.spinner && sIntBox.outVal) { if (sIntBox.spinner && sIntBox.outVal) {
*sIntBox.outVal = wgtSpinnerGetValue(sIntBox.spinner); *sIntBox.outVal = wgtSpinnerGetValue(sIntBox.spinner);
sIntBox.accepted = true;
} }
sIntBox.accepted = true; sIntBox.done = true;
sIntBox.done = true;
} }
@ -1782,6 +1800,33 @@ static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) {
} }
// Restore the modal window saved before a nested dialog loop. The loop
// may have destroyed, hidden, or deferred-destroyed the saved window
// (its form was unloaded, its app was force-killed), so validate it by
// scanning the live window stack -- pointer comparison only, since
// prevModal may point at freed memory -- before dereferencing it. A
// stale save restores to no-modal instead of re-arming the input gate
// on a window hit-testing can never reach.
static void restoreModal(AppContextT *ctx, WindowT *prevModal) {
ctx->modalWindow = NULL;
if (!prevModal) {
return;
}
for (int32_t i = 0; i < ctx->stack.count; i++) {
if (ctx->stack.windows[i] == prevModal) {
if (prevModal->visible && !prevModal->destroyPending) {
ctx->modalWindow = prevModal;
}
return;
}
}
}
// Draw word-wrapped text. Delegates the line-breaking to wordWrapIterate // Draw word-wrapped text. Delegates the line-breaking to wordWrapIterate
// and draws each emitted line via the wordWrapDrawLine callback. // and draws each emitted line via the wordWrapDrawLine callback.

View file

@ -112,39 +112,6 @@ static const uint8_t sGlyphBit[8] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02,
static const uint16_t sMaskBit[16] = {0x8000, 0x4000, 0x2000, 0x1000, 0x0800, 0x0400, 0x0200, 0x0100, 0x0080, 0x0040, 0x0020, 0x0010, 0x0008, 0x0004, 0x0002, 0x0001}; static const uint16_t sMaskBit[16] = {0x8000, 0x4000, 0x2000, 0x1000, 0x0800, 0x0400, 0x0200, 0x0100, 0x0080, 0x0040, 0x0020, 0x0010, 0x0008, 0x0004, 0x0002, 0x0001};
// ============================================================
// accelParse
// ============================================================
//
// Scans a menu/button label for the & accelerator marker and returns
// the character after it (lowercased). Follows the Windows/Motif
// convention: "&File" means Alt+F activates it, "&&" is a literal &.
// Returns 0 if no accelerator is found. The result is always
// lowercased so the WM can do a single case-insensitive compare
// against incoming Alt+key events.
char accelParse(const char *text) {
if (!text) {
return 0;
}
char ch;
AccelTokenE token;
while ((token = accelNextToken(&text, &ch)) != AccelTokenEndE) {
if (token == AccelTokenAccelE) {
if (ch >= 'A' && ch <= 'Z') {
return (char)(ch + ('a' - 'A'));
}
return ch;
}
}
return 0;
}
// ============================================================ // ============================================================
// accelNextToken // accelNextToken
// ============================================================ // ============================================================
@ -195,6 +162,39 @@ static AccelTokenE accelNextToken(const char **text, char *outCh) {
} }
// ============================================================
// accelParse
// ============================================================
//
// Scans a menu/button label for the & accelerator marker and returns
// the character after it (lowercased). Follows the Windows/Motif
// convention: "&File" means Alt+F activates it, "&&" is a literal &.
// Returns 0 if no accelerator is found. The result is always
// lowercased so the WM can do a single case-insensitive compare
// against incoming Alt+key events.
char accelParse(const char *text) {
if (!text) {
return 0;
}
char ch;
AccelTokenE token;
while ((token = accelNextToken(&text, &ch)) != AccelTokenEndE) {
if (token == AccelTokenAccelE) {
if (ch >= 'A' && ch <= 'Z') {
return (char)(ch + ('a' - 'A'));
}
return ch;
}
}
return 0;
}
// ============================================================ // ============================================================
// clipRect // clipRect
// ============================================================ // ============================================================
@ -256,8 +256,9 @@ static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *
// call already handles clipping internally, so the bevels clip // call already handles clipping internally, so the bevels clip
// correctly even when a window is partially off-screen. // correctly even when a window is partially off-screen.
// //
// face==0 means "don't fill the interior", which is used for frame-only // face==BEVEL_NO_FILL means "don't fill the interior", which is used for
// bevels where the content area is painted separately by a callback. // frame-only bevels where the content area is painted separately by a
// callback. A face of 0 is a valid packed color (black) and fills normally.
void drawBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const BevelStyleT *style) { void drawBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const BevelStyleT *style) {
int32_t bw = style->width; int32_t bw = style->width;

View file

@ -51,7 +51,17 @@
// 256 glyphs, 16 bytes per glyph, MSB = leftmost pixel // 256 glyphs, 16 bytes per glyph, MSB = leftmost pixel
// ============================================================ // ============================================================
static const uint8_t font8x16[256 * FONT_CHAR_HEIGHT] = { // The glyph table and font descriptor have external linkage with a single
// definition, compiled into the one translation unit that defines
// DVX_FONT_IMPL before including this header (dvxWm.c). Every other includer
// sees only these extern declarations, so the 4 KB glyph table is emitted
// once for the whole library instead of once per translation unit.
extern const uint8_t font8x16[256 * FONT_CHAR_HEIGHT];
extern const BitmapFontT dvxFont8x16;
#ifdef DVX_FONT_IMPL
const uint8_t font8x16[256 * FONT_CHAR_HEIGHT] = {
// Char 0: NULL (blank) // Char 0: NULL (blank)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Char 1: Smiley (outline) // Char 1: Smiley (outline)
@ -572,7 +582,7 @@ static const uint8_t font8x16[256 * FONT_CHAR_HEIGHT] = {
// Font descriptor struct // Font descriptor struct
// ============================================================ // ============================================================
static const BitmapFontT dvxFont8x16 = { const BitmapFontT dvxFont8x16 = {
.charWidth = FONT_CHAR_WIDTH, .charWidth = FONT_CHAR_WIDTH,
.charHeight = FONT_CHAR_HEIGHT, .charHeight = FONT_CHAR_HEIGHT,
.firstChar = 0, .firstChar = 0,
@ -580,4 +590,6 @@ static const BitmapFontT dvxFont8x16 = {
.glyphData = font8x16, .glyphData = font8x16,
}; };
#endif // DVX_FONT_IMPL
#endif // DVX_FONT_H #endif // DVX_FONT_H

View file

@ -214,10 +214,10 @@ int32_t dvxResRemove(const char *path, const char *name) {
return -1; return -1;
} }
DvxResDirEntryT *entries = NULL; DvxResDirEntryT *entries = NULL;
uint32_t count = 0; uint32_t count = 0;
uint8_t **data = NULL; uint8_t **data = NULL;
int readResult = dvxResReadExisting(path, dxeSize, &entries, &count, &data); int32_t readResult = dvxResReadExisting(path, dxeSize, &entries, &count, &data);
// Distinguish a read/OOM error from a file with no resources to remove. // Distinguish a read/OOM error from a file with no resources to remove.
if (readResult < 0) { if (readResult < 0) {

View file

@ -325,48 +325,56 @@ static inline bool wclsHas(const WidgetT *w, int32_t methodId) {
return w->wclass && w->wclass->handlers[methodId] != NULL; return w->wclass && w->wclass->handlers[methodId] != NULL;
} }
static inline void wclsPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { static inline void wclsPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
typedef void (*FnT)(WidgetT *, DisplayT *, const BlitOpsT *, const BitmapFontT *, const ColorSchemeT *); typedef void (*FnT)(WidgetT *, DisplayT *, const BlitOpsT *, const BitmapFontT *, const ColorSchemeT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_PAINT] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_PAINT] : NULL;
if (fn) { fn(w, d, ops, font, colors); } if (fn) { fn(w, d, ops, font, colors); }
} }
static inline void wclsPaintOverlay(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { static inline void wclsPaintOverlay(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
typedef void (*FnT)(WidgetT *, DisplayT *, const BlitOpsT *, const BitmapFontT *, const ColorSchemeT *); typedef void (*FnT)(WidgetT *, DisplayT *, const BlitOpsT *, const BitmapFontT *, const ColorSchemeT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_PAINT_OVERLAY] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_PAINT_OVERLAY] : NULL;
if (fn) { fn(w, d, ops, font, colors); } if (fn) { fn(w, d, ops, font, colors); }
} }
static inline void wclsCalcMinSize(WidgetT *w, const BitmapFontT *font) { static inline void wclsCalcMinSize(WidgetT *w, const BitmapFontT *font) {
typedef void (*FnT)(WidgetT *, const BitmapFontT *); typedef void (*FnT)(WidgetT *, const BitmapFontT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CALC_MIN_SIZE] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CALC_MIN_SIZE] : NULL;
if (fn) { fn(w, font); } if (fn) { fn(w, font); }
} }
static inline void wclsLayout(WidgetT *w, const BitmapFontT *font) { static inline void wclsLayout(WidgetT *w, const BitmapFontT *font) {
typedef void (*FnT)(WidgetT *, const BitmapFontT *); typedef void (*FnT)(WidgetT *, const BitmapFontT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_LAYOUT] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_LAYOUT] : NULL;
if (fn) { fn(w, font); } if (fn) { fn(w, font); }
} }
static inline void wclsGetLayoutMetrics(const WidgetT *w, const BitmapFontT *font, int32_t *pad, int32_t *gap, int32_t *extraTop, int32_t *borderW) { static inline void wclsGetLayoutMetrics(const WidgetT *w, const BitmapFontT *font, int32_t *pad, int32_t *gap, int32_t *extraTop, int32_t *borderW) {
typedef void (*FnT)(const WidgetT *, const BitmapFontT *, int32_t *, int32_t *, int32_t *, int32_t *); typedef void (*FnT)(const WidgetT *, const BitmapFontT *, int32_t *, int32_t *, int32_t *, int32_t *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_LAYOUT_METRICS] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_LAYOUT_METRICS] : NULL;
if (fn) { fn(w, font, pad, gap, extraTop, borderW); } if (fn) { fn(w, font, pad, gap, extraTop, borderW); }
} }
static inline void wclsOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { static inline void wclsOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
typedef void (*FnT)(WidgetT *, WidgetT *, int32_t, int32_t); typedef void (*FnT)(WidgetT *, WidgetT *, int32_t, int32_t);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_MOUSE] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_MOUSE] : NULL;
if (fn) { fn(w, root, vx, vy); } if (fn) { fn(w, root, vx, vy); }
} }
static inline void wclsOnKey(WidgetT *w, int32_t key, int32_t mod) { static inline void wclsOnKey(WidgetT *w, int32_t key, int32_t mod) {
typedef void (*FnT)(WidgetT *, int32_t, int32_t); typedef void (*FnT)(WidgetT *, int32_t, int32_t);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_KEY] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_KEY] : NULL;
if (fn) { fn(w, key, mod); } if (fn) { fn(w, key, mod); }
} }
// Dispatched on the widget LOSING keyboard focus, before the app-level // Dispatched on the widget LOSING keyboard focus, before the app-level
// onBlur callback. Editing widgets (e.g. the spinner) commit/clamp their // onBlur callback. Editing widgets (e.g. the spinner) commit/clamp their
// in-progress text here. Must be called from every focus-loss transition // in-progress text here. Must be called from every focus-loss transition
@ -377,12 +385,14 @@ static inline void wclsOnBlur(WidgetT *w) {
if (fn) { fn(w); } if (fn) { fn(w); }
} }
static inline void wclsOnAccelActivate(WidgetT *w, WidgetT *root) { static inline void wclsOnAccelActivate(WidgetT *w, WidgetT *root) {
typedef void (*FnT)(WidgetT *, WidgetT *); typedef void (*FnT)(WidgetT *, WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_ACCEL_ACTIVATE] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_ACCEL_ACTIVATE] : NULL;
if (fn) { fn(w, root); } if (fn) { fn(w, root); }
} }
// Default destroy behavior: if the widget's class does not supply an // Default destroy behavior: if the widget's class does not supply an
// explicit WGT_METHOD_DESTROY handler, free w->data automatically (and // explicit WGT_METHOD_DESTROY handler, free w->data automatically (and
// the text-at-offset-0 first, if WCLASS_HAS_TEXT is set). This removes // the text-at-offset-0 first, if WCLASS_HAS_TEXT is set). This removes
@ -405,78 +415,91 @@ static inline void wclsDestroy(WidgetT *w) {
} }
} }
static inline void wclsOnChildChanged(WidgetT *parent, WidgetT *child) { static inline void wclsOnChildChanged(WidgetT *parent, WidgetT *child) {
typedef void (*FnT)(WidgetT *, WidgetT *); typedef void (*FnT)(WidgetT *, WidgetT *);
FnT fn = parent->wclass ? (FnT)parent->wclass->handlers[WGT_METHOD_ON_CHILD_CHANGED] : NULL; FnT fn = parent->wclass ? (FnT)parent->wclass->handlers[WGT_METHOD_ON_CHILD_CHANGED] : NULL;
if (fn) { fn(parent, child); } if (fn) { fn(parent, child); }
} }
static inline const char *wclsGetText(const WidgetT *w) { static inline const char *wclsGetText(const WidgetT *w) {
typedef const char *(*FnT)(const WidgetT *); typedef const char *(*FnT)(const WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_TEXT] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_TEXT] : NULL;
return fn ? fn(w) : ""; return fn ? fn(w) : "";
} }
static inline void wclsSetText(WidgetT *w, const char *text) { static inline void wclsSetText(WidgetT *w, const char *text) {
typedef void (*FnT)(WidgetT *, const char *); typedef void (*FnT)(WidgetT *, const char *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_SET_TEXT] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_SET_TEXT] : NULL;
if (fn) { fn(w, text); } if (fn) { fn(w, text); }
} }
static inline bool wclsClearSelection(WidgetT *w) { static inline bool wclsClearSelection(WidgetT *w) {
typedef bool (*FnT)(WidgetT *); typedef bool (*FnT)(WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CLEAR_SELECTION] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CLEAR_SELECTION] : NULL;
return fn ? fn(w) : false; return fn ? fn(w) : false;
} }
static inline void wclsClosePopup(WidgetT *w) { static inline void wclsClosePopup(WidgetT *w) {
typedef void (*FnT)(WidgetT *); typedef void (*FnT)(WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CLOSE_POPUP] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CLOSE_POPUP] : NULL;
if (fn) { fn(w); } if (fn) { fn(w); }
} }
static inline void wclsGetPopupRect(const WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) { static inline void wclsGetPopupRect(const WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) {
typedef void (*FnT)(const WidgetT *, const BitmapFontT *, int32_t, int32_t *, int32_t *, int32_t *, int32_t *); typedef void (*FnT)(const WidgetT *, const BitmapFontT *, int32_t, int32_t *, int32_t *, int32_t *, int32_t *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_POPUP_RECT] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_POPUP_RECT] : NULL;
if (fn) { fn(w, font, contentH, popX, popY, popW, popH); } if (fn) { fn(w, font, contentH, popX, popY, popW, popH); }
} }
static inline void wclsOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { static inline void wclsOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
typedef void (*FnT)(WidgetT *, WidgetT *, int32_t, int32_t); typedef void (*FnT)(WidgetT *, WidgetT *, int32_t, int32_t);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_DRAG_UPDATE] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_DRAG_UPDATE] : NULL;
if (fn) { fn(w, root, x, y); } if (fn) { fn(w, root, x, y); }
} }
static inline void wclsOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { static inline void wclsOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
typedef void (*FnT)(WidgetT *, WidgetT *, int32_t, int32_t); typedef void (*FnT)(WidgetT *, WidgetT *, int32_t, int32_t);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_DRAG_END] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_DRAG_END] : NULL;
if (fn) { fn(w, root, x, y); } if (fn) { fn(w, root, x, y); }
} }
static inline int32_t wclsGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy) { static inline int32_t wclsGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy) {
typedef int32_t (*FnT)(const WidgetT *, int32_t, int32_t); typedef int32_t (*FnT)(const WidgetT *, int32_t, int32_t);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_CURSOR_SHAPE] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_CURSOR_SHAPE] : NULL;
return fn ? fn(w, vx, vy) : 0; return fn ? fn(w, vx, vy) : 0;
} }
static inline void wclsPoll(WidgetT *w, WindowT *win) { static inline void wclsPoll(WidgetT *w, WindowT *win) {
typedef void (*FnT)(WidgetT *, WindowT *); typedef void (*FnT)(WidgetT *, WindowT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_POLL] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_POLL] : NULL;
if (fn) { fn(w, win); } if (fn) { fn(w, win); }
} }
static inline int32_t wclsQuickRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { static inline int32_t wclsQuickRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
typedef int32_t (*FnT)(WidgetT *, int32_t *, int32_t *); typedef int32_t (*FnT)(WidgetT *, int32_t *, int32_t *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_QUICK_REPAINT] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_QUICK_REPAINT] : NULL;
return fn ? fn(w, outY, outH) : 0; return fn ? fn(w, outY, outH) : 0;
} }
static inline void wclsScrollChildIntoView(WidgetT *parent, const WidgetT *child) { static inline void wclsScrollChildIntoView(WidgetT *parent, const WidgetT *child) {
typedef void (*FnT)(WidgetT *, const WidgetT *); typedef void (*FnT)(WidgetT *, const WidgetT *);
FnT fn = parent->wclass ? (FnT)parent->wclass->handlers[WGT_METHOD_SCROLL_CHILD_INTO_VIEW] : NULL; FnT fn = parent->wclass ? (FnT)parent->wclass->handlers[WGT_METHOD_SCROLL_CHILD_INTO_VIEW] : NULL;
if (fn) { fn(parent, child); } if (fn) { fn(parent, child); }
} }
// ============================================================ // ============================================================
// Window integration // Window integration
// ============================================================ // ============================================================
@ -723,6 +746,11 @@ typedef struct {
void wgtRegisterIface(const char *name, const WgtIfaceT *iface); void wgtRegisterIface(const char *name, const WgtIfaceT *iface);
const WgtIfaceT *wgtGetIface(const char *name); const WgtIfaceT *wgtGetIface(const char *name);
// Case-insensitive lookup of a property descriptor on iface by name.
// Single source of truth for the runtime and IDE property-override rule.
// Returns NULL if iface is NULL or no property matches.
const WgtPropDescT *wgtIfaceFindProp(const WgtIfaceT *iface, const char *propName);
// Find a widget type name by its VB-style name (e.g. "CommandButton" -> "button"). // Find a widget type name by its VB-style name (e.g. "CommandButton" -> "button").
// Returns NULL if no widget has that basName. Case-insensitive. // Returns NULL if no widget has that basName. Case-insensitive.
const char *wgtFindByBasName(const char *basName); const char *wgtFindByBasName(const char *basName);

View file

@ -124,7 +124,6 @@ extern WidgetT **sPollWidgets; // stb_ds dynamic array
// widget, the focused widget) may be dangling and must not be touched. // widget, the focused widget) may be dangling and must not be touched.
extern uint32_t sWidgetGen; extern uint32_t sWidgetGen;
extern void (*sCursorBlinkFn)(void); extern void (*sCursorBlinkFn)(void);
extern void (*sWidgetDestroyFn)(WidgetT *w);
// ============================================================ // ============================================================
// Core widget functions (widgetCore.c) // Core widget functions (widgetCore.c)
@ -139,6 +138,18 @@ void widgetDestroyChildren(WidgetT *w);
// from the poll list. Call for every destroyed node. // from the poll list. Call for every destroyed node.
void widgetClearReferences(WidgetT *w); void widgetClearReferences(WidgetT *w);
// Clears every global interaction pointer whose widget lives on win.
// Called when a window's destruction is deferred (destroyPending), so no
// drag/popup/key-press dispatch reaches its still-allocated widget tree.
void widgetDetachWindowReferences(WindowT *win);
// Widget-destroy notification. Subscribers registered here are fired by
// widgetClearReferences for every destroyed widget so a module holding a
// static WidgetT pointer can null it before the widget is freed. Register
// is idempotent; unregister is a no-op if the callback is absent.
void widgetRegisterDestroyFn(void (*fn)(WidgetT *w));
void widgetUnregisterDestroyFn(void (*fn)(WidgetT *w));
// Allocation // Allocation
WidgetT *widgetAlloc(WidgetT *parent, int32_t type); WidgetT *widgetAlloc(WidgetT *parent, int32_t type);
@ -176,6 +187,7 @@ WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y);
// Scrollbar helpers // Scrollbar helpers
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize); void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize);
int32_t widgetScrollbarThumbDragScroll(int32_t trackLen, int32_t total, int32_t visible, int32_t relMouse, int32_t maxScroll);
// ============================================================ // ============================================================
// Scrollbar drawing and hit testing (widgetScrollbar.c) // Scrollbar drawing and hit testing (widgetScrollbar.c)

View file

@ -54,6 +54,8 @@
#include "dvxWm.h" #include "dvxWm.h"
#include "dvxVideo.h" #include "dvxVideo.h"
#include "dvxDraw.h" #include "dvxDraw.h"
// This TU owns the single definition of the shared 8x16 font table/descriptor.
#define DVX_FONT_IMPL
#include "dvxFont.h" #include "dvxFont.h"
#include "dvxComp.h" #include "dvxComp.h"
#include "dvxWgt.h" #include "dvxWgt.h"
@ -127,9 +129,11 @@ static MenuItemT *menuNewItem(MenuT *menu, const char *label);
static ScrollbarT *scrollbarAdd(WindowT *win, ScrollbarT **slot, ScrollbarOrientE orient, const char *orientName, int32_t min, int32_t max, int32_t pageSize); static ScrollbarT *scrollbarAdd(WindowT *win, ScrollbarT **slot, ScrollbarOrientE orient, const char *orientName, int32_t min, int32_t max, int32_t pageSize);
static void scrollbarCommitValue(WindowT *win, ScrollbarT *sb, DirtyListT *dl, int32_t sbScreenX, int32_t sbScreenY, int32_t oldValue); static void scrollbarCommitValue(WindowT *win, ScrollbarT *sb, DirtyListT *dl, int32_t sbScreenX, int32_t sbScreenY, int32_t oldValue);
static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32_t *thumbSize); static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32_t *thumbSize);
static int32_t wmAdjustIndexForRaise(int32_t index, int32_t raisedSlot, int32_t newTop);
static int32_t wmAdjustIndexForRemoval(int32_t index, int32_t removedSlot); static int32_t wmAdjustIndexForRemoval(int32_t index, int32_t removedSlot);
static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY); static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY);
static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu); static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu);
static MenuItemT *wmMenuFindItemRecursive(MenuT *menu, int32_t id, MenuT **outMenu);
// Lays out menu bar label positions left-to-right. Each label gets // Lays out menu bar label positions left-to-right. Each label gets
@ -289,18 +293,24 @@ static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fon
// Tighten clip to the menu bar bounds INTERSECTED with the incoming dirty // Tighten clip to the menu bar bounds INTERSECTED with the incoming dirty
// rect (already set by wmDrawChrome) so labels never paint outside either // rect (already set by wmDrawChrome) so labels never paint outside either
// region. setClipRect only clamps to screen bounds, it does not intersect // region. setClipRect only clamps to screen bounds, it does not intersect
// with the prior clip, so we must compute the intersection ourselves. // with the prior clip, so we intersect against it with the shared
// rectIntersect primitive.
int32_t savedClipX = d->clipX; int32_t savedClipX = d->clipX;
int32_t savedClipY = d->clipY; int32_t savedClipY = d->clipY;
int32_t savedClipW = d->clipW; int32_t savedClipW = d->clipW;
int32_t savedClipH = d->clipH; int32_t savedClipH = d->clipH;
int32_t barLeft = win->x + CHROME_BORDER_WIDTH; RectT barRect = { win->x + CHROME_BORDER_WIDTH, barY, win->w - CHROME_BORDER_WIDTH * 2, barH };
int32_t barRight = barLeft + (win->w - CHROME_BORDER_WIDTH * 2); RectT savedClip = { savedClipX, savedClipY, savedClipW, savedClipH };
int32_t clipLeft = DVX_MAX(barLeft, savedClipX); RectT menuClip;
int32_t clipTop = DVX_MAX(barY, savedClipY);
int32_t clipRight = DVX_MIN(barRight, savedClipX + savedClipW); if (rectIntersect(&barRect, &savedClip, &menuClip)) {
int32_t clipBottom = DVX_MIN(barY + barH, savedClipY + savedClipH); setClipRect(d, menuClip.x, menuClip.y, menuClip.w, menuClip.h);
setClipRect(d, clipLeft, clipTop, clipRight - clipLeft, clipBottom - clipTop); } else {
// Menu bar lies fully outside the dirty rect: an empty clip rejects
// every label span while still letting the separator line paint once
// the clip is restored below.
setClipRect(d, savedClipX, savedClipY, 0, 0);
}
for (int32_t i = 0; i < win->menuBar->menuCount; i++) { for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
MenuT *menu = win->menuBar->menus[i]; MenuT *menu = win->menuBar->menus[i];
@ -462,27 +472,6 @@ static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW
} }
// Renders a window-level scrollbar by delegating to the shared draw-layer
// painter. Computes the screen position from the window origin plus the
// scrollbar's window-relative x/y, derives the thumb geometry from the
// scroll range, and hands everything to drawScrollbar().
//
// winX/winY are the window's screen position; sb->x/y are relative to the
// window origin. This split lets scrollbar positions survive window drags
// without recalculation -- only winX/winY change.
static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY) {
int32_t x = winX + sb->x;
int32_t y = winY + sb->y;
int32_t thumbPos;
int32_t thumbSize;
scrollbarThumbInfo(sb, &thumbPos, &thumbSize);
drawScrollbar(d, ops, colors, sb->orient, x, y, sb->length, SCROLLBAR_WIDTH, thumbPos, thumbSize);
}
// Renders the title bar: background fill, close gadget (left), minimize // Renders the title bar: background fill, close gadget (left), minimize
// and maximize gadgets (right), and centered title text. // and maximize gadgets (right), and centered title text.
// //
@ -591,8 +580,9 @@ static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT
// Frees a single menu item's submenu subtree (if any) and clears the // Frees a single menu item's submenu subtree (if any) and clears the
// pointer, so removing or clearing an item that carries a submenu does not // pointer. Only called from menu-tree teardown (freeMenuRecursive); the
// leak the child MenuT and its descendants. // item-level mutation APIs (wmClearMenuItems/wmRemoveMenuItem) must NOT use
// it because an open popup may still reference the subtree.
static void freeMenuItemSubMenu(MenuItemT *item) { static void freeMenuItemSubMenu(MenuItemT *item) {
if (item->subMenu) { if (item->subMenu) {
@ -649,6 +639,38 @@ static bool menuGrowItems(MenuT *menu) {
} }
// Applies a checked state to a menu item, honoring radio-group semantics.
// A radio item being checked unchecks every sibling in its contiguous
// radio-group run (bounded by the first non-radio item on each side) and
// checks only the target; any other case sets the item's checked flag
// directly. Shared by wmMenuItemSetChecked, wmMenuItemSetCheckedInMenu, and
// the app-level click handler (clickMenuCheckRadio) so the group-scan rule
// has a single implementation.
void menuItemApplyChecked(MenuT *menu, int32_t idx, bool checked) {
MenuItemT *item = &menu->items[idx];
if (item->type == MenuItemRadioE && checked) {
int32_t groupStart = idx;
while (groupStart > 0 && menu->items[groupStart - 1].type == MenuItemRadioE) {
groupStart--;
}
int32_t groupEnd = idx;
while (groupEnd < menu->itemCount - 1 && menu->items[groupEnd + 1].type == MenuItemRadioE) {
groupEnd++;
}
for (int32_t g = groupStart; g <= groupEnd; g++) {
menu->items[g].checked = (g == idx);
}
} else {
item->checked = checked;
}
}
// Grows the item array, takes the next slot, and initializes the common // Grows the item array, takes the next slot, and initializes the common
// fields (zeroed, label copied + NUL-terminated, enabled, accelerator). // fields (zeroed, label copied + NUL-terminated, enabled, accelerator).
// Returns the new item, or NULL if the array could not grow. // Returns the new item, or NULL if the array could not grow.
@ -761,7 +783,31 @@ static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32
} }
// wmMenuFindItem -- find menu item by command ID across all menus // Renders a window-level scrollbar by delegating to the shared draw-layer
// painter. Computes the screen position from the window origin plus the
// scrollbar's window-relative x/y, derives the thumb geometry from the
// scroll range, and hands everything to drawScrollbar().
//
// winX/winY are the window's screen position; sb->x/y are relative to the
// window origin. This split lets scrollbar positions survive window drags
// without recalculation -- only winX/winY change.
static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY) {
int32_t x = winX + sb->x;
int32_t y = winY + sb->y;
int32_t thumbPos;
int32_t thumbSize;
scrollbarThumbInfo(sb, &thumbPos, &thumbSize);
drawScrollbar(d, ops, colors, sb->orient, x, y, sb->length, SCROLLBAR_WIDTH, thumbPos, thumbSize);
}
// wmMenuFindItem -- find a menu item by command ID anywhere in a menu bar,
// descending into nested submenus to any depth. When outMenu is non-NULL it
// receives the menu that directly contains the matched item so callers can
// locate radio-group siblings and compute the item index.
static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu) { static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu) {
if (!bar) { if (!bar) {
@ -769,29 +815,10 @@ static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu) {
} }
for (int32_t m = 0; m < bar->menuCount; m++) { for (int32_t m = 0; m < bar->menuCount; m++) {
MenuT *menu = bar->menus[m]; MenuItemT *found = wmMenuFindItemRecursive(bar->menus[m], id, outMenu);
for (int32_t i = 0; i < menu->itemCount; i++) { if (found) {
if (menu->items[i].id == id) { return found;
if (outMenu) {
*outMenu = menu;
}
return &menu->items[i];
}
// Search submenus recursively
if (menu->items[i].subMenu) {
for (int32_t j = 0; j < menu->items[i].subMenu->itemCount; j++) {
if (menu->items[i].subMenu->items[j].id == id) {
if (outMenu) {
*outMenu = menu->items[i].subMenu;
}
return &menu->items[i].subMenu->items[j];
}
}
}
} }
} }
@ -799,22 +826,28 @@ static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu) {
} }
// wmMenuFindItemInMenu -- recursively find an item by command ID within a // wmMenuFindItemRecursive -- find a menu item by command ID within a single
// single menu tree (the menu itself plus every nested submenu). Used for // menu tree (the menu plus every nested submenu, to any depth). When outMenu
// popup/context menus that are not attached to a menu bar, so the menu-bar // is non-NULL it receives the menu that directly contains the matched item.
// lookups (wmMenuFindItem) do not apply. // Shared id-search used by wmMenuFindItem (menu bars) and wmMenuFindItemInMenu
MenuItemT *wmMenuFindItemInMenu(MenuT *menu, int32_t id) { // (standalone popup/context menus).
static MenuItemT *wmMenuFindItemRecursive(MenuT *menu, int32_t id, MenuT **outMenu) {
if (!menu) { if (!menu) {
return NULL; return NULL;
} }
for (int32_t i = 0; i < menu->itemCount; i++) { for (int32_t i = 0; i < menu->itemCount; i++) {
if (menu->items[i].id == id) { if (menu->items[i].id == id) {
if (outMenu) {
*outMenu = menu;
}
return &menu->items[i]; return &menu->items[i];
} }
if (menu->items[i].subMenu) { if (menu->items[i].subMenu) {
MenuItemT *found = wmMenuFindItemInMenu(menu->items[i].subMenu, id); MenuItemT *found = wmMenuFindItemRecursive(menu->items[i].subMenu, id, outMenu);
if (found) { if (found) {
return found; return found;
@ -879,17 +912,25 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label) {
// the content area by CHROME_MENU_HEIGHT to make room for the bar. // the content area by CHROME_MENU_HEIGHT to make room for the bar.
MenuBarT *wmAddMenuBar(WindowT *win) { MenuBarT *wmAddMenuBar(WindowT *win) {
// Tear down any existing bar (recursive menu cleanup) so a second add does // Allocate the new bar BEFORE tearing down any existing one. The
// not leak the prior MenuBarT, its menus array, and all MenuT/submenus. // teardown grows the content rect without reallocating the content
wmDestroyMenuBar(win, NULL); // buffer (d is NULL), so failing the malloc afterwards would leave
// contentH grown past the buffer and the next full repaint would
// overrun the heap. Allocating first makes the failure path a no-op.
MenuBarT *bar = (MenuBarT *)malloc(sizeof(MenuBarT));
win->menuBar = (MenuBarT *)malloc(sizeof(MenuBarT)); if (!bar) {
if (!win->menuBar) {
dvxLog("WM: failed to allocate menu bar"); dvxLog("WM: failed to allocate menu bar");
return NULL; return NULL;
} }
// Tear down any existing bar (recursive menu cleanup) so a second add does
// not leak the prior MenuBarT, its menus array, and all MenuT/submenus.
// The transient content-rect grow is undone by wmUpdateContentRect below
// before any repaint can observe it.
wmDestroyMenuBar(win, NULL);
win->menuBar = bar;
memset(win->menuBar, 0, sizeof(MenuBarT)); memset(win->menuBar, 0, sizeof(MenuBarT));
win->menuBar->activeIdx = -1; win->menuBar->activeIdx = -1;
wmUpdateContentRect(win); wmUpdateContentRect(win);
@ -984,6 +1025,25 @@ ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page
} }
// Adjusts a stored window-stack index after the window at raisedSlot has been
// raised to newTop and the entries above it shifted down by one. Mirrors
// wmAdjustIndexForRemoval: the raised window itself moves to newTop, any index
// above the old slot decrements, and lower indices (and inactive -1 sentinels)
// are left unchanged.
static int32_t wmAdjustIndexForRaise(int32_t index, int32_t raisedSlot, int32_t newTop) {
if (index == raisedSlot) {
return newTop;
}
if (index > raisedSlot) {
return index - 1;
}
return index;
}
// Adjusts a stored window-stack index after the window at removedSlot has // Adjusts a stored window-stack index after the window at removedSlot has
// been removed and higher entries shifted down. Returns -1 when the stored // been removed and higher entries shifted down. Returns -1 when the stored
// index was the removed window itself -- the caller's interaction (drag, // index was the removed window itself -- the caller's interaction (drag,
@ -1006,10 +1066,14 @@ static int32_t wmAdjustIndexForRemoval(int32_t index, int32_t removedSlot) {
void wmClearMenuItems(MenuT *menu) { void wmClearMenuItems(MenuT *menu) {
if (menu) { if (menu) {
for (int32_t i = 0; i < menu->itemCount; i++) { // Detach only -- do NOT free item->subMenu trees here. The app-layer
freeMenuItemSubMenu(&menu->items[i]); // popup system holds direct MenuT pointers into an open popup chain
} // (popup.menu / parentStack), and this MenuT*-only API cannot see or
// reset that state, so freeing a submenu the popup still references
// would be a use-after-free on the next composite or click. Detached
// trees are deliberately orphaned instead (bounded leak); stale
// subMenu pointers past itemCount are harmless because menuNewItem
// zeroes reused slots.
menu->itemCount = 0; menu->itemCount = 0;
} }
} }
@ -1069,11 +1133,11 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int
memset(win, 0, sizeof(*win)); memset(win, 0, sizeof(*win));
static int32_t nextId = 1; static int32_t nextId = 1;
win->id = nextId++; win->id = nextId++;
win->x = x; win->x = x;
win->y = y; win->y = y;
win->w = w; win->w = w;
win->h = h; win->h = h;
win->visible = true; win->visible = true;
win->focused = false; win->focused = false;
win->minimized = false; win->minimized = false;
@ -1705,6 +1769,15 @@ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT
} }
// wmMenuFindItemInMenu -- recursively find an item by command ID within a
// single menu tree (the menu itself plus every nested submenu). Used for
// popup/context menus that are not attached to a menu bar, so the menu-bar
// lookups (wmMenuFindItem) do not apply.
MenuItemT *wmMenuFindItemInMenu(MenuT *menu, int32_t id) {
return wmMenuFindItemRecursive(menu, id, NULL);
}
bool wmMenuItemIsChecked(MenuBarT *bar, int32_t id) { bool wmMenuItemIsChecked(MenuBarT *bar, int32_t id) {
MenuItemT *item = wmMenuFindItem(bar, id, NULL); MenuItemT *item = wmMenuFindItem(bar, id, NULL);
return item ? item->checked : false; return item ? item->checked : false;
@ -1719,27 +1792,7 @@ void wmMenuItemSetChecked(MenuBarT *bar, int32_t id, bool checked) {
return; return;
} }
if (item->type == MenuItemRadioE && checked && menu) { menuItemApplyChecked(menu, (int32_t)(item - menu->items), checked);
// Find the radio group and uncheck all others
int32_t idx = (int32_t)(item - menu->items);
int32_t groupStart = idx;
while (groupStart > 0 && menu->items[groupStart - 1].type == MenuItemRadioE) {
groupStart--;
}
int32_t groupEnd = idx;
while (groupEnd < menu->itemCount - 1 && menu->items[groupEnd + 1].type == MenuItemRadioE) {
groupEnd++;
}
for (int32_t i = groupStart; i <= groupEnd; i++) {
menu->items[i].checked = (i == idx);
}
} else {
item->checked = checked;
}
} }
@ -1753,28 +1806,7 @@ void wmMenuItemSetCheckedInMenu(MenuT *menu, int32_t id, bool checked) {
for (int32_t i = 0; i < menu->itemCount; i++) { for (int32_t i = 0; i < menu->itemCount; i++) {
if (menu->items[i].id == id) { if (menu->items[i].id == id) {
MenuItemT *item = &menu->items[i]; menuItemApplyChecked(menu, i, checked);
if (item->type == MenuItemRadioE && checked) {
int32_t groupStart = i;
while (groupStart > 0 && menu->items[groupStart - 1].type == MenuItemRadioE) {
groupStart--;
}
int32_t groupEnd = i;
while (groupEnd < menu->itemCount - 1 && menu->items[groupEnd + 1].type == MenuItemRadioE) {
groupEnd++;
}
for (int32_t g = groupStart; g <= groupEnd; g++) {
menu->items[g].checked = (g == i);
}
} else {
item->checked = checked;
}
return; return;
} }
@ -1937,7 +1969,8 @@ void wmMinimizedIconRect(const WindowStackT *stack, const DisplayT *d, int32_t *
// + menu bar (if present) + scrollbar space (if present). // + menu bar (if present) + scrollbar space (if present).
// //
// This function is called on every resize move event, so it must be cheap. // This function is called on every resize move event, so it must be cheap.
// No allocations, no string operations -- just arithmetic on cached values. // No allocations; the only string work is measuring the menu-bar label
// widths (when a menu bar is present) to size the minimum width.
void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) { void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) {
int32_t gadgetS = CHROME_TITLE_HEIGHT - GADGET_INSET * 2; int32_t gadgetS = CHROME_TITLE_HEIGHT - GADGET_INSET * 2;
@ -2071,33 +2104,15 @@ void wmRaiseWindow(WindowStackT *stack, DirtyListT *dl, int32_t idx) {
dirtyListAdd(dl, win->x, win->y, win->w, win->h); dirtyListAdd(dl, win->x, win->y, win->w, win->h);
if (stack->focusedIdx == idx) { // focusedIdx / dragWindow / resizeWindow / scrollWindow are stack-index
stack->focusedIdx = stack->count - 1; // references, so they must shift with the raise (wmDestroyWindow adjusts
} else if (stack->focusedIdx > idx) { // the same set on removal): a raise during an active scroll or resize --
stack->focusedIdx--; // e.g. an onScroll callback raising a window -- would otherwise leave the
} // next wmResizeMove / wmScrollbarDrag operating on the wrong slot.
stack->focusedIdx = wmAdjustIndexForRaise(stack->focusedIdx, idx, stack->count - 1);
if (stack->dragWindow == idx) { stack->dragWindow = wmAdjustIndexForRaise(stack->dragWindow, idx, stack->count - 1);
stack->dragWindow = stack->count - 1; stack->resizeWindow = wmAdjustIndexForRaise(stack->resizeWindow, idx, stack->count - 1);
} else if (stack->dragWindow > idx) { stack->scrollWindow = wmAdjustIndexForRaise(stack->scrollWindow, idx, stack->count - 1);
stack->dragWindow--;
}
// resizeWindow / scrollWindow must shift too (wmDestroyWindow already
// adjusts all three): a raise during an active scroll or resize --
// e.g. an onScroll callback raising a window -- would otherwise leave
// the next wmResizeMove / wmScrollbarDrag operating on the wrong slot.
if (stack->resizeWindow == idx) {
stack->resizeWindow = stack->count - 1;
} else if (stack->resizeWindow > idx) {
stack->resizeWindow--;
}
if (stack->scrollWindow == idx) {
stack->scrollWindow = stack->count - 1;
} else if (stack->scrollWindow > idx) {
stack->scrollWindow--;
}
} }
@ -2144,7 +2159,10 @@ bool wmRemoveMenuItem(MenuT *menu, int32_t id) {
for (int32_t i = 0; i < menu->itemCount; i++) { for (int32_t i = 0; i < menu->itemCount; i++) {
if (menu->items[i].id == id && !menu->items[i].separator) { if (menu->items[i].id == id && !menu->items[i].separator) {
freeMenuItemSubMenu(&menu->items[i]); // Detach only -- do NOT free the item's subMenu tree here. An
// open popup chain may still reference it directly (see the
// matching comment in wmClearMenuItems), so the tree is
// deliberately orphaned rather than freed under the popup.
memmove(&menu->items[i], &menu->items[i + 1], (menu->itemCount - i - 1) * sizeof(MenuItemT)); memmove(&menu->items[i], &menu->items[i + 1], (menu->itemCount - i - 1) * sizeof(MenuItemT));
menu->itemCount--; menu->itemCount--;
return true; return true;
@ -2644,6 +2662,12 @@ void wmSetFocus(WindowStackT *stack, DirtyListT *dl, int32_t idx) {
return; return;
} }
// A window queued for deferred destruction is INERT: focusing it
// would fire onFocus into app state that may already be torn down.
if (stack->windows[idx]->destroyPending) {
return;
}
// Unfocus old window // Unfocus old window
WindowT *oldWin = NULL; WindowT *oldWin = NULL;
@ -2800,12 +2824,22 @@ void wmUpdateContentRect(WindowT *win) {
} }
} }
if (win->contentW < 0) { win->contentW = 0; } if (win->contentW < 0) {
if (win->contentH < 0) { win->contentH = 0; } win->contentW = 0;
}
if (win->contentH < 0) {
win->contentH = 0;
}
// The content clamps above do not propagate into the scrollbar lengths // The content clamps above do not propagate into the scrollbar lengths
// (assigned from the pre-clamp content size), so a window smaller than its // (assigned from the pre-clamp content size), so a window smaller than its
// chrome would leave a negative length feeding scrollbarThumbInfo/draw. // chrome would leave a negative length feeding scrollbarThumbInfo/draw.
if (win->vScroll && win->vScroll->length < 0) { win->vScroll->length = 0; } if (win->vScroll && win->vScroll->length < 0) {
if (win->hScroll && win->hScroll->length < 0) { win->hScroll->length = 0; } win->vScroll->length = 0;
}
if (win->hScroll && win->hScroll->length < 0) {
win->hScroll->length = 0;
}
} }

View file

@ -135,6 +135,13 @@ void wmMenuItemSetChecked(MenuBarT *bar, int32_t id, bool checked);
// Searches submenus recursively. // Searches submenus recursively.
void wmMenuItemSetCheckedInMenu(MenuT *menu, int32_t id, bool checked); void wmMenuItemSetCheckedInMenu(MenuT *menu, int32_t id, bool checked);
// Apply a checked state to the item at index idx within menu, honoring
// implicit radio-group semantics (checking a radio item unchecks its
// contiguous group siblings). This is the single shared primitive behind
// wmMenuItemSetChecked, wmMenuItemSetCheckedInMenu, and the app's menu
// click handler, so the group-scan rule lives in exactly one place.
void menuItemApplyChecked(MenuT *menu, int32_t idx, bool checked);
// Enable or disable a menu item by command ID. // Enable or disable a menu item by command ID.
void wmMenuItemSetEnabled(MenuBarT *bar, int32_t id, bool enabled); void wmMenuItemSetEnabled(MenuBarT *bar, int32_t id, bool enabled);

View file

@ -308,6 +308,12 @@ char *platformReadFile(const char *path, int32_t *outLen);
// it's NULL or already points at a non-whitespace byte. // it's NULL or already points at a non-whitespace byte.
const char *dvxSkipWs(const char *s); const char *dvxSkipWs(const char *s);
// Bounded formatted append into buf at offset pos, never exceeding
// bufSize. Returns the new position, or -1 if the result would truncate,
// vsnprintf failed, or pos was already out of range. Passing a -1 back in
// as pos propagates, collapsing a chain of appends to one truncation check.
int32_t dvxStrAppendf(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) __attribute__((format(printf, 4, 5)));
// Strip trailing ' ', '\t', '\r', '\n' from buf in place. Returns the // Strip trailing ' ', '\t', '\r', '\n' from buf in place. Returns the
// new length. buf may be NULL or empty. // new length. buf may be NULL or empty.
int32_t dvxTrimRight(char *buf); int32_t dvxTrimRight(char *buf);

View file

@ -435,6 +435,12 @@ void *dvxRealloc(void *ptr, size_t size) {
return realloc(ptr, size); return realloc(ptr, size);
} }
// Guard the header addition against size_t wrap, which would otherwise
// hand back a non-NULL pointer over an undersized block.
if (size > SIZE_MAX - sizeof(DvxAllocHeaderT)) {
return NULL;
}
int32_t appId = hdr->appId; int32_t appId = hdr->appId;
uint32_t oldSize = hdr->size; uint32_t oldSize = hdr->size;
@ -844,6 +850,7 @@ static void int9Handler(void) {
if (next != sKeyUp.tail) { if (next != sKeyUp.tail) {
sKeyUp.buf[sKeyUp.head].scancode = scan & 0x7F; sKeyUp.buf[sKeyUp.head].scancode = scan & 0x7F;
sKeyUp.buf[sKeyUp.head].ascii = 0; sKeyUp.buf[sKeyUp.head].ascii = 0;
sKeyUp.head = next; sKeyUp.head = next;
} }
} }
@ -2440,23 +2447,22 @@ static int32_t setVesaMode(uint16_t mode) {
// Formatted append to the system info buffer (newline-terminated). // Formatted append to the system info buffer (newline-terminated).
static void sysInfoAppend(const char *fmt, ...) { static void sysInfoAppend(const char *fmt, ...) {
if (sSysInfoPos >= PLATFORM_SYSINFO_MAX - 1) { char line[PLATFORM_SYSINFO_MAX];
return;
}
va_list ap; va_list ap;
int32_t pos;
va_start(ap, fmt); va_start(ap, fmt);
int32_t written = vsnprintf(sSysInfoBuf + sSysInfoPos, PLATFORM_SYSINFO_MAX - sSysInfoPos, fmt, ap); vsnprintf(line, sizeof(line), fmt, ap);
va_end(ap); va_end(ap);
if (written > 0) { // Accumulate the formatted line and its trailing newline through the
sSysInfoPos += written; // shared bounded appender. The intermediate buffer is the full
} // sysinfo size, so it adds no truncation point the direct write lacked;
// a -1 return leaves sSysInfoPos untouched (buffer full or truncated).
pos = dvxStrAppendf(sSysInfoBuf, PLATFORM_SYSINFO_MAX, sSysInfoPos, "%s\n", line);
if (sSysInfoPos < PLATFORM_SYSINFO_MAX - 1) { if (pos >= 0) {
sSysInfoBuf[sSysInfoPos] = '\n'; sSysInfoPos = pos;
sSysInfoPos++;
sSysInfoBuf[sSysInfoPos] = '\0';
} }
} }
@ -2564,6 +2570,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(dvxReadDirFree) DXE_EXPORT(dvxReadDirFree)
DXE_EXPORT(dvxRealloc) DXE_EXPORT(dvxRealloc)
DXE_EXPORT(dvxSkipWs) DXE_EXPORT(dvxSkipWs)
DXE_EXPORT(dvxStrAppendf)
DXE_EXPORT(dvxStrdup) DXE_EXPORT(dvxStrdup)
DXE_EXPORT(dvxTrimRight) DXE_EXPORT(dvxTrimRight)

View file

@ -34,6 +34,7 @@
#include <ctype.h> #include <ctype.h>
#include <dirent.h> #include <dirent.h>
#include <errno.h> #include <errno.h>
#include <stdarg.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -62,6 +63,33 @@ const char *dvxSkipWs(const char *s) {
} }
// Bounded formatted append: format fmt/args into buf starting at byte
// offset pos, never writing past bufSize. Returns the new position
// (pos + bytes written) on success, or -1 if the result was truncated,
// vsnprintf failed, or pos was already out of range. Feeding a -1 back in
// as pos propagates, so a chain of appends collapses to a single
// truncation check on the final return value.
int32_t dvxStrAppendf(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) {
if (!buf || pos < 0 || pos >= bufSize) {
return -1;
}
va_list args;
int32_t avail = bufSize - pos;
int32_t written;
va_start(args, fmt);
written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args);
va_end(args);
if (written < 0 || written >= avail) {
return -1;
}
return pos + written;
}
int32_t dvxTrimRight(char *buf) { int32_t dvxTrimRight(char *buf) {
if (!buf) { if (!buf) {
return 0; return 0;

View file

@ -149,6 +149,26 @@ int32_t wgtIfaceCount(void) {
} }
// Case-insensitive lookup of a property descriptor by name. The single
// source of truth for "does this widget interface define property X" --
// used by the form runtime (get/set) and the IDE (designer/properties)
// so both layers resolve the Visible/Enabled override identically.
// Returns NULL if iface is NULL or the property is not found.
const WgtPropDescT *wgtIfaceFindProp(const WgtIfaceT *iface, const char *propName) {
if (!iface) {
return NULL;
}
for (int32_t i = 0; i < iface->propCount; i++) {
if (strcasecmp(iface->props[i].name, propName) == 0) {
return &iface->props[i];
}
}
return NULL;
}
const char *wgtIfaceGetPath(const char *name) { const char *wgtIfaceGetPath(const char *name) {
if (!name || !sIfaceMap) { if (!name || !sIfaceMap) {
return NULL; return NULL;

View file

@ -76,6 +76,13 @@ WidgetT *sDragWidget = NULL; // widget being dragged (any drag type)
WidgetT **sPollWidgets = NULL; // stb_ds dynamic array WidgetT **sPollWidgets = NULL; // stb_ds dynamic array
uint32_t sWidgetGen = 0; // bumped per widget destroy (see dvxWgtP.h) uint32_t sWidgetGen = 0; // bumped per widget destroy (see dvxWgtP.h)
// Widget-destroy subscribers. A DXE that keeps a static WidgetT pointer
// (e.g. texthelp's selection tracker) registers a callback here so its
// static is nulled before the widget is freed. A subscriber LIST rather
// than a single slot means a second registrant cannot silently clobber a
// first, reviving the use-after-free the hook exists to prevent.
static void (**sWidgetDestroyFns)(WidgetT *w) = NULL; // stb_ds dynamic array
// Shared clipboard -- process-wide, not per-widget. // Shared clipboard -- process-wide, not per-widget.
static char *sClipboard = NULL; static char *sClipboard = NULL;
static int32_t sClipboardLen = 0; static int32_t sClipboardLen = 0;
@ -325,10 +332,22 @@ void widgetClearReferences(WidgetT *w) {
sKeyPressedBtn = NULL; sKeyPressedBtn = NULL;
} }
// Notify any DXE-registered hook (e.g. texthelp's selection tracker) so // The on-screen tooltip may borrow w->tooltip, which dies with the
// foreign static pointers to w are nulled before w is freed. // widget; hide it so the compositor stops drawing a freed string.
if (sWidgetDestroyFn) { // The context lives in the window root's userData (see wgtInitWindow);
sWidgetDestroyFn(w); // w itself may already be detached from the tree at this point.
if (w->tooltip && w->window && w->window->widgetRoot) {
dvxInvalidateTooltip((AppContextT *)w->window->widgetRoot->userData, w->tooltip);
}
// Notify every DXE-registered subscriber (e.g. texthelp's selection
// tracker) so foreign static pointers to w are nulled before w is freed.
// arrlen is re-read each iteration so a subscriber that unregisters one
// mid-walk cannot index past the array.
for (int32_t i = 0; i < (int32_t)arrlen(sWidgetDestroyFns); i++) {
if (sWidgetDestroyFns[i]) {
sWidgetDestroyFns[i](w);
}
} }
if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL)) { if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL)) {
@ -385,6 +404,36 @@ void widgetDestroyChildren(WidgetT *w) {
} }
// Clears every global interaction pointer (focus/popup/drag/key-pressed)
// whose widget lives on this window. Companion to widgetClearReferences
// for DEFERRED window destruction: the window's widget tree stays
// allocated until the pending-destroy flush, but its backing app state
// (e.g. BASIC control structs in userData) may already be freed, so
// drag/popup/key-press dispatch into it must be cut off immediately.
void widgetDetachWindowReferences(WindowT *win) {
if (sFocusedWidget && sFocusedWidget->window == win) {
sFocusedWidget = NULL;
}
if (sOpenPopup && sOpenPopup->window == win) {
wclsClosePopup(sOpenPopup);
sOpenPopup = NULL;
}
if (sClosedPopup && sClosedPopup->window == win) {
sClosedPopup = NULL;
}
if (sDragWidget && sDragWidget->window == win) {
sDragWidget = NULL;
}
if (sKeyPressedBtn && sKeyPressedBtn->window == win) {
sKeyPressedBtn = NULL;
}
}
// Finds a widget with the given Alt+key accelerator. Recurses the // Finds a widget with the given Alt+key accelerator. Recurses the
// tree depth-first, respecting visibility and enabled state. // tree depth-first, respecting visibility and enabled state.
// //
@ -580,6 +629,25 @@ bool widgetIsHorizContainer(int32_t type) {
} }
// Register a widget-destroy subscriber. Idempotent: a callback already
// present is not added twice, so a module can register on every attach
// without tracking its own state. Fired by widgetClearReferences for
// every destroyed widget so subscribers can null any static pointer to it.
void widgetRegisterDestroyFn(void (*fn)(WidgetT *w)) {
if (!fn) {
return;
}
for (int32_t i = 0; i < (int32_t)arrlen(sWidgetDestroyFns); i++) {
if (sWidgetDestroyFns[i] == fn) {
return;
}
}
arrput(sWidgetDestroyFns, fn);
}
// Unlinks a child from its parent's child list. O(n) in the number // Unlinks a child from its parent's child list. O(n) in the number
// of children because the singly-linked list requires walking to // of children because the singly-linked list requires walking to
// find the predecessor. This is acceptable because child removal // find the predecessor. This is acceptable because child removal
@ -638,3 +706,36 @@ void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSi
*thumbPos = 0; *thumbPos = 0;
} }
} }
// Map a thumb drag to a new scroll position. relMouse is the mouse offset
// (from the track origin) minus the grab offset within the thumb. The
// thumb travels (trackLen - thumbSize) pixels to cover maxScroll units, so
// scroll = maxScroll * relMouse / (trackLen - thumbSize), clamped to range.
// This is the single canonical form shared by every scrolling widget.
int32_t widgetScrollbarThumbDragScroll(int32_t trackLen, int32_t total, int32_t visible, int32_t relMouse, int32_t maxScroll) {
if (maxScroll <= 0) {
return 0;
}
int32_t thumbPos = 0;
int32_t thumbSize = 0;
widgetScrollbarThumb(trackLen, total, visible, 0, &thumbPos, &thumbSize);
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
return clampInt(newScroll, 0, maxScroll);
}
// Remove a widget-destroy subscriber. No-op if the callback is absent, so
// a double-unregister is safe.
void widgetUnregisterDestroyFn(void (*fn)(WidgetT *w)) {
for (int32_t i = 0; i < (int32_t)arrlen(sWidgetDestroyFns); i++) {
if (sWidgetDestroyFns[i] == fn) {
arrdel(sWidgetDestroyFns, i);
return;
}
}
}

View file

@ -59,10 +59,57 @@ static int32_t sPrevMouseY = -1;
// Prototypes // Prototypes
// ============================================================ // ============================================================
static void dispatchButtonEdges(WidgetT *hit, uint32_t snapGen, int32_t buttons, int32_t prevButtons, int32_t relX, int32_t relY);
static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons); static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons);
static void widgetVirtualSize(const WindowT *win, const WidgetT *root, int32_t *outW, int32_t *outH); static void widgetVirtualSize(const WindowT *win, const WidgetT *root, int32_t *outW, int32_t *outH);
// Dispatch mouse button press/release edges to the widget under the cursor.
// Each transition between prevButtons and buttons fires the widget's
// onMouseDown (press edge) or onMouseUp (release edge) callback with the
// 1-based button ordinal (1 left, 2 right, 3 middle). Any callback may
// destroy widgets, so every dereference is gated on the caller's generation
// snapshot still matching sWidgetGen.
//
// The left release edge is delivered first so both callers keep their
// original per-path ordering: on the left-release path (buttons has
// MOUSE_LEFT clear) this fires the left onMouseUp before any right/middle
// edge, matching the pre-extraction leftUp-first order; on the press-time
// path (MOUSE_LEFT set) the left-release edge never fires, so the remaining
// edges deliver left/right/middle presses then right/middle releases exactly
// as before.
static void dispatchButtonEdges(WidgetT *hit, uint32_t snapGen, int32_t buttons, int32_t prevButtons, int32_t relX, int32_t relY) {
// Left release edge first (leftUp-first order on the release path;
// unreachable on the press path where MOUSE_LEFT is set)
if (sWidgetGen == snapGen && !(buttons & MOUSE_LEFT) && (prevButtons & MOUSE_LEFT) && hit->onMouseUp) {
hit->onMouseUp(hit, 1, relX, relY);
}
// Press edges (left, right, middle)
if (sWidgetGen == snapGen && (buttons & MOUSE_LEFT) && !(prevButtons & MOUSE_LEFT) && hit->onMouseDown) {
hit->onMouseDown(hit, 1, relX, relY);
}
if (sWidgetGen == snapGen && (buttons & MOUSE_RIGHT) && !(prevButtons & MOUSE_RIGHT) && hit->onMouseDown) {
hit->onMouseDown(hit, 2, relX, relY);
}
if (sWidgetGen == snapGen && (buttons & MOUSE_MIDDLE) && !(prevButtons & MOUSE_MIDDLE) && hit->onMouseDown) {
hit->onMouseDown(hit, 3, relX, relY);
}
// Remaining release edges (right, middle)
if (sWidgetGen == snapGen && !(buttons & MOUSE_RIGHT) && (prevButtons & MOUSE_RIGHT) && hit->onMouseUp) {
hit->onMouseUp(hit, 2, relX, relY);
}
if (sWidgetGen == snapGen && !(buttons & MOUSE_MIDDLE) && (prevButtons & MOUSE_MIDDLE) && hit->onMouseUp) {
hit->onMouseUp(hit, 3, relX, relY);
}
}
// Manages automatic scrollbar addition/removal for widget-based windows. // Manages automatic scrollbar addition/removal for widget-based windows.
// Called on every invalidation to ensure scrollbars match the current // Called on every invalidation to ensure scrollbars match the current
// widget tree's minimum size requirements. // widget tree's minimum size requirements.
@ -199,10 +246,15 @@ void widgetOnBlur(WindowT *win) {
sFocusedWidget = NULL; sFocusedWidget = NULL;
wgtInvalidatePaint(prev); wgtInvalidatePaint(prev);
// Snapshot the destroy generation: the blur commit below may fire
// a user Change handler that destroys the widget -- prev is
// dangling then and must not be dereferenced.
uint32_t gen = sWidgetGen;
// Commit/clamp any in-progress edit before the app onBlur. // Commit/clamp any in-progress edit before the app onBlur.
wclsOnBlur(prev); wclsOnBlur(prev);
if (prev->onBlur) { if (sWidgetGen == gen && prev->onBlur) {
prev->onBlur(prev); prev->onBlur(prev);
} }
} }
@ -350,6 +402,14 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons) { static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons) {
// Defense in depth: a drag on a deferred-destroyed window must be
// dropped, not updated -- the widget tree is still allocated but its
// backing app state may already be freed. (deferDestroyWindow also
// clears this via widgetDetachWindowReferences.)
if (sDragWidget && sDragWidget->window && sDragWidget->window->destroyPending) {
sDragWidget = NULL;
}
// Close popups from other windows // Close popups from other windows
if (sOpenPopup && sOpenPopup->window != win) { if (sOpenPopup && sOpenPopup->window != win) {
wclsClosePopup(sOpenPopup); wclsClosePopup(sOpenPopup);
@ -362,6 +422,12 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
wgtInvalidatePaint(sDragWidget); wgtInvalidatePaint(sDragWidget);
sDragWidget = NULL; sDragWidget = NULL;
// Record the release edge. Leaving a stale LEFT bit here would
// make the next stationary press fail the leftPressEdge gate
// (swallowing the click) and make the next move event deliver a
// phantom left onMouseUp to whatever widget is under the cursor.
sPrevMouseButtons = buttons;
return; return;
} }
@ -457,25 +523,14 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
int32_t relX = x - upHit->x - upHit->contentOffX; int32_t relX = x - upHit->x - upHit->contentOffX;
int32_t relY = y - upHit->y - upHit->contentOffY; int32_t relY = y - upHit->y - upHit->contentOffY;
if ((sPrevMouseButtons & MOUSE_LEFT) && upHit->onMouseUp) { // Snapshot the destroy generation: any callback may destroy
upHit->onMouseUp(upHit, 1, relX, relY); // upHit (a BASIC MouseUp handler rebuilding its form), and several
} // button edges can coalesce into one poll, so dispatchButtonEdges
// gates every dereference on it. MOUSE_LEFT is clear here, so this
// delivers the left onMouseUp plus any right/middle edges.
uint32_t upGen = sWidgetGen;
if ((buttons & MOUSE_RIGHT) && !(sPrevMouseButtons & MOUSE_RIGHT) && upHit->onMouseDown) { dispatchButtonEdges(upHit, upGen, buttons, sPrevMouseButtons, relX, relY);
upHit->onMouseDown(upHit, 2, relX, relY);
}
if (!(buttons & MOUSE_RIGHT) && (sPrevMouseButtons & MOUSE_RIGHT) && upHit->onMouseUp) {
upHit->onMouseUp(upHit, 2, relX, relY);
}
if ((buttons & MOUSE_MIDDLE) && !(sPrevMouseButtons & MOUSE_MIDDLE) && upHit->onMouseDown) {
upHit->onMouseDown(upHit, 3, relX, relY);
}
if (!(buttons & MOUSE_MIDDLE) && (sPrevMouseButtons & MOUSE_MIDDLE) && upHit->onMouseUp) {
upHit->onMouseUp(upHit, 3, relX, relY);
}
} }
sPrevMouseButtons = buttons; sPrevMouseButtons = buttons;
@ -493,15 +548,26 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
return; return;
} }
// Press-time dispatch must fire only on the left-button DOWN edge, not on
// every held-and-move event. Otherwise a non-drag-capturing widget
// (checkbox, spinner, radio) re-runs its onMouse on every move pixel while
// the button is held, re-toggling or re-stepping state. Drag widgets set
// sDragWidget on this first edge and are then handled by the sDragWidget
// fast paths above, so the edge gate still fires their initial press.
bool leftPressEdge = (buttons & MOUSE_LEFT) && !(sPrevMouseButtons & MOUSE_LEFT);
// Clear focus from the previously focused widget. Must set // Clear focus from the previously focused widget. Must set
// sFocusedWidget to NULL BEFORE invalidating so the inline paint // sFocusedWidget to NULL BEFORE invalidating so the inline paint
// sees the widget as unfocused and erases its highlight. Only // sees the widget as unfocused and erases its highlight. Only on
// clear the selection when the click landed on a DIFFERENT widget // the press edge -- wclsOnMouse (which re-establishes focus) is
// -- same-widget clicks (drag-select, shift-click) manage their // edge-gated below, so clearing on a held-and-move event would blur
// own selection in the widget's mouse handler. // the widget mid-click with no way to get focus back. Only clear
// the selection when the click landed on a DIFFERENT widget --
// same-widget clicks (drag-select, shift-click) manage their own
// selection in the widget's mouse handler.
WidgetT *prevFocus = sFocusedWidget; WidgetT *prevFocus = sFocusedWidget;
if (sFocusedWidget) { if (leftPressEdge && sFocusedWidget) {
sFocusedWidget = NULL; sFocusedWidget = NULL;
if (hit != prevFocus) { if (hit != prevFocus) {
@ -517,19 +583,21 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
// may be dangling, so every later dereference is gated on it. // may be dangling, so every later dereference is gated on it.
uint32_t gen = sWidgetGen; uint32_t gen = sWidgetGen;
// Press-time dispatch must fire only on the left-button DOWN edge, not on
// every held-and-move event. Otherwise a non-drag-capturing widget
// (checkbox, spinner, radio) re-runs its onMouse on every move pixel while
// the button is held, re-toggling or re-stepping state. Drag widgets set
// sDragWidget on this first edge and are then handled by the sDragWidget
// fast paths above, so the edge gate still fires their initial press.
bool leftPressEdge = (buttons & MOUSE_LEFT) && !(sPrevMouseButtons & MOUSE_LEFT);
if (leftPressEdge) { if (leftPressEdge) {
// Dispatch to the hit widget's mouse handler via vtable. The handler // Dispatch to the hit widget's mouse handler via vtable. The handler
// is responsible for setting sFocusedWidget if it wants focus. // is responsible for setting sFocusedWidget if it wants focus.
if (hit->enabled) { if (hit->enabled) {
wclsOnMouse(hit, root, vx, vy); wclsOnMouse(hit, root, vx, vy);
// Repaint the hit widget after its handler runs so press-driven
// visual state (checkbox toggle, dropdown popup, slider thumb)
// shows immediately. Centralized here because the dispatcher
// otherwise invalidates only the previous focus owner, forcing
// each widget class to self-invalidate. Guarded: the handler
// may have destroyed hit.
if (sWidgetGen == gen) {
wgtInvalidatePaint(hit);
}
} }
// Universal click/double-click callbacks -- fire for ALL widget types // Universal click/double-click callbacks -- fire for ALL widget types
@ -554,43 +622,11 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
int32_t relX = vx - hit->x - hit->contentOffX; int32_t relX = vx - hit->x - hit->contentOffX;
int32_t relY = vy - hit->y - hit->contentOffY; int32_t relY = vy - hit->y - hit->contentOffY;
// MouseDown: button just pressed // Button press/release edges. MOUSE_LEFT is always set once execution
if ((buttons & MOUSE_LEFT) && !(sPrevMouseButtons & MOUSE_LEFT)) { // reaches here, so dispatchButtonEdges delivers the left onMouseDown
if (sWidgetGen == gen && hit->onMouseDown) { // plus any right/middle edges; the left release edge is owned by the
hit->onMouseDown(hit, 1, relX, relY); // early-return release path above.
} dispatchButtonEdges(hit, gen, buttons, sPrevMouseButtons, relX, relY);
}
if ((buttons & MOUSE_RIGHT) && !(sPrevMouseButtons & MOUSE_RIGHT)) {
if (sWidgetGen == gen && hit->onMouseDown) {
hit->onMouseDown(hit, 2, relX, relY);
}
}
if ((buttons & MOUSE_MIDDLE) && !(sPrevMouseButtons & MOUSE_MIDDLE)) {
if (sWidgetGen == gen && hit->onMouseDown) {
hit->onMouseDown(hit, 3, relX, relY);
}
}
// MouseUp: button just released
if (!(buttons & MOUSE_LEFT) && (sPrevMouseButtons & MOUSE_LEFT)) {
if (sWidgetGen == gen && hit->onMouseUp) {
hit->onMouseUp(hit, 1, relX, relY);
}
}
if (!(buttons & MOUSE_RIGHT) && (sPrevMouseButtons & MOUSE_RIGHT)) {
if (sWidgetGen == gen && hit->onMouseUp) {
hit->onMouseUp(hit, 2, relX, relY);
}
}
if (!(buttons & MOUSE_MIDDLE) && (sPrevMouseButtons & MOUSE_MIDDLE)) {
if (sWidgetGen == gen && hit->onMouseUp) {
hit->onMouseUp(hit, 3, relX, relY);
}
}
// MouseMove: position changed // MouseMove: position changed
if (vx != sPrevMouseX || vy != sPrevMouseY) { if (vx != sPrevMouseX || vy != sPrevMouseY) {

View file

@ -376,12 +376,21 @@ void wgtSetFocused(WidgetT *w) {
wgtInvalidatePaint(w); wgtInvalidatePaint(w);
if (prev && prev != w) { if (prev && prev != w) {
// Snapshot the destroy generation: the blur commit and app onBlur
// below may fire a user Change handler that destroys widgets --
// prev and w are dangling then and must not be dereferenced.
uint32_t gen = sWidgetGen;
// Commit/clamp any in-progress edit before the app onBlur. // Commit/clamp any in-progress edit before the app onBlur.
wclsOnBlur(prev); wclsOnBlur(prev);
if (prev->onBlur) { if (sWidgetGen == gen && prev->onBlur) {
prev->onBlur(prev); prev->onBlur(prev);
} }
if (sWidgetGen != gen) {
return;
}
} }
if (w->onFocus) { if (w->onFocus) {
@ -424,6 +433,13 @@ void wgtSetText(WidgetT *w, const char *text) {
void wgtSetTooltip(WidgetT *w, const char *text) { void wgtSetTooltip(WidgetT *w, const char *text) {
if (w) { if (w) {
// The on-screen tooltip may borrow the OLD string, whose owner may
// free it before reassigning (e.g. a BASIC ToolTipText write);
// hide it before the pointer is replaced.
if (w->tooltip && w->tooltip != text && w->window && w->window->widgetRoot) {
dvxInvalidateTooltip((AppContextT *)w->window->widgetRoot->userData, w->tooltip);
}
w->tooltip = text; w->tooltip = text;
} }
} }

View file

@ -28,13 +28,16 @@
#include "listHelp.h" #include "listHelp.h"
#include "../texthelp/textHelp.h" #include "../texthelp/textHelp.h"
#include "stb_ds_wrap.h"
#include <stdlib.h>
#include <string.h> #include <string.h>
// ============================================================ // ============================================================
// Prototypes // Prototypes
// ============================================================ // ============================================================
char **widgetAdoptOwnedStrings(char **owned, const char **src, int32_t count);
void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color); void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color);
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH); void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH);
int32_t widgetMaxItemLen(const char **items, int32_t count); int32_t widgetMaxItemLen(const char **items, int32_t count);
@ -44,6 +47,27 @@ bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t po
int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, int32_t currentIdx); int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, int32_t currentIdx);
// Rebuild an internally-owned stb_ds string array as a deep copy of an
// external source array. Shared by ListBox (ownedItems) and ListView
// (ownedCells) so the free-old/reset/strdup adoption sequence lives in one
// place. Returns the (possibly reallocated) owned array.
char **widgetAdoptOwnedStrings(char **owned, const char **src, int32_t count) {
for (int32_t i = 0; i < (int32_t)arrlen(owned); i++) {
free(owned[i]);
}
arrsetlen(owned, 0);
for (int32_t i = 0; i < count; i++) {
const char *s = src ? src[i] : NULL;
arrput(owned, strdup(s ? s : ""));
}
return owned;
}
// Draws a small downward-pointing filled triangle (7, 5, 3, 1 pixels // Draws a small downward-pointing filled triangle (7, 5, 3, 1 pixels
// wide across 4 rows) centered at the given position. Used by both // wide across 4 rows) centered at the given position. Used by both
// Dropdown and ComboBox for the drop button arrow glyph. // Dropdown and ComboBox for the drop button arrow glyph.

View file

@ -35,6 +35,19 @@
#define DROPDOWN_MAX_VISIBLE 8 #define DROPDOWN_MAX_VISIBLE 8
#define POPUP_SCROLLBAR_W SCROLLBAR_WIDTH #define POPUP_SCROLLBAR_W SCROLLBAR_WIDTH
// ============================================================
// Owned string-array adoption
// ============================================================
// Rebuild an internally-owned stb_ds string array as a deep copy of an
// external source array, freeing any previously owned strings first. Shared
// by ListBox (ownedItems) and ListView (ownedCells): a reorderable list must
// own its strings so drag-reorder can rewrite the array in place without
// mutating a caller-owned SetItems/SetData array. Returns the (possibly
// reallocated) owned array so the caller can repoint its handle. NULL source
// elements become empty strings.
char **widgetAdoptOwnedStrings(char **owned, const char **src, int32_t count);
// ============================================================ // ============================================================
// Dropdown arrow glyph // Dropdown arrow glyph
// ============================================================ // ============================================================

View file

@ -62,6 +62,13 @@
#define FRAME_NAK 0x02 #define FRAME_NAK 0x02
#define FRAME_RST 0x03 #define FRAME_RST 0x03
// RST frames carry a marker in the SEQ byte so a reset reply can be told
// apart from a reset request. Only requests are answered -- answering
// every RST with another RST would ping-pong forever once a single RST
// frame is on the wire.
#define RST_MARKER_REQUEST 0x00
#define RST_MARKER_REPLY 0x01
// Header size: SEQ + TYPE + LEN // Header size: SEQ + TYPE + LEN
#define HEADER_SIZE 3 #define HEADER_SIZE 3
// CRC size // CRC size
@ -116,11 +123,11 @@ typedef struct {
uint8_t data[PKT_MAX_PAYLOAD]; uint8_t data[PKT_MAX_PAYLOAD];
} TxSlotT; } TxSlotT;
// Connection state. txSlots is a sliding window indexed by [0..txCount-1], // Connection state. txSlots is a circular sliding window: logical slot i
// where slot 0 is the oldest unacked frame (sequence txAckSeq) and // (0..txCount-1) lives at physical index (txHead + i) % PKT_MAX_WINDOW and
// slot txCount-1 is the newest. When an ACK advances txAckSeq, the // holds sequence txAckSeq + i. txHead is the physical index of the oldest
// surviving slots are shifted down with memmove so slot i always holds // unacked frame (sequence txAckSeq). A cumulative ACK advances txHead and
// sequence txAckSeq + i. // txAckSeq by the freed count, so the window slides in O(1) with no copying.
struct PktConnS { struct PktConnS {
int com; int com;
int windowSize; int windowSize;
@ -132,6 +139,7 @@ struct PktConnS {
uint8_t txAckSeq; // oldest unacknowledged sequence uint8_t txAckSeq; // oldest unacknowledged sequence
TxSlotT txSlots[PKT_MAX_WINDOW]; TxSlotT txSlots[PKT_MAX_WINDOW];
int txCount; // number of slots in use int txCount; // number of slots in use
int txHead; // physical index of oldest unacked slot
// Receive state (Go-Back-N receiver: only accepts in-order frames) // Receive state (Go-Back-N receiver: only accepts in-order frames)
uint8_t rxExpectSeq; // next expected sequence number uint8_t rxExpectSeq; // next expected sequence number
@ -199,7 +207,7 @@ static const uint16_t sCrcTable[256] = {
// Prototypes (alphabetical) // Prototypes (alphabetical)
// ======================================================================== // ========================================================================
static int ackAdvance(PktConnT *conn, uint8_t seq); static int32_t ackAdvance(PktConnT *conn, uint8_t seq);
static uint16_t crcCalc(const uint8_t *data, int len); static uint16_t crcCalc(const uint8_t *data, int len);
bool pktCanSend(PktConnT *conn); bool pktCanSend(PktConnT *conn);
void pktClose(PktConnT *conn); void pktClose(PktConnT *conn);
@ -214,7 +222,7 @@ static bool rxProcessByte(PktConnT *conn, uint8_t byte);
static void sendAck(PktConnT *conn, uint8_t seq); static void sendAck(PktConnT *conn, uint8_t seq);
static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len); static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len);
static void sendNak(PktConnT *conn, uint8_t seq); static void sendNak(PktConnT *conn, uint8_t seq);
static void sendRst(PktConnT *conn); static void sendRst(PktConnT *conn, uint8_t marker);
static int seqInWindow(uint8_t seq, uint8_t base, int size); static int seqInWindow(uint8_t seq, uint8_t base, int size);
static int txSlotIndex(PktConnT *conn, uint8_t seq); static int txSlotIndex(PktConnT *conn, uint8_t seq);
@ -222,7 +230,7 @@ static int txSlotIndex(PktConnT *conn, uint8_t seq);
// Returns how many outstanding slots a cumulative ACK frees, or -1 if the // Returns how many outstanding slots a cumulative ACK frees, or -1 if the
// ACK's sequence number is outside the live send window. Wrap-safe via // ACK's sequence number is outside the live send window. Wrap-safe via
// unsigned subtraction (txNextSeq - txAckSeq == txCount). // unsigned subtraction (txNextSeq - txAckSeq == txCount).
static int ackAdvance(PktConnT *conn, uint8_t seq) { static int32_t ackAdvance(PktConnT *conn, uint8_t seq) {
uint8_t diff = seq - conn->txAckSeq; uint8_t diff = seq - conn->txAckSeq;
if (diff > (uint8_t)conn->txCount) { if (diff > (uint8_t)conn->txCount) {
@ -310,6 +318,7 @@ PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *call
conn->txNextSeq = 0; conn->txNextSeq = 0;
conn->txAckSeq = 0; conn->txAckSeq = 0;
conn->txCount = 0; conn->txCount = 0;
conn->txHead = 0;
conn->rxExpectSeq = 0; conn->rxExpectSeq = 0;
conn->rxState = RX_STATE_HUNT; conn->rxState = RX_STATE_HUNT;
conn->rxFrameLen = 0; conn->rxFrameLen = 0;
@ -363,11 +372,12 @@ int pktReset(PktConnT *conn) {
conn->txNextSeq = 0; conn->txNextSeq = 0;
conn->txAckSeq = 0; conn->txAckSeq = 0;
conn->txCount = 0; conn->txCount = 0;
conn->txHead = 0;
conn->rxExpectSeq = 0; conn->rxExpectSeq = 0;
conn->rxState = RX_STATE_HUNT; conn->rxState = RX_STATE_HUNT;
conn->rxFrameLen = 0; conn->rxFrameLen = 0;
sendRst(conn); sendRst(conn, RST_MARKER_REQUEST);
return PKT_SUCCESS; return PKT_SUCCESS;
} }
@ -402,7 +412,7 @@ int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) {
} }
// Store in retransmit buffer // Store in retransmit buffer
slot = &conn->txSlots[conn->txCount]; slot = &conn->txSlots[(conn->txHead + conn->txCount) % PKT_MAX_WINDOW];
memcpy(slot->data, data, len); memcpy(slot->data, data, len);
slot->len = len; slot->len = len;
slot->seq = conn->txNextSeq; slot->seq = conn->txNextSeq;
@ -462,12 +472,21 @@ static bool processFrame(PktConnT *conn, const uint8_t *frame, int len) {
switch (type) { switch (type) {
case FRAME_DATA: case FRAME_DATA:
if (seq == conn->rxExpectSeq) { if (seq == conn->rxExpectSeq) {
// In-order delivery // In-order delivery. Copy the payload and advance the
if (conn->callback) { // receive window BEFORE invoking the callback: the callback
conn->callback(conn->callbackCtx, &frame[HEADER_SIZE], payloadLen); // may legally re-enter pktSend/pktPoll (see pktPoll), and
// stale RX state would deliver this same frame twice while
// nested receive bytes overwrite rxFrame under the reader.
uint8_t payloadCopy[PKT_MAX_PAYLOAD];
if (payloadLen > 0) {
memcpy(payloadCopy, &frame[HEADER_SIZE], payloadLen);
} }
conn->rxExpectSeq++; conn->rxExpectSeq++;
sendAck(conn, conn->rxExpectSeq); sendAck(conn, conn->rxExpectSeq);
if (conn->callback) {
conn->callback(conn->callbackCtx, payloadCopy, payloadLen);
}
delivered = true; delivered = true;
} else if (seqInWindow(seq, conn->rxExpectSeq, conn->windowSize)) { } else if (seqInWindow(seq, conn->rxExpectSeq, conn->windowSize)) {
// Out of order but in window -- NAK the one we want // Out of order but in window -- NAK the one we want
@ -484,18 +503,16 @@ static bool processFrame(PktConnT *conn, const uint8_t *frame, int len) {
case FRAME_ACK: { case FRAME_ACK: {
// ACK carries the next expected sequence number (cumulative). // ACK carries the next expected sequence number (cumulative).
// Free exactly the acknowledged slots; ignore out-of-window ACKs // Free exactly the acknowledged slots; ignore out-of-window ACKs
// (n < 0) and no-progress duplicates (n == 0). The surviving // (n < 0) and no-progress duplicates (n == 0). Advancing txHead
// slots must be shifted down so slot i again holds sequence // by the freed count slides the circular window in O(1): logical
// txAckSeq + i -- txSlotIndex, the NAK loop, and pktSend all // slot i still maps to sequence txAckSeq + i, the invariant that
// depend on that invariant. Without the shift, a partial ACK // txSlotIndex, the NAK loop, and pktSend all depend on.
// misaligns the window and the next send overwrites the int32_t n = ackAdvance(conn, seq);
// retransmit copy of an in-flight frame.
int n = ackAdvance(conn, seq);
if (n > 0) { if (n > 0) {
conn->txAckSeq = (uint8_t)(conn->txAckSeq + n); conn->txAckSeq = (uint8_t)(conn->txAckSeq + n);
conn->txCount -= n; conn->txCount -= n;
memmove(&conn->txSlots[0], &conn->txSlots[n], (size_t)conn->txCount * sizeof(TxSlotT)); conn->txHead = (conn->txHead + n) % PKT_MAX_WINDOW;
} }
break; break;
} }
@ -506,7 +523,7 @@ static bool processFrame(PktConnT *conn, const uint8_t *frame, int len) {
if (idx >= 0) { if (idx >= 0) {
// Retransmit this slot and all after it (go-back-N) // Retransmit this slot and all after it (go-back-N)
for (int i = idx; i < conn->txCount; i++) { for (int i = idx; i < conn->txCount; i++) {
TxSlotT *slot = &conn->txSlots[i]; TxSlotT *slot = &conn->txSlots[(conn->txHead + i) % PKT_MAX_WINDOW];
sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len); sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len);
slot->timer = clock(); slot->timer = clock();
} }
@ -515,12 +532,17 @@ static bool processFrame(PktConnT *conn, const uint8_t *frame, int len) {
} }
case FRAME_RST: case FRAME_RST:
// Remote requested reset -- clear state and respond with RST // Remote reset -- clear local sequence state. Only a reset
// request is answered (with the reply marker); a reply is never
// answered, so RST frames cannot ping-pong indefinitely.
conn->txNextSeq = 0; conn->txNextSeq = 0;
conn->txAckSeq = 0; conn->txAckSeq = 0;
conn->txCount = 0; conn->txCount = 0;
conn->txHead = 0;
conn->rxExpectSeq = 0; conn->rxExpectSeq = 0;
sendRst(conn); if (seq == RST_MARKER_REQUEST) {
sendRst(conn, RST_MARKER_REPLY);
}
break; break;
} }
@ -538,7 +560,7 @@ static void retransmitCheck(PktConnT *conn) {
clock_t timeout = (clock_t)RETRANSMIT_TIMEOUT_MS * CLOCKS_PER_SEC / 1000; clock_t timeout = (clock_t)RETRANSMIT_TIMEOUT_MS * CLOCKS_PER_SEC / 1000;
for (int i = 0; i < conn->txCount; i++) { for (int i = 0; i < conn->txCount; i++) {
TxSlotT *slot = &conn->txSlots[i]; TxSlotT *slot = &conn->txSlots[(conn->txHead + i) % PKT_MAX_WINDOW];
if (now - slot->timer >= timeout) { if (now - slot->timer >= timeout) {
sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len); sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len);
slot->timer = now; slot->timer = now;
@ -564,15 +586,19 @@ static bool rxProcessByte(PktConnT *conn, uint8_t byte) {
case RX_STATE_ACTIVE: case RX_STATE_ACTIVE:
if (byte == FLAG_BYTE) { if (byte == FLAG_BYTE) {
// End of frame (or start of next) // End of frame (or start of next). Consume the frame length
// BEFORE processing: processFrame may run a callback that
// re-enters pktPoll, and stale RX state would re-parse this
// frame or discard a nested partial frame afterward.
bool delivered = false; bool delivered = false;
int frameLen = conn->rxFrameLen;
if (conn->rxFrameLen >= MIN_FRAME_SIZE) { conn->rxFrameLen = 0;
delivered = processFrame(conn, conn->rxFrame, conn->rxFrameLen);
if (frameLen >= MIN_FRAME_SIZE) {
delivered = processFrame(conn, conn->rxFrame, frameLen);
} }
// Reset for next frame
conn->rxFrameLen = 0;
return delivered; return delivered;
} else if (byte == ESC_BYTE) { } else if (byte == ESC_BYTE) {
conn->rxState = RX_STATE_ESCAPE; conn->rxState = RX_STATE_ESCAPE;
@ -660,8 +686,8 @@ static void sendNak(PktConnT *conn, uint8_t seq) {
} }
static void sendRst(PktConnT *conn) { static void sendRst(PktConnT *conn, uint8_t marker) {
sendFrame(conn, 0, FRAME_RST, 0, 0); sendFrame(conn, marker, FRAME_RST, 0, 0);
} }

View file

@ -156,7 +156,10 @@
#define UART_READ_MCR(C) inportb((C)->base + UART_MCR) #define UART_READ_MCR(C) inportb((C)->base + UART_MCR)
#define UART_READ_LSR(C) ((C)->lsr = inportb((C)->base + UART_LSR)) #define UART_READ_LSR(C) ((C)->lsr = inportb((C)->base + UART_LSR))
#define UART_READ_MSR(C) ((C)->msr = inportb((C)->base + UART_MSR)) #define UART_READ_MSR(C) ((C)->msr = inportb((C)->base + UART_MSR))
#define UART_READ_BPS(C) ((OUTP((C)->base + UART_LCR, inportb((C)->base + UART_LCR) | LCR_DLAB) & 0) | inportw((C)->base + UART_DLW) | (OUTP((C)->base + UART_LCR, inportb((C)->base + UART_LCR) & ~LCR_DLAB) & 0)) #define UART_READ_BPS(C, D) \
{ outportb((C)->base + UART_LCR, inportb((C)->base + UART_LCR) | LCR_DLAB); \
(D) = (uint16_t)inportw((C)->base + UART_DLW); \
outportb((C)->base + UART_LCR, inportb((C)->base + UART_LCR) & ~LCR_DLAB); }
// UART write macros // UART write macros
#define UART_WRITE_DATA(C, D) outportb((C)->base + UART_TX, (D)) #define UART_WRITE_DATA(C, D) outportb((C)->base + UART_TX, (D))
@ -873,6 +876,7 @@ int rs232GetBase(int com) {
int32_t rs232GetBps(int com) { int32_t rs232GetBps(int com) {
Rs232StateT *port = &sComPorts[com]; Rs232StateT *port = &sComPorts[com];
uint16_t divisor;
if (com < COM_MIN || com > COM_MAX) { if (com < COM_MIN || com > COM_MAX) {
return RS232_ERR_INVALID_PORT; return RS232_ERR_INVALID_PORT;
@ -881,7 +885,13 @@ int32_t rs232GetBps(int com) {
return RS232_ERR_NOT_OPEN; return RS232_ERR_NOT_OPEN;
} }
return divisorToBps((uint16_t)UART_READ_BPS(port)); // The ISR must not fire inside the DLAB=1 window: with DLAB set, its
// data register access hits the divisor latch instead of RBR/THR.
asm("CLI");
UART_READ_BPS(port, divisor);
asm("STI");
return divisorToBps(divisor);
} }
@ -1291,7 +1301,11 @@ int rs232SetBps(int com, int32_t bps) {
return divisor; return divisor;
} }
// The ISR must not fire inside the DLAB=1 window (see rs232GetBps).
asm("CLI");
UART_WRITE_BPS(port, (uint16_t)divisor); UART_WRITE_BPS(port, (uint16_t)divisor);
asm("STI");
return RS232_SUCCESS; return RS232_SUCCESS;
} }

View file

@ -46,6 +46,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h>
#include "secLink.h" #include "secLink.h"
#include "../rs232/rs232.h" #include "../rs232/rs232.h"
#include "../packet/packet.h" #include "../packet/packet.h"
@ -67,6 +68,11 @@
#define ENCRYPT_FLAG 0x80 #define ENCRYPT_FLAG 0x80
#define CHANNEL_MASK 0x7F #define CHANNEL_MASK 0x7F
// Handshake wait bound. The peer may need several seconds for its DH
// keypair on slow hardware, but a silent peer (proxy or BBS not running)
// must not hang the single-threaded caller forever.
#define HANDSHAKE_TIMEOUT_MS 30000
// ======================================================================== // ========================================================================
// Types // Types
@ -84,6 +90,7 @@ struct SecLinkS {
uint8_t myPub[SEC_DH_KEY_SIZE]; uint8_t myPub[SEC_DH_KEY_SIZE];
uint8_t remoteKey[SEC_DH_KEY_SIZE]; uint8_t remoteKey[SEC_DH_KEY_SIZE];
bool gotRemoteKey; bool gotRemoteKey;
bool txEncryptBusy;
}; };
@ -117,8 +124,26 @@ static void completeHandshake(SecLinkT *link) {
uint8_t txKey[SEC_XTEA_KEY_SIZE]; uint8_t txKey[SEC_XTEA_KEY_SIZE];
uint8_t rxKey[SEC_XTEA_KEY_SIZE]; uint8_t rxKey[SEC_XTEA_KEY_SIZE];
bool weAreLower; bool weAreLower;
secDhComputeSecret(link->dh, link->remoteKey, SEC_DH_KEY_SIZE); int rc;
secDhDeriveKey(link->dh, masterKey, SEC_XTEA_KEY_SIZE);
// An invalid remote key (out of range, e.g. all zeros) must fail the
// handshake -- ignoring these return codes would leave masterKey as
// uninitialized stack memory and enter READY with garbage key material.
rc = secDhComputeSecret(link->dh, link->remoteKey, SEC_DH_KEY_SIZE);
if (rc != SEC_SUCCESS) {
secDhDestroy(link->dh);
link->dh = 0;
link->state = STATE_ERROR;
return;
}
rc = secDhDeriveKey(link->dh, masterKey, SEC_XTEA_KEY_SIZE);
if (rc != SEC_SUCCESS) {
secDhDestroy(link->dh);
link->dh = 0;
link->state = STATE_ERROR;
return;
}
// Derive directional keys so each side encrypts with a unique key // Derive directional keys so each side encrypts with a unique key
weAreLower = (memcmp(link->myPub, link->remoteKey, SEC_DH_KEY_SIZE) < 0); weAreLower = (memcmp(link->myPub, link->remoteKey, SEC_DH_KEY_SIZE) < 0);
@ -127,6 +152,12 @@ static void completeHandshake(SecLinkT *link) {
rxKey[i] = masterKey[i] ^ (weAreLower ? RX_KEY_XOR : TX_KEY_XOR); rxKey[i] = masterKey[i] ^ (weAreLower ? RX_KEY_XOR : TX_KEY_XOR);
} }
// A repeat handshake replaces the session ciphers. Destroy the previous
// contexts (securely zeroing their key material) before overwriting the
// pointers, or every re-handshake would leak both cipher blocks.
secCipherDestroy(link->txCipher);
secCipherDestroy(link->rxCipher);
link->txCipher = secCipherCreate(txKey); link->txCipher = secCipherCreate(txKey);
link->rxCipher = secCipherCreate(rxKey); link->rxCipher = secCipherCreate(rxKey);
@ -141,16 +172,10 @@ static void completeHandshake(SecLinkT *link) {
// If either cipher failed to allocate, abort rather than entering READY // If either cipher failed to allocate, abort rather than entering READY
// with a NULL cipher (which would silently transmit cleartext). // with a NULL cipher (which would silently transmit cleartext).
if (!link->txCipher || !link->rxCipher) { if (!link->txCipher || !link->rxCipher) {
if (link->txCipher) { secCipherDestroy(link->txCipher);
secCipherDestroy(link->txCipher); link->txCipher = 0;
link->txCipher = 0; secCipherDestroy(link->rxCipher);
} link->rxCipher = 0;
if (link->rxCipher) {
secCipherDestroy(link->rxCipher);
link->rxCipher = 0;
}
link->state = STATE_ERROR; link->state = STATE_ERROR;
return; return;
} }
@ -216,15 +241,9 @@ void secLinkClose(SecLinkT *link) {
return; return;
} }
if (link->txCipher) { secCipherDestroy(link->txCipher);
secCipherDestroy(link->txCipher); secCipherDestroy(link->rxCipher);
} secDhDestroy(link->dh);
if (link->rxCipher) {
secCipherDestroy(link->rxCipher);
}
if (link->dh) {
secDhDestroy(link->dh);
}
if (link->pkt) { if (link->pkt) {
pktClose(link->pkt); pktClose(link->pkt);
} }
@ -245,18 +264,18 @@ int secLinkGetPending(SecLinkT *link) {
int secLinkHandshake(SecLinkT *link) { int secLinkHandshake(SecLinkT *link) {
int len; clock_t start;
int rc; clock_t timeout;
int len;
int rc;
if (!link) { if (!link) {
return SECLINK_ERR_PARAM; return SECLINK_ERR_PARAM;
} }
// Generate DH keypair (idempotent: free any context left by a prior call) // Generate DH keypair (idempotent: free any context left by a prior call)
if (link->dh) { secDhDestroy(link->dh);
secDhDestroy(link->dh); link->dh = 0;
link->dh = 0;
}
link->dh = secDhCreate(); link->dh = secDhCreate();
if (!link->dh) { if (!link->dh) {
@ -285,8 +304,21 @@ int secLinkHandshake(SecLinkT *link) {
return SECLINK_ERR_HANDSHAKE; return SECLINK_ERR_HANDSHAKE;
} }
// Poll until the callback completes the handshake (or signals an error) // Poll until the callback completes the handshake (or signals an error).
// The wait is bounded: pktPoll only reports PKT_ERR_DISCONNECTED for a
// closed port, never for an open but silent line, so without a timeout
// a missing peer would hang the caller forever.
start = clock();
timeout = (clock_t)HANDSHAKE_TIMEOUT_MS * CLOCKS_PER_SEC / 1000;
while (link->state != STATE_READY && link->state != STATE_ERROR) { while (link->state != STATE_READY && link->state != STATE_ERROR) {
if (clock() - start >= timeout) {
secDhDestroy(link->dh);
link->dh = 0;
link->state = STATE_INIT;
return SECLINK_ERR_TIMEOUT;
}
if (pktPoll(link->pkt) == PKT_ERR_DISCONNECTED) { if (pktPoll(link->pkt) == PKT_ERR_DISCONNECTED) {
secDhDestroy(link->dh); secDhDestroy(link->dh);
link->dh = 0; link->dh = 0;
@ -371,6 +403,15 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, b
return SECLINK_ERR_PARAM; return SECLINK_ERR_PARAM;
} }
// A blocking pktSend pumps pktPoll, whose receive callback may legally
// re-enter secLinkSend. A second encrypted frame must not reach the wire
// before the outer frame that was already encrypted -- the receiver
// decrypts in arrival order, so the reorder would desynchronize the
// XTEA-CTR keystream. Refuse the re-entrant encrypted send instead.
if (encrypt && link->txEncryptBusy) {
return SECLINK_ERR_SEND;
}
// CRITICAL: check window space BEFORE encrypting. If we encrypted first // CRITICAL: check window space BEFORE encrypting. If we encrypted first
// and then the send failed, the cipher counter would have advanced but // and then the send failed, the cipher counter would have advanced but
// the data wouldn't have been sent, permanently desynchronizing the // the data wouldn't have been sent, permanently desynchronizing the
@ -388,12 +429,19 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, b
// Copy payload after header // Copy payload after header
memcpy(buf + SECLINK_CHAN_HDR_SIZE, data, len); memcpy(buf + SECLINK_CHAN_HDR_SIZE, data, len);
// Encrypt the payload portion only (not the header) // Encrypt the payload portion only (not the header). The busy flag
// covers the window from counter advance until the frame is queued.
if (encrypt) { if (encrypt) {
link->txEncryptBusy = true;
secCipherCrypt(link->txCipher, buf + SECLINK_CHAN_HDR_SIZE, len); secCipherCrypt(link->txCipher, buf + SECLINK_CHAN_HDR_SIZE, len);
} }
rc = pktSend(link->pkt, buf, len + SECLINK_CHAN_HDR_SIZE, block); rc = pktSend(link->pkt, buf, len + SECLINK_CHAN_HDR_SIZE, block);
if (encrypt) {
link->txEncryptBusy = false;
}
if (rc != PKT_SUCCESS) { if (rc != PKT_SUCCESS) {
return SECLINK_ERR_SEND; return SECLINK_ERR_SEND;
} }

View file

@ -62,6 +62,7 @@
#define SECLINK_ERR_HANDSHAKE -4 #define SECLINK_ERR_HANDSHAKE -4
#define SECLINK_ERR_NOT_READY -5 #define SECLINK_ERR_NOT_READY -5
#define SECLINK_ERR_SEND -6 #define SECLINK_ERR_SEND -6
#define SECLINK_ERR_TIMEOUT -7
// Channel header is one byte: bit 7 = encrypted flag, bits 6..0 = channel // Channel header is one byte: bit 7 = encrypted flag, bits 6..0 = channel
#define SECLINK_CHAN_HDR_SIZE 1 #define SECLINK_CHAN_HDR_SIZE 1
@ -94,7 +95,9 @@ SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopB
void secLinkClose(SecLinkT *link); void secLinkClose(SecLinkT *link);
// Perform DH key exchange. Blocks until both sides have exchanged keys // Perform DH key exchange. Blocks until both sides have exchanged keys
// and derived cipher keys. RNG must be seeded before calling this. // and derived cipher keys, or until an internal timeout expires (returns
// SECLINK_ERR_TIMEOUT if the peer never responds). RNG must be seeded
// before calling this.
int secLinkHandshake(SecLinkT *link); int secLinkHandshake(SecLinkT *link);
// Get number of unacknowledged packets in the transmit window. // Get number of unacknowledged packets in the transmit window.
@ -111,6 +114,9 @@ int secLinkPoll(SecLinkT *link);
// sending (requires completed handshake). Clear packets can be sent // sending (requires completed handshake). Clear packets can be sent
// without a handshake. len must be 1..SECLINK_MAX_PAYLOAD. // without a handshake. len must be 1..SECLINK_MAX_PAYLOAD.
// If block is true, waits for transmit window space. // If block is true, waits for transmit window space.
// A blocking send pumps the receive callback; an encrypted send re-entered
// from that callback while an encrypted send is in progress is refused with
// SECLINK_ERR_SEND to keep the cipher streams in sync.
int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block); int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block);
// Send an arbitrarily large buffer by splitting it into SECLINK_MAX_PAYLOAD // Send an arbitrarily large buffer by splitting it into SECLINK_MAX_PAYLOAD

View file

@ -171,7 +171,7 @@ void secRngBytes(uint8_t *buf, int len);
int secRngGatherEntropy(uint8_t *buf, int len); int secRngGatherEntropy(uint8_t *buf, int len);
void secRngSeed(const uint8_t *entropy, int len); void secRngSeed(const uint8_t *entropy, int len);
static void secureZero(void *ptr, int len); static void secureZero(void *ptr, int len);
static void xteaCtrKeystream(uint32_t counter[2], const uint32_t key[4], uint8_t *out, int len, bool xorMode); static void xteaCtrKeystream(uint32_t counter[2], const uint32_t key[4], uint8_t *out, int32_t len, bool xorMode);
static void xteaEncryptBlock(uint32_t v[2], const uint32_t key[4]); static void xteaEncryptBlock(uint32_t v[2], const uint32_t key[4]);
@ -726,12 +726,12 @@ static void secureZero(void *ptr, int len) {
// counter in place. When xorMode is true the keystream is XOR'd into out // counter in place. When xorMode is true the keystream is XOR'd into out
// (cipher); when false it is copied (DRBG output). Shared by secCipherCrypt // (cipher); when false it is copied (DRBG output). Shared by secCipherCrypt
// and secRngBytes so the counter/keystream logic lives in one place. // and secRngBytes so the counter/keystream logic lives in one place.
static void xteaCtrKeystream(uint32_t counter[2], const uint32_t key[4], uint8_t *out, int len, bool xorMode) { static void xteaCtrKeystream(uint32_t counter[2], const uint32_t key[4], uint8_t *out, int32_t len, bool xorMode) {
uint32_t block[2]; uint32_t block[2];
int pos = 0; int32_t pos = 0;
while (pos < len) { while (pos < len) {
int take; int32_t take;
block[0] = counter[0]; block[0] = counter[0];
block[1] = counter[1]; block[1] = counter[1];
@ -746,7 +746,7 @@ static void xteaCtrKeystream(uint32_t counter[2], const uint32_t key[4], uint8_t
if (xorMode) { if (xorMode) {
const uint8_t *keystream = (const uint8_t *)block; const uint8_t *keystream = (const uint8_t *)block;
for (int i = 0; i < take; i++) { for (int32_t i = 0; i < take; i++) {
out[pos + i] ^= keystream[i]; out[pos + i] ^= keystream[i];
} }
} else { } else {

View file

@ -212,9 +212,9 @@ static void refreshTaskList(void) {
if (app && app->state == AppStateRunningE) { if (app && app->state == AppStateRunningE) {
TmRowStringsT row = {0}; TmRowStringsT row = {0};
// Copy the name into stable per-row storage too: the raw // Copy the name into the per-row storage alongside the derived
// app->name points into the reallocatable sApps array and would // columns so every cell pointer handed to the list view is backed
// dangle once another app load grows it. // by the same stable sRowStrs entry.
snprintf(row.name, sizeof(row.name), "%s", app->name); snprintf(row.name, sizeof(row.name), "%s", app->name);
for (int32_t w = 0; w < sCtx->stack.count; w++) { for (int32_t w = 0; w < sCtx->stack.count; w++) {

View file

@ -168,9 +168,10 @@ bool isWordChar(char c) {
// Nulls the last-selected tracker when its widget is being destroyed. // Nulls the last-selected tracker when its widget is being destroyed.
// Registered with libdvx's sWidgetDestroyFn hook so the static can never // Registered as a libdvx widget-destroy subscriber (widgetRegisterDestroyFn)
// dangle past a wgtDestroy (this lives in a different TU than // so the static can never dangle past a wgtDestroy (this lives in a
// widgetClearReferences, which cannot reach this static directly). // different TU than widgetClearReferences, which cannot reach this static
// directly).
void textEditClearWidgetRef(WidgetT *w) { void textEditClearWidgetRef(WidgetT *w) {
if (sLastSelectedWidget == w) { if (sLastSelectedWidget == w) {
sLastSelectedWidget = NULL; sLastSelectedWidget = NULL;
@ -990,8 +991,8 @@ int32_t textEditVisualColToOff(const char *buf, int32_t len, int32_t lineStart,
static void textHelpInit(void) { static void textHelpInit(void) {
sCursorBlinkFn = wgtUpdateCursorBlink; sCursorBlinkFn = wgtUpdateCursorBlink;
sWidgetDestroyFn = textEditClearWidgetRef; widgetRegisterDestroyFn(textEditClearWidgetRef);
} }
@ -1370,6 +1371,10 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
int32_t restoreOff = *pUndoCursor < *pLen ? *pUndoCursor : *pLen; int32_t restoreOff = *pUndoCursor < *pLen ? *pUndoCursor : *pLen;
*pUndoLen = saveLen; *pUndoLen = saveLen;
*pUndoCursor = tmpCursor; *pUndoCursor = tmpCursor;
// Buffer geometry changed; rebuild the line cache before
// converting the restore offset, otherwise the cursor maps
// to a stale row/col from the pre-undo buffer.
textEditLineCacheDirty(lc);
FROM_OFF(restoreOff); FROM_OFF(restoreOff);
*pDesiredCol = *pCol; *pDesiredCol = *pCol;
*pSA = -1; *pSA = -1;
@ -1761,6 +1766,10 @@ navigation: {
int32_t hi = SEL_HI(); int32_t hi = SEL_HI();
memmove(buf + lo, buf + hi, *pLen - hi + 1); memmove(buf + lo, buf + hi, *pLen - hi + 1);
*pLen -= (hi - lo); *pLen -= (hi - lo);
// Buffer geometry changed; rebuild the line cache before
// converting the offset and before the incremental insert
// notify below, otherwise it patches a stale line table.
textEditLineCacheDirty(lc);
FROM_OFF(lo); FROM_OFF(lo);
*pSA = -1; *pSA = -1;
*pSC = -1; *pSC = -1;
@ -2502,28 +2511,9 @@ void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorScheme
int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff) { int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff) {
(void)vertical; (void)vertical;
int32_t maxScroll = total - visible; int32_t maxScroll = total - visible;
int32_t trackLen = len - thick * 2;
if (maxScroll <= 0) {
return 0;
}
int32_t trackLen = len - thick * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, total, visible, 0, &thumbPos, &thumbSize);
int32_t rel = mouseCoord - sbStart - thick - dragOff; int32_t rel = mouseCoord - sbStart - thick - dragOff;
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * rel) / (trackLen - thumbSize) : 0; return widgetScrollbarThumbDragScroll(trackLen, total, visible, rel, maxScroll);
if (newScroll < 0) {
newScroll = 0;
}
if (newScroll > maxScroll) {
newScroll = maxScroll;
}
return newScroll;
} }

View file

@ -132,6 +132,7 @@ static void processHcfDir(const char *dirPath);
static void readDeps(ModuleT *mod); static void readDeps(ModuleT *mod);
static void scanDir(const char *dirPath, const char *ext, ModuleT **mods); static void scanDir(const char *dirPath, const char *ext, ModuleT **mods);
static void splashDrawScreen(void); static void splashDrawScreen(void);
static void splashShutdownIfActive(void);
static void splashUpdateProgress(void); static void splashUpdateProgress(void);
static void validateDeps(const ModuleT *libs, const ModuleT *widgets); static void validateDeps(const ModuleT *libs, const ModuleT *widgets);
int main(int argc, char *argv[]); int main(int argc, char *argv[]);
@ -567,8 +568,7 @@ static void loadInOrder(ModuleT *mods) {
if (!mods[i].handle) { if (!mods[i].handle) {
const char *err = dlerror(); const char *err = dlerror();
dvxLog(" FAILED: %s", err ? err : "(unknown)"); dvxLog(" FAILED: %s", err ? err : "(unknown)");
platformSplashShutdown(); splashShutdownIfActive();
sSplashActive = 0;
fprintf(stderr, "FATAL: Failed to load %s\n %s\n", mods[i].path, err ? err : "(unknown error)"); fprintf(stderr, "FATAL: Failed to load %s\n %s\n", mods[i].path, err ? err : "(unknown error)");
exit(1); exit(1);
} }
@ -640,14 +640,14 @@ static void logAndReadDeps(ModuleT *mods) {
int32_t pos = snprintf(line, sizeof(line), " %s deps:", mods[i].baseName); int32_t pos = snprintf(line, sizeof(line), " %s deps:", mods[i].baseName);
for (int32_t d = 0; d < arrlen(mods[i].deps); d++) { for (int32_t d = 0; d < arrlen(mods[i].deps); d++) {
// snprintf returns the would-be length on truncation; without // dvxStrAppendf bounds the accumulate and returns -1 on
// this guard pos can exceed sizeof(line) and make the size_t // truncation (or a prior failure), so a long dep list can
// 'sizeof(line) - pos' underflow into a huge write. // never overrun the fixed line buffer.
if (pos < 0 || (size_t)pos >= sizeof(line)) { pos = dvxStrAppendf(line, sizeof(line), pos, " %s", mods[i].deps[d]);
if (pos < 0) {
break; break;
} }
pos += snprintf(line + pos, sizeof(line) - (size_t)pos, " %s", mods[i].deps[d]);
} }
dvxLog("%s", line); dvxLog("%s", line);
@ -880,6 +880,14 @@ static void splashDrawScreen(void) {
} }
static void splashShutdownIfActive(void) {
if (sSplashActive) {
platformSplashShutdown();
sSplashActive = 0;
}
}
static void splashUpdateProgress(void) { static void splashUpdateProgress(void) {
if (!sSplashActive || sSplashTotal <= 0) { if (!sSplashActive || sSplashTotal <= 0) {
return; return;
@ -926,6 +934,12 @@ static void validateDeps(const ModuleT *libs, const ModuleT *widgets) {
if (!found) { if (!found) {
dvxLog("FATAL: %s %s requires missing dep: %s", kinds[pi], mods[i].baseName, depName); dvxLog("FATAL: %s %s requires missing dep: %s", kinds[pi], mods[i].baseName, depName);
// Restore text mode before the first print or the
// message prints invisibly under the graphics-mode
// splash.
splashShutdownIfActive();
fprintf(stderr, "FATAL: %s %s requires missing dep: %s\n", kinds[pi], mods[i].baseName, depName); fprintf(stderr, "FATAL: %s %s requires missing dep: %s\n", kinds[pi], mods[i].baseName, depName);
errors++; errors++;
} }
@ -934,15 +948,8 @@ static void validateDeps(const ModuleT *libs, const ModuleT *widgets) {
} }
if (errors > 0) { if (errors > 0) {
// The splash was already shut down when the first error printed.
fprintf(stderr, "%d unresolved dependency reference(s); aborting.\n", (int)errors); fprintf(stderr, "%d unresolved dependency reference(s); aborting.\n", (int)errors);
// Restore text mode first or the message prints invisibly under the
// graphics-mode splash.
if (sSplashActive) {
platformSplashShutdown();
sSplashActive = 0;
}
exit(1); exit(1);
} }
} }
@ -989,11 +996,7 @@ int main(int argc, char *argv[]) {
void **handles = loadAllModules(); void **handles = loadAllModules();
if (!handles || arrlen(handles) == 0) { if (!handles || arrlen(handles) == 0) {
if (sSplashActive) { splashShutdownIfActive();
platformSplashShutdown();
sSplashActive = 0;
}
fprintf(stderr, "No modules loaded from %s/ or %s/\n", LIBS_DIR, WIDGET_DIR); fprintf(stderr, "No modules loaded from %s/ or %s/\n", LIBS_DIR, WIDGET_DIR);
arrfree(handles); arrfree(handles);
return 1; return 1;
@ -1009,10 +1012,7 @@ int main(int argc, char *argv[]) {
if (!shellMain) { if (!shellMain) {
dvxLog("ERROR: No module exports shellMain"); dvxLog("ERROR: No module exports shellMain");
if (sSplashActive) { splashShutdownIfActive();
platformSplashShutdown();
sSplashActive = 0;
}
for (int32_t i = arrlen(handles) - 1; i >= 0; i--) { for (int32_t i = arrlen(handles) - 1; i >= 0; i--) {
dlclose(handles[i]); dlclose(handles[i]);

View file

@ -48,7 +48,7 @@ all: $(HOSTDIR)/dvxres $(HOSTDIR)/mkicon $(HOSTDIR)/mktbicon $(HOSTDIR)/mkwgtico
PLATFORM_UTIL = ../libs/kpunch/libdvx/platform/dvxPlatformUtil.c PLATFORM_UTIL = ../libs/kpunch/libdvx/platform/dvxPlatformUtil.c
STB_DS_IMPL = ../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c STB_DS_IMPL = ../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c
$(HOSTDIR)/dvxres: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(HOSTDIR) $(HOSTDIR)/dvxres: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h dvxResWrite.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(HOSTDIR)
$(HOSTCC) $(CFLAGS) -o $@ dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL) $(HOSTCC) $(CFLAGS) -o $@ dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
$(HOSTDIR)/mkicon: mkicon.c bmpDraw.c bmpDraw.h | $(HOSTDIR) $(HOSTDIR)/mkicon: mkicon.c bmpDraw.c bmpDraw.h | $(HOSTDIR)
@ -91,7 +91,7 @@ $(SYSTEMDIR)/DVXHLPC.EXE: dvxhlpc.c hlpcCompile.h ../apps/kpunch/dvxhelp/hlpform
../../obj/loader: ../../obj/loader:
mkdir -p ../../obj/loader mkdir -p ../../obj/loader
$(SYSTEMDIR)/DVXRES.EXE: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(SYSTEMDIR) $(SYSTEMDIR)/DVXRES.EXE: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h dvxResWrite.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(SYSTEMDIR)
$(DOSCC) $(DOSCFLAGS) -o $(SYSTEMDIR)/dvxres.exe dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL) $(DOSCC) $(DOSCFLAGS) -o $(SYSTEMDIR)/dvxres.exe dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
$(EXE2COFF) $(SYSTEMDIR)/dvxres.exe $(EXE2COFF) $(SYSTEMDIR)/dvxres.exe
cat $(CWSDSTUB) $(SYSTEMDIR)/dvxres > $@ cat $(CWSDSTUB) $(SYSTEMDIR)/dvxres > $@

View file

@ -37,6 +37,13 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h>
// Permission bits (setuid/setgid/sticky plus rwxrwxrwx) copied from the
// original file onto its replacement when a resource add rewrites through a
// temp-and-rename swap. Without this the fresh temp file keeps only the
// umask default and strips the executable bit off host tools.
#define DVX_RES_PERM_MASK 07777
// ============================================================ // ============================================================
// dvxResDxeContentSize // dvxResDxeContentSize
@ -207,7 +214,14 @@ static int dvxResWriteBlock(const char *path, long dxeSize, DvxResDirEntryT *ent
// leave a corrupt, unrecoverable .app/.wgt. // leave a corrupt, unrecoverable .app/.wgt.
char tmpPath[DVX_MAX_PATH]; char tmpPath[DVX_MAX_PATH];
snprintf(tmpPath, sizeof(tmpPath), "%s.tmp", path); int32_t tmpLen = snprintf(tmpPath, sizeof(tmpPath), "%s.tmp", path);
// A truncated temp path can collapse to the original path itself, and
// fopen(tmpPath, "wb") would then destroy the file the temp-file scheme
// exists to protect. Abort instead of writing anywhere unexpected.
if (tmpLen < 0 || (size_t)tmpLen >= sizeof(tmpPath)) {
return -1;
}
FILE *f = fopen(path, "rb"); FILE *f = fopen(path, "rb");
@ -215,6 +229,14 @@ static int dvxResWriteBlock(const char *path, long dxeSize, DvxResDirEntryT *ent
return -1; return -1;
} }
// Capture the original file's permission bits before the swap. A freshly
// created temp file is born with only the umask default, so without this
// the swap silently strips the executable bit off host tools like bascomp
// and leaves them unrunnable (exit 127) after a resource add.
struct stat srcStat;
bool haveSrcMode = (stat(path, &srcStat) == 0);
uint8_t *dxeBuf = (uint8_t *)malloc(dxeSize); uint8_t *dxeBuf = (uint8_t *)malloc(dxeSize);
if (!dxeBuf) { if (!dxeBuf) {
@ -282,15 +304,45 @@ static int dvxResWriteBlock(const char *path, long dxeSize, DvxResDirEntryT *ent
return -1; return -1;
} }
// DOS rename cannot overwrite an existing file, so the original must be // Carry the original file's permission bits onto the replacement so the
// removed first. The original survives every error path above. // atomic swap does not downgrade an executable to a plain data file.
remove(path); if (haveSrcMode) {
chmod(tmpPath, srcStat.st_mode & DVX_RES_PERM_MASK);
}
if (rename(tmpPath, path) != 0) { // DOS rename cannot overwrite an existing file, so the original is renamed
// aside to a backup rather than deleted outright. Only after the
// replacement is renamed into place is the backup removed; if the swap
// fails the original is restored, so it survives every error path.
char bakPath[DVX_MAX_PATH];
int32_t bakLen = snprintf(bakPath, sizeof(bakPath), "%s.bak", path);
if (bakLen < 0 || (size_t)bakLen >= sizeof(bakPath)) {
remove(tmpPath); remove(tmpPath);
return -1; return -1;
} }
// A stale backup from an earlier failed swap would block renaming the
// original aside.
remove(bakPath);
bool hadOriginal = (rename(path, bakPath) == 0);
if (rename(tmpPath, path) != 0) {
// Swap failed: restore the original and drop the written replacement.
if (hadOriginal) {
rename(bakPath, path);
}
remove(tmpPath);
return -1;
}
// The replacement is in place; the saved original is no longer needed.
if (hadOriginal) {
remove(bakPath);
}
return 0; return 0;
} }
@ -315,10 +367,10 @@ static int dvxResAppendEntry(const char *path, const char *name, uint32_t type,
return -1; return -1;
} }
DvxResDirEntryT *entries = NULL; DvxResDirEntryT *entries = NULL;
uint32_t count = 0; uint32_t count = 0;
uint8_t **data = NULL; uint8_t **data = NULL;
int readResult = dvxResReadExisting(path, dxeSize, &entries, &count, &data); int32_t readResult = dvxResReadExisting(path, dxeSize, &entries, &count, &data);
// A read error has already freed any partial allocations and reset the // A read error has already freed any partial allocations and reset the
// outputs; bail rather than truncate the file to only the new entry. // outputs; bail rather than truncate the file to only the new entry.

View file

@ -198,6 +198,7 @@ static int compareTrigrams(const void *a, const void *b);
static void emitError(const char *fmt, ...); static void emitError(const char *fmt, ...);
static int emitHtml(const char *outputPath); static int emitHtml(const char *outputPath);
static void emitWarning(const char *fmt, ...); static void emitWarning(const char *fmt, ...);
static void fatalOom(void);
static int32_t findImage(const char *filename); static int32_t findImage(const char *filename);
static void flushParagraph(TopicT *topic, char *para, int32_t paraLen, uint8_t type, uint8_t flags); static void flushParagraph(TopicT *topic, char *para, int32_t paraLen, uint8_t type, uint8_t flags);
static void freeAll(void); static void freeAll(void);
@ -230,7 +231,9 @@ static void addImageRef(const char *filename) {
if (imageCount >= imageCap) { if (imageCount >= imageCap) {
imageCap = imageCap ? imageCap * 2 : 16; imageCap = imageCap ? imageCap * 2 : 16;
imageRefs = realloc(imageRefs, sizeof(ImageRefT) * imageCap); imageRefs = realloc(imageRefs, sizeof(ImageRefT) * imageCap);
if (!imageRefs) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } if (!imageRefs) {
fatalOom();
}
} }
ImageRefT *img = &imageRefs[imageCount++]; ImageRefT *img = &imageRefs[imageCount++];
snprintf(img->path, sizeof(img->path), "%s/%s", imageDir, filename); snprintf(img->path, sizeof(img->path), "%s/%s", imageDir, filename);
@ -243,7 +246,9 @@ static void addIndexEntry(const char *keyword, int32_t topicIdx) {
if (indexCount >= indexCap) { if (indexCount >= indexCap) {
indexCap = indexCap ? indexCap * 2 : 64; indexCap = indexCap ? indexCap * 2 : 64;
indexEntries = realloc(indexEntries, sizeof(IndexEntryT) * indexCap); indexEntries = realloc(indexEntries, sizeof(IndexEntryT) * indexCap);
if (!indexEntries) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } if (!indexEntries) {
fatalOom();
}
} }
IndexEntryT *e = &indexEntries[indexCount++]; IndexEntryT *e = &indexEntries[indexCount++];
snprintf(e->keyword, sizeof(e->keyword), "%s", keyword); snprintf(e->keyword, sizeof(e->keyword), "%s", keyword);
@ -257,8 +262,7 @@ static RecordT *addRecord(TopicT *topic, uint8_t type, uint8_t flags, const char
topic->recordCap *= 2; topic->recordCap *= 2;
topic->records = realloc(topic->records, sizeof(RecordT) * topic->recordCap); topic->records = realloc(topic->records, sizeof(RecordT) * topic->recordCap);
if (!topic->records) { if (!topic->records) {
fprintf(stderr, "fatal: out of memory\n"); fatalOom();
exit(1);
} }
} }
RecordT *r = &topic->records[topic->recordCount++]; RecordT *r = &topic->records[topic->recordCount++];
@ -267,7 +271,9 @@ static RecordT *addRecord(TopicT *topic, uint8_t type, uint8_t flags, const char
r->dataLen = dataLen; r->dataLen = dataLen;
if (data && dataLen > 0) { if (data && dataLen > 0) {
r->data = malloc(dataLen + 1); r->data = malloc(dataLen + 1);
if (!r->data) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } if (!r->data) {
fatalOom();
}
memcpy(r->data, data, dataLen); memcpy(r->data, data, dataLen);
r->data[dataLen] = '\0'; r->data[dataLen] = '\0';
} else { } else {
@ -282,7 +288,9 @@ static void addTocEntry(const char *title, int32_t topicIdx, int32_t depth) {
if (tocCount >= tocCap) { if (tocCount >= tocCap) {
tocCap = tocCap ? tocCap * 2 : 32; tocCap = tocCap ? tocCap * 2 : 32;
tocEntries = realloc(tocEntries, sizeof(TocEntryT) * tocCap); tocEntries = realloc(tocEntries, sizeof(TocEntryT) * tocCap);
if (!tocEntries) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } if (!tocEntries) {
fatalOom();
}
} }
TocEntryT *e = &tocEntries[tocCount++]; TocEntryT *e = &tocEntries[tocCount++];
snprintf(e->title, sizeof(e->title), "%s", title); snprintf(e->title, sizeof(e->title), "%s", title);
@ -297,7 +305,9 @@ static TopicT *addTopic(const char *id) {
if (topicCount >= topicCap) { if (topicCount >= topicCap) {
topicCap = topicCap ? topicCap * 2 : 32; topicCap = topicCap ? topicCap * 2 : 32;
topics = realloc(topics, sizeof(TopicT) * topicCap); topics = realloc(topics, sizeof(TopicT) * topicCap);
if (!topics) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } if (!topics) {
fatalOom();
}
} }
TopicT *t = &topics[topicCount++]; TopicT *t = &topics[topicCount++];
memset(t, 0, sizeof(*t)); memset(t, 0, sizeof(*t));
@ -306,8 +316,7 @@ static TopicT *addTopic(const char *id) {
t->recordCap = 32; t->recordCap = 32;
t->records = malloc(sizeof(RecordT) * t->recordCap); t->records = malloc(sizeof(RecordT) * t->recordCap);
if (!t->records) { if (!t->records) {
fprintf(stderr, "fatal: out of memory\n"); fatalOom();
exit(1);
} }
return t; return t;
} }
@ -327,7 +336,9 @@ static void addTrigram(uint8_t a, uint8_t b, uint8_t c, uint16_t topicIdx) {
if (trigramCount >= trigramCap) { if (trigramCount >= trigramCap) {
trigramCap = trigramCap ? trigramCap * 2 : 1024; trigramCap = trigramCap ? trigramCap * 2 : 1024;
trigrams = realloc(trigrams, sizeof(TrigramT) * trigramCap); trigrams = realloc(trigrams, sizeof(TrigramT) * trigramCap);
if (!trigrams) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } if (!trigrams) {
fatalOom();
}
} }
tri = &trigrams[trigramCount++]; tri = &trigrams[trigramCount++];
tri->trigram[0] = a; tri->trigram[0] = a;
@ -336,7 +347,9 @@ static void addTrigram(uint8_t a, uint8_t b, uint8_t c, uint16_t topicIdx) {
tri->postingCap = 8; tri->postingCap = 8;
tri->postingCount = 0; tri->postingCount = 0;
tri->postings = malloc(sizeof(uint16_t) * tri->postingCap); tri->postings = malloc(sizeof(uint16_t) * tri->postingCap);
if (!tri->postings) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } if (!tri->postings) {
fatalOom();
}
} }
// Topics are processed in ascending order (buildSearchIndex), so a repeat // Topics are processed in ascending order (buildSearchIndex), so a repeat
@ -349,7 +362,9 @@ static void addTrigram(uint8_t a, uint8_t b, uint8_t c, uint16_t topicIdx) {
if (tri->postingCount >= tri->postingCap) { if (tri->postingCount >= tri->postingCap) {
tri->postingCap *= 2; tri->postingCap *= 2;
tri->postings = realloc(tri->postings, sizeof(uint16_t) * tri->postingCap); tri->postings = realloc(tri->postings, sizeof(uint16_t) * tri->postingCap);
if (!tri->postings) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } if (!tri->postings) {
fatalOom();
}
} }
tri->postings[tri->postingCount++] = topicIdx; tri->postings[tri->postingCount++] = topicIdx;
} }
@ -400,8 +415,7 @@ static void bufAppend(BufT *buf, const void *data, int32_t len) {
buf->cap *= 2; buf->cap *= 2;
buf->data = realloc(buf->data, buf->cap); buf->data = realloc(buf->data, buf->cap);
if (!buf->data) { if (!buf->data) {
fprintf(stderr, "fatal: out of memory\n"); fatalOom();
exit(1);
} }
} }
memcpy(buf->data + buf->size, data, len); memcpy(buf->data + buf->size, data, len);
@ -414,8 +428,7 @@ static void bufInit(BufT *buf) {
buf->size = 0; buf->size = 0;
buf->data = malloc(buf->cap); buf->data = malloc(buf->cap);
if (!buf->data) { if (!buf->data) {
fprintf(stderr, "fatal: out of memory\n"); fatalOom();
exit(1);
} }
} }
@ -601,6 +614,12 @@ static void emitWarning(const char *fmt, ...) {
} }
static void fatalOom(void) {
fprintf(stderr, "fatal: out of memory\n");
exit(1);
}
static int32_t findImage(const char *filename) { static int32_t findImage(const char *filename) {
for (int32_t i = 0; i < imageCount; i++) { for (int32_t i = 0; i < imageCount; i++) {
// Compare just the filename portion // Compare just the filename portion
@ -1110,6 +1129,8 @@ static void parseDirective(const char *line, TopicT **curTopic, bool *inList, bo
} else if (*inNote) { } else if (*inNote) {
flushType = HLP_REC_NOTE; flushType = HLP_REC_NOTE;
flushFlags = *noteFlags; flushFlags = *noteFlags;
} else if (*inList) {
flushType = HLP_REC_LIST_ITEM;
} }
flushParagraph(*curTopic, para, *paraLen, flushType, flushFlags); flushParagraph(*curTopic, para, *paraLen, flushType, flushFlags);
*paraLen = 0; *paraLen = 0;
@ -1481,7 +1502,9 @@ static int pass5Serialize(const char *outputPath) {
uint32_t *topicContentOffsets = calloc((size_t)allocCount, sizeof(uint32_t)); uint32_t *topicContentOffsets = calloc((size_t)allocCount, sizeof(uint32_t));
uint32_t *topicContentSizes = calloc((size_t)allocCount, sizeof(uint32_t)); uint32_t *topicContentSizes = calloc((size_t)allocCount, sizeof(uint32_t));
if (!topicContentOffsets || !topicContentSizes) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } if (!topicContentOffsets || !topicContentSizes) {
fatalOom();
}
for (int32_t t = 0; t < topicCount; t++) { for (int32_t t = 0; t < topicCount; t++) {
TopicT *topic = &topics[t]; TopicT *topic = &topics[t];
@ -1616,7 +1639,9 @@ static int pass5Serialize(const char *outputPath) {
// --- 7. Topic directory (sorted by topic ID) --- // --- 7. Topic directory (sorted by topic ID) ---
hdr.topicDirOffset = offset; hdr.topicDirOffset = offset;
HlpTopicDirT *topicDir = calloc(topicCount, sizeof(HlpTopicDirT)); HlpTopicDirT *topicDir = calloc(topicCount, sizeof(HlpTopicDirT));
if (!topicDir) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } if (!topicDir) {
fatalOom();
}
for (int32_t i = 0; i < topicCount; i++) { for (int32_t i = 0; i < topicCount; i++) {
topicDir[i].topicIdStr = strTableFind(topics[i].id); topicDir[i].topicIdStr = strTableFind(topics[i].id);
topicDir[i].titleStr = strTableFind(topics[i].title); topicDir[i].titleStr = strTableFind(topics[i].title);
@ -1704,8 +1729,7 @@ static void regroupTocBySections(void) {
int32_t newCap = tocCount + sectionCount; int32_t newCap = tocCount + sectionCount;
TocEntryT *newToc = malloc(sizeof(TocEntryT) * newCap); TocEntryT *newToc = malloc(sizeof(TocEntryT) * newCap);
if (!newToc) { if (!newToc) {
fprintf(stderr, "fatal: out of memory\n"); fatalOom();
exit(1);
} }
int32_t newCount = 0; int32_t newCount = 0;
@ -1739,7 +1763,9 @@ static void regroupTocBySections(void) {
if (newCount > tocCap) { if (newCount > tocCap) {
tocCap = newCount; tocCap = newCount;
tocEntries = realloc(tocEntries, sizeof(TocEntryT) * tocCap); tocEntries = realloc(tocEntries, sizeof(TocEntryT) * tocCap);
if (!tocEntries) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } if (!tocEntries) {
fatalOom();
}
} }
memcpy(tocEntries, newToc, sizeof(TocEntryT) * newCount); memcpy(tocEntries, newToc, sizeof(TocEntryT) * newCount);
tocCount = newCount; tocCount = newCount;
@ -1784,8 +1810,7 @@ static int32_t strTableAdd(const char *str) {
strTabCap *= 2; strTabCap *= 2;
strTab = realloc(strTab, strTabCap); strTab = realloc(strTab, strTabCap);
if (!strTab) { if (!strTab) {
fprintf(stderr, "fatal: out of memory\n"); fatalOom();
exit(1);
} }
} }
@ -1794,8 +1819,7 @@ static int32_t strTableAdd(const char *str) {
strEntryCap *= 2; strEntryCap *= 2;
strEntries = realloc(strEntries, sizeof(StrEntryT) * strEntryCap); strEntries = realloc(strEntries, sizeof(StrEntryT) * strEntryCap);
if (!strEntries) { if (!strEntries) {
fprintf(stderr, "fatal: out of memory\n"); fatalOom();
exit(1);
} }
} }

View file

@ -483,7 +483,13 @@ int main(int argc, char *argv[]) {
// Wait for ENTER from terminal before connecting to BBS // Wait for ENTER from terminal before connecting to BBS
printf("Waiting for terminal to send ENTER...\n"); printf("Waiting for terminal to send ENTER...\n");
while (sRunning) { while (sRunning) {
secLinkPoll(link); // Bail if the DOS client dropped, otherwise this loop polls a dead
// socket forever waiting for an ENTER that can never arrive.
if (secLinkPoll(link) == PKT_ERR_DISCONNECTED) {
printf("DOS client disconnected.\n");
sRunning = false;
break;
}
if (sGotEnter) { if (sGotEnter) {
break; break;
} }
@ -526,9 +532,15 @@ int main(int argc, char *argv[]) {
while (sRunning) { while (sRunning) {
poll(fds, 2, POLL_TIMEOUT_MS); poll(fds, 2, POLL_TIMEOUT_MS);
// Process incoming secLink packets from the DOS side // Process incoming secLink packets from the DOS side (callback
// (callback forwards decrypted data to BBS) // forwards decrypted data to BBS). A client FIN leaves the fd
secLinkPoll(link); // level-triggered readable without POLLERR/POLLHUP, so the revents
// check below never fires -- the disconnect must be caught here or
// the loop spins at full CPU forever.
if (secLinkPoll(link) == PKT_ERR_DISCONNECTED) {
printf("DOS client disconnected.\n");
break;
}
// Read from BBS, filter telnet, send clean data to the DOS side // Read from BBS, filter telnet, send clean data to the DOS side
if (fds[1].revents & POLLIN) { if (fds[1].revents & POLLIN) {

View file

@ -32,7 +32,7 @@
// 16-color CGA palette, blink attribute) // 16-color CGA palette, blink attribute)
// 2. Cell-based storage is extremely compact -- 2 bytes per cell means an // 2. Cell-based storage is extremely compact -- 2 bytes per cell means an
// 80x25 screen is only 4000 bytes, fitting in L1 cache on a 486 // 80x25 screen is only 4000 bytes, fitting in L1 cache on a 486
// 3. Dirty-row tracking via a 32-bit bitmask allows sub-millisecond // 3. Dirty-row tracking via a per-row bitmask allows sub-millisecond
// incremental repaints without scanning the entire buffer // incremental repaints without scanning the entire buffer
// //
// The ANSI parser is a 3-state machine (NORMAL -> ESC -> CSI) that handles // The ANSI parser is a 3-state machine (NORMAL -> ESC -> CSI) that handles
@ -80,11 +80,20 @@ static int32_t sTypeId = -1;
#define BLINK_MS 500 #define BLINK_MS 500
#define CURSOR_MS 250 #define CURSOR_MS 250
// dirtyRows is a 32-bit bitmask, so ANSI_MAX_DIRTY_ROWS is fixed at 32. // dirtyRows is a multi-word bitmask sized at ANSI_DIRTY_WORDS(rows)
// Rows beyond this still render correctly -- they just always repaint // uint32_t words, so terminals taller than 32 rows keep per-row dirty
// because we can't track their dirty state in the mask. // tracking instead of losing updates for rows past bit 31.
#define ANSI_MAX_DIRTY_ROWS 32 #define ANSI_DIRTY_WORD_BITS 32
#define ANSI_DIRTY_ALL_ROWS 0xFFFFFFFF #define ANSI_DIRTY_WORD_ALL 0xFFFFFFFFU
#define ANSI_DIRTY_WORDS(rows) (((rows) + ANSI_DIRTY_WORD_BITS - 1) / ANSI_DIRTY_WORD_BITS)
// Geometry and scrollback caps. wgtAnsiTerm and wgtAnsiTermSetScrollback
// are public API; without these caps cols * rows * 2 (or maxLines * cols
// * 2) can overflow int32_t and wrap to a tiny allocation that cursor
// writes and scroll memmoves then overrun.
#define ANSI_MAX_COLS 512
#define ANSI_MAX_ROWS 512
#define ANSI_MAX_SCROLLBACK 32768
// Clipboard buffer size for selection copy. // Clipboard buffer size for selection copy.
#define ANSI_CLIPBOARD_BUF 4096 #define ANSI_CLIPBOARD_BUF 4096
@ -109,7 +118,7 @@ typedef struct {
int32_t scrollPos; int32_t scrollPos;
clock_t blinkTime; clock_t blinkTime;
clock_t cursorTime; clock_t cursorTime;
uint32_t dirtyRows; uint32_t *dirtyRows; // ANSI_DIRTY_WORDS(rows) words, one bit per row
int32_t lastCursorRow; int32_t lastCursorRow;
int32_t lastCursorCol; int32_t lastCursorCol;
uint32_t packedPalette[16]; uint32_t packedPalette[16];
@ -152,8 +161,12 @@ static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d);
static void ansiTermClearSelection(WidgetT *w); static void ansiTermClearSelection(WidgetT *w);
static void ansiTermCopySelection(WidgetT *w); static void ansiTermCopySelection(WidgetT *w);
static void ansiTermDeleteLines(WidgetT *w, int32_t count); static void ansiTermDeleteLines(WidgetT *w, int32_t count);
static void ansiTermDirtyAll(WidgetT *w);
static bool ansiTermDirtyAny(const WidgetT *w);
static void ansiTermDirtyClear(WidgetT *w);
static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count); static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count);
static void ansiTermDirtyRow(WidgetT *w, int32_t row); static void ansiTermDirtyRow(WidgetT *w, int32_t row);
static bool ansiTermDirtyTest(const WidgetT *w, int32_t row);
static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd); static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd);
static void ansiTermEraseDisplay(WidgetT *w, int32_t mode); static void ansiTermEraseDisplay(WidgetT *w, int32_t mode);
static void ansiTermEraseLine(WidgetT *w, int32_t mode); static void ansiTermEraseLine(WidgetT *w, int32_t mode);
@ -216,7 +229,7 @@ static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) {
static void ansiTermClearSelection(WidgetT *w) { static void ansiTermClearSelection(WidgetT *w) {
AnsiTermDataT *at = (AnsiTermDataT *)w->data; AnsiTermDataT *at = (AnsiTermDataT *)w->data;
if (ansiTermHasSelection(w)) { at->dirtyRows = ANSI_DIRTY_ALL_ROWS; } if (ansiTermHasSelection(w)) { ansiTermDirtyAll(w); }
at->selStartLine = -1; at->selStartLine = -1;
at->selStartCol = -1; at->selStartCol = -1;
at->selEndLine = -1; at->selEndLine = -1;
@ -266,23 +279,60 @@ static void ansiTermDeleteLines(WidgetT *w, int32_t count) {
} }
static void ansiTermDirtyAll(WidgetT *w) {
AnsiTermDataT *at = (AnsiTermDataT *)w->data;
int32_t words = ANSI_DIRTY_WORDS(at->rows);
for (int32_t i = 0; i < words; i++) {
at->dirtyRows[i] = ANSI_DIRTY_WORD_ALL;
}
}
static bool ansiTermDirtyAny(const WidgetT *w) {
const AnsiTermDataT *at = (const AnsiTermDataT *)w->data;
int32_t words = ANSI_DIRTY_WORDS(at->rows);
for (int32_t i = 0; i < words; i++) {
if (at->dirtyRows[i] != 0) {
return true;
}
}
return false;
}
static void ansiTermDirtyClear(WidgetT *w) {
AnsiTermDataT *at = (AnsiTermDataT *)w->data;
int32_t words = ANSI_DIRTY_WORDS(at->rows);
for (int32_t i = 0; i < words; i++) {
at->dirtyRows[i] = 0;
}
}
static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) { static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) {
AnsiTermDataT *at = (AnsiTermDataT *)w->data; AnsiTermDataT *at = (AnsiTermDataT *)w->data;
int32_t startRow = startCell / at->cols; int32_t startRow = startCell / at->cols;
int32_t endRow = (startCell + count - 1) / at->cols; int32_t endRow = (startCell + count - 1) / at->cols;
for (int32_t r = startRow; r <= endRow && r < ANSI_MAX_DIRTY_ROWS; r++) { for (int32_t r = startRow; r <= endRow; r++) {
at->dirtyRows |= (1U << r); ansiTermDirtyRow(w, r);
} }
} }
static void ansiTermDirtyRow(WidgetT *w, int32_t row) { static void ansiTermDirtyRow(WidgetT *w, int32_t row) {
if (row >= 0 && row < ANSI_MAX_DIRTY_ROWS) { AnsiTermDataT *at = (AnsiTermDataT *)w->data;
((AnsiTermDataT *)w->data)->dirtyRows |= (1U << row); if (row >= 0 && row < at->rows) {
at->dirtyRows[row / ANSI_DIRTY_WORD_BITS] |= (1U << (row % ANSI_DIRTY_WORD_BITS));
} }
} }
static bool ansiTermDirtyTest(const WidgetT *w, int32_t row) {
const AnsiTermDataT *at = (const AnsiTermDataT *)w->data;
return (at->dirtyRows[row / ANSI_DIRTY_WORD_BITS] & (1U << (row % ANSI_DIRTY_WORD_BITS))) != 0;
}
static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
AnsiTermDataT *at = (AnsiTermDataT *)w->data; AnsiTermDataT *at = (AnsiTermDataT *)w->data;
int32_t *p = at->params; int32_t *p = at->params;
@ -683,7 +733,7 @@ static void ansiTermScrollDown(WidgetT *w) {
int32_t bytesPerRow = cols * 2; int32_t bytesPerRow = cols * 2;
if (bot > top) { memmove(at->cells + (top + 1) * bytesPerRow, at->cells + top * bytesPerRow, (bot - top) * bytesPerRow); } if (bot > top) { memmove(at->cells + (top + 1) * bytesPerRow, at->cells + top * bytesPerRow, (bot - top) * bytesPerRow); }
ansiTermFillCells(w, top * cols, cols); ansiTermFillCells(w, top * cols, cols);
for (int32_t r = top; r <= bot && r < ANSI_MAX_DIRTY_ROWS; r++) { at->dirtyRows |= (1U << r); } for (int32_t r = top; r <= bot; r++) { ansiTermDirtyRow(w, r); }
} }
@ -700,7 +750,7 @@ static void ansiTermScrollUp(WidgetT *w) {
} }
if (bot > top) { memmove(at->cells + top * bytesPerRow, at->cells + (top + 1) * bytesPerRow, (bot - top) * bytesPerRow); } if (bot > top) { memmove(at->cells + top * bytesPerRow, at->cells + (top + 1) * bytesPerRow, (bot - top) * bytesPerRow); }
ansiTermFillCells(w, bot * cols, cols); ansiTermFillCells(w, bot * cols, cols);
for (int32_t r = top; r <= bot && r < ANSI_MAX_DIRTY_ROWS; r++) { at->dirtyRows |= (1U << r); } for (int32_t r = top; r <= bot; r++) { ansiTermDirtyRow(w, r); }
} }
@ -717,6 +767,8 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) {
if (!parent) { return NULL; } if (!parent) { return NULL; }
if (cols <= 0) { cols = 80; } if (cols <= 0) { cols = 80; }
if (rows <= 0) { rows = 25; } if (rows <= 0) { rows = 25; }
if (cols > ANSI_MAX_COLS) { cols = ANSI_MAX_COLS; }
if (rows > ANSI_MAX_ROWS) { rows = ANSI_MAX_ROWS; }
WidgetT *w = widgetAlloc(parent, sTypeId); WidgetT *w = widgetAlloc(parent, sTypeId);
if (!w) { return NULL; } if (!w) { return NULL; }
// Use wgtDestroy (not bare free) on every failure path: widgetAlloc // Use wgtDestroy (not bare free) on every failure path: widgetAlloc
@ -724,14 +776,28 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) {
// freeing it directly leaves dangling pointers there. The destroy // freeing it directly leaves dangling pointers there. The destroy
// method is NULL-safe and frees any partially-allocated data. // method is NULL-safe and frees any partially-allocated data.
AnsiTermDataT *at = (AnsiTermDataT *)calloc(1, sizeof(AnsiTermDataT)); AnsiTermDataT *at = (AnsiTermDataT *)calloc(1, sizeof(AnsiTermDataT));
if (!at) { wgtDestroy(w); return NULL; } if (!at) {
wgtDestroy(w);
return NULL;
}
w->data = at; w->data = at;
int32_t cellCount = cols * rows; int32_t cellCount = cols * rows;
at->cells = (uint8_t *)malloc(cellCount * 2); at->cells = (uint8_t *)malloc(cellCount * 2);
if (!at->cells) { wgtDestroy(w); return NULL; } if (!at->cells) {
wgtDestroy(w);
return NULL;
}
int32_t sbMax = ANSI_DEFAULT_SCROLLBACK; int32_t sbMax = ANSI_DEFAULT_SCROLLBACK;
at->scrollback = (uint8_t *)malloc(sbMax * cols * 2); at->scrollback = (uint8_t *)malloc(sbMax * cols * 2);
if (!at->scrollback) { wgtDestroy(w); return NULL; } if (!at->scrollback) {
wgtDestroy(w);
return NULL;
}
at->dirtyRows = (uint32_t *)malloc(ANSI_DIRTY_WORDS(rows) * sizeof(uint32_t));
if (!at->dirtyRows) {
wgtDestroy(w);
return NULL;
}
at->cols = cols; at->rows = rows; at->cols = cols; at->rows = rows;
at->cursorVisible = true; at->wrapMode = true; at->cursorVisible = true; at->wrapMode = true;
at->curAttr = ANSI_DEFAULT_ATTR; at->parseState = PARSE_NORMAL; at->curAttr = ANSI_DEFAULT_ATTR; at->parseState = PARSE_NORMAL;
@ -740,7 +806,7 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) {
at->selEndLine = -1; at->selEndCol = -1; at->selEndLine = -1; at->selEndCol = -1;
at->blinkVisible = true; at->blinkTime = clock(); at->blinkVisible = true; at->blinkTime = clock();
at->cursorOn = true; at->cursorTime = clock(); at->cursorOn = true; at->cursorTime = clock();
at->dirtyRows = ANSI_DIRTY_ALL_ROWS; at->lastCursorRow = -1; at->lastCursorCol = -1; ansiTermDirtyAll(w); at->lastCursorRow = -1; at->lastCursorCol = -1;
for (int32_t i = 0; i < cellCount; i++) { at->cells[i * 2] = ' '; at->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; } for (int32_t i = 0; i < cellCount; i++) { at->cells[i * 2] = ' '; at->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; }
return w; return w;
} }
@ -760,7 +826,7 @@ void wgtAnsiTermClear(WidgetT *w) {
at->bold = false; at->parseState = PARSE_NORMAL; at->bold = false; at->parseState = PARSE_NORMAL;
// Mark every row dirty or the row-skipping paint leaves stale cells // Mark every row dirty or the row-skipping paint leaves stale cells
// until each happens to be re-dirtied by later output. // until each happens to be re-dirtied by later output.
at->dirtyRows = ANSI_DIRTY_ALL_ROWS; ansiTermDirtyAll(w);
} }
@ -796,10 +862,10 @@ int32_t wgtAnsiTermPoll(WidgetT *w) {
if (at->hasBlinkCells) { if (at->hasBlinkCells) {
int32_t cols = at->cols; int32_t rows = at->rows; int32_t cols = at->cols; int32_t rows = at->rows;
bool anyBlink = false; bool anyBlink = false;
for (int32_t row = 0; row < rows && row < ANSI_MAX_DIRTY_ROWS; row++) { for (int32_t row = 0; row < rows; row++) {
for (int32_t col = 0; col < cols; col++) { for (int32_t col = 0; col < cols; col++) {
if (at->cells[(row * cols + col) * 2 + 1] & ATTR_BLINK_BIT) { if (at->cells[(row * cols + col) * 2 + 1] & ATTR_BLINK_BIT) {
at->dirtyRows |= (1U << row); ansiTermDirtyRow(w, row);
anyBlink = true; anyBlink = true;
break; break;
} }
@ -814,8 +880,7 @@ int32_t wgtAnsiTermPoll(WidgetT *w) {
} }
if ((now - at->cursorTime) >= curInterval) { if ((now - at->cursorTime) >= curInterval) {
at->cursorTime = now; at->cursorOn = !at->cursorOn; at->cursorTime = now; at->cursorOn = !at->cursorOn;
int32_t cRow = at->cursorRow; ansiTermDirtyRow(w, at->cursorRow);
if (cRow >= 0 && cRow < ANSI_MAX_DIRTY_ROWS) { at->dirtyRows |= (1U << cRow); }
} }
if (!at->commRead) { return 0; } if (!at->commRead) { return 0; }
uint8_t buf[256]; uint8_t buf[256];
@ -831,11 +896,10 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
int32_t prevRow = at->lastCursorRow; int32_t prevRow = at->lastCursorRow;
int32_t curRow = at->cursorRow; int32_t curRow = at->cursorRow;
if (prevRow != curRow || at->lastCursorCol != at->cursorCol) { if (prevRow != curRow || at->lastCursorCol != at->cursorCol) {
if (prevRow >= 0 && prevRow < ANSI_MAX_DIRTY_ROWS) { at->dirtyRows |= (1U << prevRow); } ansiTermDirtyRow(w, prevRow);
if (curRow >= 0 && curRow < ANSI_MAX_DIRTY_ROWS) { at->dirtyRows |= (1U << curRow); } ansiTermDirtyRow(w, curRow);
} }
uint32_t dirty = at->dirtyRows; if (!ansiTermDirtyAny(w)) { return 0; }
if (dirty == 0) { return 0; }
WindowT *win = w->window; WindowT *win = w->window;
if (!win->contentBuf || !win->widgetRoot) { return 0; } if (!win->contentBuf || !win->widgetRoot) { return 0; }
AppContextT *ctx = wgtGetContext(win->widgetRoot); AppContextT *ctx = wgtGetContext(win->widgetRoot);
@ -853,10 +917,7 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
bool viewingLive = (at->scrollPos == at->scrollbackCount); bool viewingLive = (at->scrollPos == at->scrollbackCount);
int32_t repainted = 0; int32_t minRow = rows; int32_t maxRow = -1; int32_t repainted = 0; int32_t minRow = rows; int32_t maxRow = -1;
for (int32_t row = 0; row < rows; row++) { for (int32_t row = 0; row < rows; row++) {
// Rows >= 32 carry no dirty bit (dirtyRows is a 32-bit mask), so if (!ansiTermDirtyTest(w, row)) { continue; }
// force-repaint them whenever the function runs; the dirty==0 guard
// above still short-circuits the fully-idle case.
if (row < ANSI_MAX_DIRTY_ROWS && !(dirty & (1U << row))) { continue; }
int32_t lineIndex = at->scrollPos + row; int32_t lineIndex = at->scrollPos + row;
const uint8_t *lineData = ansiTermGetLine(w, lineIndex); const uint8_t *lineData = ansiTermGetLine(w, lineIndex);
int32_t curCol2 = -1; int32_t curCol2 = -1;
@ -867,7 +928,7 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
if (row > maxRow) { maxRow = row; } if (row > maxRow) { maxRow = row; }
repainted++; repainted++;
} }
at->dirtyRows = 0; at->lastCursorRow = at->cursorRow; at->lastCursorCol = at->cursorCol; ansiTermDirtyClear(w); at->lastCursorRow = at->cursorRow; at->lastCursorCol = at->cursorCol;
if (outY) { *outY = baseY + minRow * cellH; } if (outY) { *outY = baseY + minRow * cellH; }
if (outH) { *outH = (maxRow - minRow + 1) * cellH; } if (outH) { *outH = (maxRow - minRow + 1) * cellH; }
return repainted; return repainted;
@ -883,6 +944,9 @@ void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t
void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) { void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) {
if (!w || w->type != sTypeId || maxLines <= 0) { return; } if (!w || w->type != sTypeId || maxLines <= 0) { return; }
// Cap the line count so maxLines * cols * 2 cannot overflow int32_t
// (cols is already capped at ANSI_MAX_COLS on creation).
if (maxLines > ANSI_MAX_SCROLLBACK) { maxLines = ANSI_MAX_SCROLLBACK; }
AnsiTermDataT *at = (AnsiTermDataT *)w->data; AnsiTermDataT *at = (AnsiTermDataT *)w->data;
int32_t cols = at->cols; int32_t cols = at->cols;
uint8_t *newBuf = (uint8_t *)malloc(maxLines * cols * 2); uint8_t *newBuf = (uint8_t *)malloc(maxLines * cols * 2);
@ -924,7 +988,7 @@ static bool widgetAnsiTermClearSelection(WidgetT *w) {
at->selStartCol = -1; at->selStartCol = -1;
at->selEndLine = -1; at->selEndLine = -1;
at->selEndCol = -1; at->selEndCol = -1;
at->dirtyRows = ANSI_DIRTY_ALL_ROWS; ansiTermDirtyAll(w);
return true; return true;
} }
@ -934,7 +998,7 @@ static bool widgetAnsiTermClearSelection(WidgetT *w) {
void widgetAnsiTermDestroy(WidgetT *w) { void widgetAnsiTermDestroy(WidgetT *w) {
AnsiTermDataT *at = (AnsiTermDataT *)w->data; AnsiTermDataT *at = (AnsiTermDataT *)w->data;
if (at) { free(at->cells); free(at->scrollback); free(at); w->data = NULL; } if (at) { free(at->cells); free(at->scrollback); free(at->dirtyRows); free(at); w->data = NULL; }
} }
@ -968,7 +1032,7 @@ static void widgetAnsiTermOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, in
int32_t lineIndex = at->scrollPos + row; int32_t lineIndex = at->scrollPos + row;
at->selEndLine = lineIndex; at->selEndLine = lineIndex;
at->selEndCol = col; at->selEndCol = col;
at->dirtyRows = ANSI_DIRTY_ALL_ROWS; ansiTermDirtyAll(w);
} }
@ -977,7 +1041,11 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (key == KEY_CTRL_C && (mod & KEY_MOD_CTRL)) { if (key == KEY_CTRL_C && (mod & KEY_MOD_CTRL)) {
if (ansiTermHasSelection(w)) { ansiTermCopySelection(w); ansiTermClearSelection(w); wgtInvalidatePaint(w); return; } if (ansiTermHasSelection(w)) { ansiTermCopySelection(w); ansiTermClearSelection(w); wgtInvalidatePaint(w); return; }
} }
if (key == KEY_CTRL_V && (mod & KEY_MOD_CTRL)) { ansiTermPasteToComm(w); wgtInvalidatePaint(w); return; } if (key == KEY_CTRL_V && (mod & KEY_MOD_CTRL)) {
ansiTermPasteToComm(w);
wgtInvalidatePaint(w);
return;
}
if (!at->commWrite) { return; } if (!at->commWrite) { return; }
if (ansiTermHasSelection(w)) { ansiTermClearSelection(w); } if (ansiTermHasSelection(w)) { ansiTermClearSelection(w); }
uint8_t buf[8]; uint8_t buf[8];
@ -1043,7 +1111,7 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
at->selEndLine = lineIndex; at->selEndCol = clickCol; at->selEndLine = lineIndex; at->selEndCol = clickCol;
sDragWidget = hit; sDragWidget = hit;
} }
at->dirtyRows = ANSI_DIRTY_ALL_ROWS; ansiTermDirtyAll(hit);
return; return;
} }
@ -1068,15 +1136,17 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
if (at->scrollPos > maxScroll) { at->scrollPos = maxScroll; } if (at->scrollPos > maxScroll) { at->scrollPos = maxScroll; }
// scrollPos changed every visible row's content; mark all dirty or // scrollPos changed every visible row's content; mark all dirty or
// the row-skipping paint shows a stale mixed display. // the row-skipping paint shows a stale mixed display.
at->dirtyRows = ANSI_DIRTY_ALL_ROWS; ansiTermDirtyAll(hit);
} }
void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
AnsiTermDataT *at = (AnsiTermDataT *)w->data; AnsiTermDataT *at = (AnsiTermDataT *)w->data;
BevelStyleT bevel; BevelStyleT bevel;
bevel.highlight = colors->windowShadow; bevel.shadow = colors->windowHighlight; bevel.highlight = colors->windowShadow;
bevel.face = BEVEL_NO_FILL; bevel.width = ANSI_BORDER; bevel.shadow = colors->windowHighlight;
bevel.face = BEVEL_NO_FILL;
bevel.width = ANSI_BORDER;
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
ansiTermBuildPalette(w, d); ansiTermBuildPalette(w, d);
const uint32_t *palette = at->packedPalette; const uint32_t *palette = at->packedPalette;
@ -1085,10 +1155,10 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
int32_t baseX = w->x + ANSI_BORDER; int32_t baseY = w->y + ANSI_BORDER; int32_t baseX = w->x + ANSI_BORDER; int32_t baseY = w->y + ANSI_BORDER;
int32_t sbCount = at->scrollbackCount; int32_t sbCount = at->scrollbackCount;
bool viewingLive = (at->scrollPos == sbCount); bool viewingLive = (at->scrollPos == sbCount);
uint32_t dirty = at->dirtyRows; // A full paint with nothing marked dirty is an expose; repaint all rows.
if (dirty == 0) { dirty = ANSI_DIRTY_ALL_ROWS; } bool paintAll = !ansiTermDirtyAny(w);
for (int32_t row = 0; row < rows; row++) { for (int32_t row = 0; row < rows; row++) {
if (row < ANSI_MAX_DIRTY_ROWS && !(dirty & (1U << row))) { continue; } if (!paintAll && !ansiTermDirtyTest(w, row)) { continue; }
int32_t lineIndex = at->scrollPos + row; int32_t lineIndex = at->scrollPos + row;
const uint8_t *lineData = ansiTermGetLine(w, lineIndex); const uint8_t *lineData = ansiTermGetLine(w, lineIndex);
int32_t curCol = -1; int32_t curCol = -1;
@ -1096,7 +1166,7 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
drawTermRow(d, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, at->blinkVisible, curCol); drawTermRow(d, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, at->blinkVisible, curCol);
ansiTermPaintSelRow(w, d, ops, font, row, baseX, baseY); ansiTermPaintSelRow(w, d, ops, font, row, baseX, baseY);
} }
at->dirtyRows = 0; ansiTermDirtyClear(w);
// Scrollbar // Scrollbar
int32_t sbX = baseX + cols * cellW; int32_t sbY = baseY; int32_t sbX = baseX + cols * cellW; int32_t sbY = baseY;

View file

@ -75,6 +75,7 @@ static void basFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t
static uint32_t basGetPixel(const WidgetT *w, int32_t x, int32_t y); static uint32_t basGetPixel(const WidgetT *w, int32_t x, int32_t y);
static void basSetPenColor(WidgetT *w, int32_t color); static void basSetPenColor(WidgetT *w, int32_t color);
static void basSetPixel(WidgetT *w, int32_t x, int32_t y, int32_t color); static void basSetPixel(WidgetT *w, int32_t x, int32_t y, int32_t color);
static int32_t canvasBufferBytes(int32_t w, int32_t h, int32_t bpp);
static void canvasDrawDot(CanvasDataT *cd, int32_t cx, int32_t cy); static void canvasDrawDot(CanvasDataT *cd, int32_t cx, int32_t cy);
static void canvasDrawLine(CanvasDataT *cd, int32_t x0, int32_t y0, int32_t x1, int32_t y1); static void canvasDrawLine(CanvasDataT *cd, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
static void canvasFillCircleSpans(CanvasDataT *cd, int32_t cx, int32_t cy, int32_t radius); static void canvasFillCircleSpans(CanvasDataT *cd, int32_t cx, int32_t cy, int32_t radius);
@ -157,6 +158,25 @@ static void basSetPixel(WidgetT *w, int32_t x, int32_t y, int32_t color) {
} }
// Pixel buffer size for a w x h canvas at bpp bytes per pixel, or -1 when
// the size does not fit in int32_t: a wrapped multiply would allocate a tiny
// buffer that the row fill loops then overrun.
//
// pixels = (int64_t)w * h cannot overflow int64_t for int32_t w,h, but the
// subsequent * bpp can, so the range is checked by division BEFORE the final
// multiply. After a passing guard, pixels <= INT32_MAX / bpp, so both
// pixels * bpp and (since w <= w * h) the caller's pitch = w * bpp fit int32_t.
static int32_t canvasBufferBytes(int32_t w, int32_t h, int32_t bpp) {
int64_t pixels = (int64_t)w * h;
if (w < 0 || h < 0 || bpp <= 0 || pixels > (int64_t)INT32_MAX / bpp) {
return -1;
}
return (int32_t)(pixels * bpp);
}
// Draw a filled circle of diameter penSize at (cx, cy) in canvas coords. // Draw a filled circle of diameter penSize at (cx, cy) in canvas coords.
static void canvasDrawDot(CanvasDataT *cd, int32_t cx, int32_t cy) { static void canvasDrawDot(CanvasDataT *cd, int32_t cx, int32_t cy) {
@ -334,10 +354,15 @@ WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
} }
const DisplayT *d = &ctx->display; const DisplayT *d = &ctx->display;
int32_t bpp = d->format.bytesPerPixel; int32_t bpp = d->format.bytesPerPixel;
int32_t pitch = w * bpp; int32_t byteCount = canvasBufferBytes(w, h, bpp);
uint8_t *data = (uint8_t *)malloc(pitch * h); if (byteCount < 0) {
return NULL;
}
int32_t pitch = w * bpp;
uint8_t *data = (uint8_t *)malloc(byteCount);
if (!data) { if (!data) {
return NULL; return NULL;
@ -730,9 +755,15 @@ void wgtCanvasResize(WidgetT *w, int32_t newW, int32_t newH) {
} }
const DisplayT *d = &ctx->display; const DisplayT *d = &ctx->display;
int32_t bpp = d->format.bytesPerPixel; int32_t bpp = d->format.bytesPerPixel;
int32_t pitch = newW * bpp; int32_t byteCount = canvasBufferBytes(newW, newH, bpp);
uint8_t *data = (uint8_t *)malloc(pitch * newH);
if (byteCount < 0) {
return;
}
int32_t pitch = newW * bpp;
uint8_t *data = (uint8_t *)malloc(byteCount);
if (!data) { if (!data) {
return; return;
@ -825,7 +856,14 @@ void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color) {
uint8_t *dst = cd->pixelData + y * cd->canvasPitch + x * bpp; uint8_t *dst = cd->pixelData + y * cd->canvasPitch + x * bpp;
canvasPutPixel(dst, color, bpp); canvasPutPixel(dst, color, bpp);
wgtInvalidatePaint(w);
// Guard the invalidate so a per-pixel plotting burst (BASIC SetPixel loop)
// walks the ancestor chain only once per frame instead of once per pixel.
// paintDirty is cleared together with childDirty during the paint pass, so
// it is a safe proxy for "already invalidated this frame".
if (!w->paintDirty) {
wgtInvalidatePaint(w);
}
} }

View file

@ -36,7 +36,6 @@
#include "../../../libs/kpunch/sql/dvxSql.h" #include "../../../libs/kpunch/sql/dvxSql.h"
#include "thirdparty/stb_ds_wrap.h" #include "thirdparty/stb_ds_wrap.h"
#include <stdarg.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -97,7 +96,7 @@ static int32_t sTypeId = -1;
// Prototypes // Prototypes
// ============================================================ // ============================================================
static void autoSave(WidgetT *w); static bool autoSave(WidgetT *w);
static bool canWriteBack(const DataCtrlDataT *d); static bool canWriteBack(const DataCtrlDataT *d);
void dataCtrlAddNew(WidgetT *w); void dataCtrlAddNew(WidgetT *w);
static void dataCtrlCalcMinSize(WidgetT *w, const BitmapFontT *font); static void dataCtrlCalcMinSize(WidgetT *w, const BitmapFontT *font);
@ -140,16 +139,23 @@ void dataCtrlUpdateRow(WidgetT *w);
static int32_t findKeyCol(const DataCtrlDataT *d); static int32_t findKeyCol(const DataCtrlDataT *d);
static void fireReposition(WidgetT *w); static void fireReposition(WidgetT *w);
static void freeCache(DataCtrlDataT *d); static void freeCache(DataCtrlDataT *d);
static int32_t sqlAppend(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...);
void wgtRegister(void); void wgtRegister(void);
static void autoSave(WidgetT *w) { // Save the current row before a cursor move. Returns false when a save
// was attempted but the row is still dirty (open/escape/truncation/exec
// failure or a Validate cancel): the caller must not move the cursor,
// because the dirty/isNewRow flags describe THIS row and applying them
// to a different row would re-INSERT an existing row as a duplicate.
static bool autoSave(WidgetT *w) {
DataCtrlDataT *d = (DataCtrlDataT *)w->data; DataCtrlDataT *d = (DataCtrlDataT *)w->data;
if (d->dirty) { if (d->dirty && canWriteBack(d)) {
dataCtrlUpdate(w); dataCtrlUpdate(w);
return !d->dirty;
} }
return true;
} }
@ -175,8 +181,11 @@ void dataCtrlAddNew(WidgetT *w) {
DataCtrlDataT *d = (DataCtrlDataT *)w->data; DataCtrlDataT *d = (DataCtrlDataT *)w->data;
// Auto-save current row if dirty // Auto-save current row if dirty; a failed save must block AddNew or
autoSave(w); // the pending row's dirty/isNewRow flags would be clobbered below.
if (!autoSave(w)) {
return;
}
if (d->colCount == 0) { if (d->colCount == 0) {
return; return;
@ -247,27 +256,40 @@ void dataCtrlDelete(WidgetT *w) {
return; return;
} }
// Delete from database (unless it's an unsaved new row) // Delete from database (unless it's an unsaved new row). The cached
// row may only be removed below when the database row is actually
// gone; dropping it while the DELETE was skipped or failed would
// leave the UI out of sync until the row reappears on Refresh.
if (!d->isNewRow && canWriteBack(d)) { if (!d->isNewRow && canWriteBack(d)) {
int32_t db = dvxSqlOpen(d->databaseName); int32_t db = dvxSqlOpen(d->databaseName);
if (db > 0) { if (db <= 0) {
int32_t keyCol = findKeyCol(d); return;
const char *keyVal = d->rows[d->currentRow].fields[keyCol]; }
// Escape and quote the key value (injection / malformed-SQL fix). int32_t keyCol = findKeyCol(d);
// Skip the exec if the key is too long to escape (the buffer const char *keyVal = d->rows[d->currentRow].fields[keyCol];
// would be left unterminated) rather than run garbage SQL.
char keyEsc[DATA_MAX_FIELD * 2];
if (dvxSqlEscape(keyVal, keyEsc, sizeof(keyEsc)) >= 0) { // Escape and quote the key value (injection / malformed-SQL fix).
char sql[512]; // Abort if the key is too long to escape (the buffer would be
snprintf(sql, sizeof(sql), "DELETE FROM %s WHERE %s='%s'", // left unterminated) rather than run garbage SQL.
d->recordSource, d->colNames[keyCol], keyEsc); char keyEsc[DATA_MAX_FIELD * 2];
dvxSqlExec(db, sql);
}
if (dvxSqlEscape(keyVal, keyEsc, sizeof(keyEsc)) < 0) {
dvxSqlClose(db); dvxSqlClose(db);
return;
}
char sql[512];
snprintf(sql, sizeof(sql), "DELETE FROM %s WHERE %s='%s'",
d->recordSource, d->colNames[keyCol], keyEsc);
bool ok = dvxSqlExec(db, sql);
dvxSqlClose(db);
if (!ok) {
return;
} }
} }
@ -433,7 +455,10 @@ static void dataCtrlMoveFirst(WidgetT *w) {
return; return;
} }
autoSave(w); if (!autoSave(w)) {
return;
}
d->currentRow = 0; d->currentRow = 0;
d->bof = false; d->bof = false;
d->eof = false; d->eof = false;
@ -448,7 +473,10 @@ static void dataCtrlMoveLast(WidgetT *w) {
return; return;
} }
autoSave(w); if (!autoSave(w)) {
return;
}
d->currentRow = d->rowCount - 1; d->currentRow = d->rowCount - 1;
d->bof = false; d->bof = false;
d->eof = false; d->eof = false;
@ -464,7 +492,10 @@ static void dataCtrlMoveNext(WidgetT *w) {
return; return;
} }
autoSave(w); if (!autoSave(w)) {
return;
}
d->currentRow++; d->currentRow++;
d->bof = false; d->bof = false;
d->eof = (d->currentRow >= d->rowCount - 1); d->eof = (d->currentRow >= d->rowCount - 1);
@ -480,7 +511,10 @@ static void dataCtrlMovePrev(WidgetT *w) {
return; return;
} }
autoSave(w); if (!autoSave(w)) {
return;
}
d->currentRow--; d->currentRow--;
d->bof = (d->currentRow == 0); d->bof = (d->currentRow == 0);
d->eof = false; d->eof = false;
@ -717,8 +751,10 @@ void dataCtrlSetCurrentRow(WidgetT *w, int32_t row) {
return; return;
} }
// Auto-save before moving // Auto-save before moving; a failed save keeps the cursor in place.
autoSave(w); if (!autoSave(w)) {
return;
}
d->currentRow = row; d->currentRow = row;
d->bof = (row == 0); d->bof = (row == 0);
@ -802,38 +838,6 @@ static void dataCtrlSetRecordSource(WidgetT *w, const char *val) {
// dataCtrlUpdate -- save the current row to the database // dataCtrlUpdate -- save the current row to the database
// Bounded formatted append into a fixed SQL buffer. Returns pos
// unchanged once pos has reached bufSize; otherwise size-clamps the
// write and returns the new position, capped at bufSize-1 on
// truncation. Avoids the snprintf-accumulation overrun where
// bufSize-pos wraps to a huge size_t and the next write runs off the end.
static int32_t sqlAppend(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) {
va_list args;
int32_t avail;
int32_t written;
if (!buf || pos < 0 || pos >= bufSize) {
return pos;
}
avail = bufSize - pos;
va_start(args, fmt);
written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args);
va_end(args);
if (written < 0) {
return pos;
}
if (written >= avail) {
return bufSize - 1;
}
return pos + written;
}
//
// If the current row was created by AddNew, executes an INSERT. // If the current row was created by AddNew, executes an INSERT.
// Otherwise executes an UPDATE using the KeyColumn to identify the row. // Otherwise executes an UPDATE using the KeyColumn to identify the row.
// Clears the dirty and isNewRow flags on success. // Clears the dirty and isNewRow flags on success.
@ -872,17 +876,17 @@ void dataCtrlUpdate(WidgetT *w) {
for (int32_t i = 0; i < d->colCount; i++) { for (int32_t i = 0; i < d->colCount; i++) {
if (i > 0) { if (i > 0) {
pos = sqlAppend(sql, sizeof(sql), pos,", "); pos = dvxStrAppendf(sql, sizeof(sql), pos,", ");
} }
pos = sqlAppend(sql, sizeof(sql), pos,"%s", d->colNames[i]); pos = dvxStrAppendf(sql, sizeof(sql), pos,"%s", d->colNames[i]);
} }
pos = sqlAppend(sql, sizeof(sql), pos, ") VALUES ("); pos = dvxStrAppendf(sql, sizeof(sql), pos, ") VALUES (");
for (int32_t i = 0; i < d->colCount; i++) { for (int32_t i = 0; i < d->colCount; i++) {
if (i > 0) { if (i > 0) {
pos = sqlAppend(sql, sizeof(sql), pos,", "); pos = dvxStrAppendf(sql, sizeof(sql), pos,", ");
} }
char escaped[DATA_MAX_FIELD * 2]; char escaped[DATA_MAX_FIELD * 2];
@ -895,10 +899,10 @@ void dataCtrlUpdate(WidgetT *w) {
return; return;
} }
pos = sqlAppend(sql, sizeof(sql), pos,"'%s'", escaped); pos = dvxStrAppendf(sql, sizeof(sql), pos,"'%s'", escaped);
} }
sqlAppend(sql, sizeof(sql), pos, ")"); pos = dvxStrAppendf(sql, sizeof(sql), pos, ")");
} else { } else {
// UPDATE table SET col1='val1', ... WHERE keyCol=keyVal // UPDATE table SET col1='val1', ... WHERE keyCol=keyVal
int32_t keyCol = findKeyCol(d); int32_t keyCol = findKeyCol(d);
@ -914,7 +918,7 @@ void dataCtrlUpdate(WidgetT *w) {
} }
if (!first) { if (!first) {
pos = sqlAppend(sql, sizeof(sql), pos,", "); pos = dvxStrAppendf(sql, sizeof(sql), pos,", ");
} }
char escaped[DATA_MAX_FIELD * 2]; char escaped[DATA_MAX_FIELD * 2];
@ -924,7 +928,7 @@ void dataCtrlUpdate(WidgetT *w) {
return; return;
} }
pos = sqlAppend(sql, sizeof(sql), pos,"%s='%s'", d->colNames[i], escaped); pos = dvxStrAppendf(sql, sizeof(sql), pos,"%s='%s'", d->colNames[i], escaped);
first = false; first = false;
} }
@ -937,13 +941,25 @@ void dataCtrlUpdate(WidgetT *w) {
return; return;
} }
sqlAppend(sql, sizeof(sql), pos, " WHERE %s='%s'", d->colNames[keyCol], keyEsc); pos = dvxStrAppendf(sql, sizeof(sql), pos, " WHERE %s='%s'", d->colNames[keyCol], keyEsc);
} }
dvxSqlExec(db, sql); // A negative position means the statement was truncated; executing it
// would lose values or the WHERE clause. Keep the row dirty so the
// edit is not silently discarded.
if (pos < 0) {
dvxSqlClose(db);
return;
}
bool ok = dvxSqlExec(db, sql);
dvxSqlClose(db); dvxSqlClose(db);
d->dirty = false;
d->isNewRow = false; if (ok) {
d->dirty = false;
d->isNewRow = false;
}
} }

View file

@ -493,15 +493,11 @@ static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y)
return; return;
} }
int32_t track = innerH - WGT_SB_W * 2; int32_t track = innerH - WGT_SB_W * 2;
int32_t thumbPos; int32_t sbY = w->y + DBGRID_BORDER + headerH;
int32_t thumbSize; int32_t relMouse = y - sbY - WGT_SB_W - d->sbDragOff;
widgetScrollbarThumb(track, rowCount, visRows, d->scrollPos, &thumbPos, &thumbSize);
int32_t sbY = w->y + DBGRID_BORDER + headerH; d->scrollPos = widgetScrollbarThumbDragScroll(track, rowCount, visRows, relMouse, maxScroll);
int32_t relMouse = y - sbY - WGT_SB_W - d->sbDragOff;
int32_t newScroll = (track > thumbSize) ? (maxScroll * relMouse) / (track - thumbSize) : 0;
d->scrollPos = clampInt(newScroll, 0, maxScroll);
wgtInvalidatePaint(w); wgtInvalidatePaint(w);
return; return;
@ -525,15 +521,11 @@ static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y)
return; return;
} }
int32_t track = innerW - WGT_SB_W * 2; int32_t track = innerW - WGT_SB_W * 2;
int32_t thumbPos; int32_t sbX = w->x + DBGRID_BORDER;
int32_t thumbSize; int32_t relMouse = x - sbX - WGT_SB_W - d->sbDragOff;
widgetScrollbarThumb(track, d->totalColW, innerW, d->scrollPosH, &thumbPos, &thumbSize);
int32_t sbX = w->x + DBGRID_BORDER; d->scrollPosH = widgetScrollbarThumbDragScroll(track, d->totalColW, innerW, relMouse, maxScroll);
int32_t relMouse = x - sbX - WGT_SB_W - d->sbDragOff;
int32_t newScroll = (track > thumbSize) ? (maxScroll * relMouse) / (track - thumbSize) : 0;
d->scrollPosH = clampInt(newScroll, 0, maxScroll);
wgtInvalidatePaint(w); wgtInvalidatePaint(w);
} }
@ -887,6 +879,10 @@ static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B
// Background fill // Background fill
rectFill(disp, ops, contentX, dataY, innerW, innerH, colors->contentBg); rectFill(disp, ops, contentX, dataY, innerW, innerH, colors->contentBg);
// The alternating-row shade is loop-invariant, so compute it once here
// instead of recomputing it (up to twice) per odd row.
uint32_t altBg = dbGridDarkenColor(disp, colors->contentBg, DBGRID_ALT_ROW_SHADE);
for (int32_t i = 0; i < visRows && d->scrollPos + i < rowCount; i++) { for (int32_t i = 0; i < visRows && d->scrollPos + i < rowCount; i++) {
int32_t dispRow = d->scrollPos + i; int32_t dispRow = d->scrollPos + i;
int32_t dataRow = displayToDataRow(d, dispRow); int32_t dataRow = displayToDataRow(d, dispRow);
@ -894,7 +890,6 @@ static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B
// Alternating row background // Alternating row background
if (i % 2 == 1) { if (i % 2 == 1) {
uint32_t altBg = dbGridDarkenColor(disp, colors->contentBg, DBGRID_ALT_ROW_SHADE);
rectFill(disp, ops, contentX, ry, innerW, rowH, altBg); rectFill(disp, ops, contentX, ry, innerW, rowH, altBg);
} }
@ -906,7 +901,7 @@ static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B
} }
uint32_t fg = selected ? colors->menuHighlightFg : colors->contentFg; uint32_t fg = selected ? colors->menuHighlightFg : colors->contentFg;
uint32_t bg = selected ? colors->menuHighlightBg : (i % 2 == 1 ? dbGridDarkenColor(disp, colors->contentBg, DBGRID_ALT_ROW_SHADE) : colors->contentBg); uint32_t bg = selected ? colors->menuHighlightBg : (i % 2 == 1 ? altBg : colors->contentBg);
// Draw cells // Draw cells
int32_t cx = contentX - d->scrollPosH; int32_t cx = contentX - d->scrollPosH;

View file

@ -71,6 +71,7 @@ typedef struct {
// Prototypes // Prototypes
// ============================================================ // ============================================================
static void dropdownEnsureHoverVisible(DropdownDataT *d);
static void dropdownSyncOwned(WidgetT *w); static void dropdownSyncOwned(WidgetT *w);
WidgetT *wgtDropdown(WidgetT *parent); WidgetT *wgtDropdown(WidgetT *parent);
void wgtDropdownAddItem(WidgetT *w, const char *text); void wgtDropdownAddItem(WidgetT *w, const char *text);
@ -94,6 +95,25 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const
void widgetDropdownPaintPopup(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetDropdownPaintPopup(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
// Adjust scrollPos so the current hover index sits within the visible popup
// window. Shared by every dropdown open/navigate path that moves hoverIdx.
// A negative hoverIdx (no selection) leaves scrollPos untouched so it is
// never driven negative.
static void dropdownEnsureHoverVisible(DropdownDataT *d) {
if (d->hoverIdx < 0) {
return;
}
if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) {
d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
if (d->hoverIdx < d->scrollPos) {
d->scrollPos = d->hoverIdx;
}
}
static void dropdownSyncOwned(WidgetT *w) { static void dropdownSyncOwned(WidgetT *w) {
DropdownDataT *d = (DropdownDataT *)w->data; DropdownDataT *d = (DropdownDataT *)w->data;
d->items = (const char **)d->ownedItems; d->items = (const char **)d->ownedItems;
@ -212,15 +232,7 @@ void widgetDropdownAccelActivate(WidgetT *w, WidgetT *root) {
sOpenPopup = w; sOpenPopup = w;
// Scroll the selected item into view, mirroring the keyboard-open path. // Scroll the selected item into view, mirroring the keyboard-open path.
if (d->hoverIdx >= 0) { dropdownEnsureHoverVisible(d);
if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) {
d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
if (d->hoverIdx < d->scrollPos) {
d->scrollPos = d->hoverIdx;
}
}
wgtInvalidatePaint(w); wgtInvalidatePaint(w);
} }
@ -294,18 +306,12 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (key == KEY_UP) { if (key == KEY_UP) {
if (d->hoverIdx > 0) { if (d->hoverIdx > 0) {
d->hoverIdx--; d->hoverIdx--;
dropdownEnsureHoverVisible(d);
if (d->hoverIdx < d->scrollPos) {
d->scrollPos = d->hoverIdx;
}
} }
} else if (key == KEY_DOWN) { } else if (key == KEY_DOWN) {
if (d->hoverIdx < d->itemCount - 1) { if (d->hoverIdx < d->itemCount - 1) {
d->hoverIdx++; d->hoverIdx++;
dropdownEnsureHoverVisible(d);
if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) {
d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
} }
} else if (key == 0x0D || key == ' ') { } else if (key == 0x0D || key == ' ') {
d->selectedIdx = d->hoverIdx; d->selectedIdx = d->hoverIdx;
@ -322,12 +328,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (found >= 0) { if (found >= 0) {
d->hoverIdx = found; d->hoverIdx = found;
dropdownEnsureHoverVisible(d);
if (d->hoverIdx < d->scrollPos) {
d->scrollPos = d->hoverIdx;
} else if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) {
d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
} }
} }
} else { } else {
@ -336,14 +337,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
d->open = true; d->open = true;
d->hoverIdx = d->selectedIdx; d->hoverIdx = d->selectedIdx;
sOpenPopup = w; sOpenPopup = w;
dropdownEnsureHoverVisible(d);
if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) {
d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
if (d->hoverIdx < d->scrollPos) {
d->scrollPos = d->hoverIdx;
}
} else if (key == KEY_DOWN) { } else if (key == KEY_DOWN) {
// Down arrow: cycle selection forward (wheel-friendly) // Down arrow: cycle selection forward (wheel-friendly)
if (d->selectedIdx < d->itemCount - 1) { if (d->selectedIdx < d->itemCount - 1) {
@ -435,17 +429,7 @@ void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
sOpenPopup = w; sOpenPopup = w;
// Scroll the selected item into view, mirroring the keyboard-open path. // Scroll the selected item into view, mirroring the keyboard-open path.
// Guard against hoverIdx == -1 (no selection) so scrollPos is never dropdownEnsureHoverVisible(d);
// driven negative.
if (d->hoverIdx >= 0) {
if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) {
d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
if (d->hoverIdx < d->scrollPos) {
d->scrollPos = d->hoverIdx;
}
}
// Repaint so the popup overlay appears immediately; the mouse // Repaint so the popup overlay appears immediately; the mouse
// dispatcher does not invalidate the hit widget on open. // dispatcher does not invalidate the hit widget on open.

View file

@ -78,11 +78,14 @@ void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
// Ownership contract: the widget takes ownership of pixelData
// unconditionally. On success it is freed by widgetImageDestroy; on
// failure it is freed here before returning NULL. Callers must never
// free pixelData themselves, even when this returns NULL.
WidgetT *wgtImage(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch) { WidgetT *wgtImage(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch) {
WidgetT *wgt = widgetAlloc(parent, sTypeId); WidgetT *wgt = widgetAlloc(parent, sTypeId);
if (!wgt) { if (!wgt) {
// The widget owns pixelData; free it on every failure path.
free(pixelData); free(pixelData);
return NULL; return NULL;
} }

View file

@ -212,7 +212,8 @@ static void listBoxSyncOwned(WidgetT *w) {
// Copy the current (external) item strings into the owned storage so a // Copy the current (external) item strings into the owned storage so a
// reorderable list box can rearrange them without mutating a caller-owned // reorderable list box can rearrange them without mutating a caller-owned
// SetItems array (which may be const/shared). Mirrors listViewAdoptCells. // SetItems array (which may be const/shared). The strdup adoption sequence
// itself lives in the shared widgetAdoptOwnedStrings helper (listHelp).
// No-op when already owned. Preserves the current item order and selection. // No-op when already owned. Preserves the current item order and selection.
static void listBoxAdoptItems(WidgetT *w) { static void listBoxAdoptItems(WidgetT *w) {
ListBoxDataT *d = (ListBoxDataT *)w->data; ListBoxDataT *d = (ListBoxDataT *)w->data;
@ -223,17 +224,7 @@ static void listBoxAdoptItems(WidgetT *w) {
int32_t count = d->itemCount; int32_t count = d->itemCount;
for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) { d->ownedItems = widgetAdoptOwnedStrings(d->ownedItems, d->items, count);
free(d->ownedItems[i]);
}
arrsetlen(d->ownedItems, 0);
for (int32_t i = 0; i < count; i++) {
const char *src = d->items ? d->items[i] : NULL;
arrput(d->ownedItems, strdup(src ? src : ""));
}
d->items = (const char **)d->ownedItems; d->items = (const char **)d->ownedItems;
d->itemCount = count; d->itemCount = count;
d->maxItemLen = widgetMaxItemLen(d->items, count); d->maxItemLen = widgetMaxItemLen(d->items, count);
@ -832,13 +823,11 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm
// Clamp item text to the content width so long items don't overpaint // Clamp item text to the content width so long items don't overpaint
// the scrollbar and neighboring widgets (no per-widget paint clip). // the scrollbar and neighboring widgets (no per-widget paint clip).
// strnlen bounds the scan to the visible width instead of walking the
// whole (possibly long) item string on every repaint.
int32_t availW = contentW - LISTBOX_PAD; int32_t availW = contentW - LISTBOX_PAD;
int32_t maxChars = availW > 0 ? availW / font->charWidth : 0; int32_t maxChars = availW > 0 ? availW / font->charWidth : 0;
int32_t itemLen = (int32_t)strlen(lb->items[idx]); int32_t itemLen = (int32_t)strnlen(lb->items[idx], (size_t)maxChars);
if (itemLen > maxChars) {
itemLen = maxChars;
}
drawTextN(d, ops, font, innerX, iy, lb->items[idx], itemLen, ifg, ibg, false); drawTextN(d, ops, font, innerX, iy, lb->items[idx], itemLen, ifg, ibg, false);
@ -964,14 +953,10 @@ static void widgetListBoxScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dr
} }
int32_t trackLen = innerH - WGT_SB_W * 2; int32_t trackLen = innerH - WGT_SB_W * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, d->itemCount, visibleRows, d->scrollPos, &thumbPos, &thumbSize);
int32_t sbY = w->y + LISTBOX_BORDER; int32_t sbY = w->y + LISTBOX_BORDER;
int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff; int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff;
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
d->scrollPos = clampInt(newScroll, 0, maxScroll); d->scrollPos = widgetScrollbarThumbDragScroll(trackLen, d->itemCount, visibleRows, relMouse, maxScroll);
} }

View file

@ -154,6 +154,7 @@ static void allocListViewSelBits(WidgetT *w);
static void basSetColumns(WidgetT *w, const char *spec); static void basSetColumns(WidgetT *w, const char *spec);
static void listViewAdoptCells(WidgetT *w); static void listViewAdoptCells(WidgetT *w);
static void listViewBuildSortIndex(WidgetT *w); static void listViewBuildSortIndex(WidgetT *w);
static void listViewRemapOwnedCells(WidgetT *w, int32_t newColCount);
static void listViewSyncOwned(WidgetT *w); static void listViewSyncOwned(WidgetT *w);
static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font); static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font);
WidgetT *wgtListView(WidgetT *parent); WidgetT *wgtListView(WidgetT *parent);
@ -412,6 +413,8 @@ static void listViewSyncOwned(WidgetT *w) {
// MUST own its cells: drag-reorder rewrites the cell array in place and must // MUST own its cells: drag-reorder rewrites the cell array in place and must
// not mutate a caller-owned SetData array (which may be const/shared), so // not mutate a caller-owned SetData array (which may be const/shared), so
// widgetListViewReorderDrop refuses to move rows unless ownsCells is true. // widgetListViewReorderDrop refuses to move rows unless ownsCells is true.
// The strdup adoption sequence itself lives in the shared
// widgetAdoptOwnedStrings helper (listHelp), shared with ListBox.
// No-op when already owned. Preserves the current row order and selection. // No-op when already owned. Preserves the current row order and selection.
static void listViewAdoptCells(WidgetT *w) { static void listViewAdoptCells(WidgetT *w) {
ListViewDataT *lv = (ListViewDataT *)w->data; ListViewDataT *lv = (ListViewDataT *)w->data;
@ -420,21 +423,64 @@ static void listViewAdoptCells(WidgetT *w) {
return; return;
} }
// Columns may not be set yet (SetData before SetColumns is legal).
// Without a column count the row-major cell count is unknowable, so
// adopting here would discard the external data while rowCount stays
// nonzero (paint would then index a NULL/empty owned array once the
// columns arrive). Defer -- wgtListViewSetColumns adopts once the
// first column count is known.
if (lv->colCount <= 0) {
return;
}
int32_t cellCount = lv->rowCount * lv->colCount; int32_t cellCount = lv->rowCount * lv->colCount;
for (int32_t i = 0; i < (int32_t)arrlen(lv->ownedCells); i++) { lv->ownedCells = widgetAdoptOwnedStrings(lv->ownedCells, lv->cellData, cellCount);
free(lv->ownedCells[i]); lv->cellData = (const char **)lv->ownedCells;
lv->nextCell = cellCount;
lv->ownsCells = true;
}
// Remap the owned row-major cell array to a new column count, preserving
// rows. Cells beyond a narrower row are freed; a wider row is padded with
// empty strings. Without this, changing the column count after rows exist
// leaves rowCount * colCount larger than the cell array and paint/autosize
// index past its end.
static void listViewRemapOwnedCells(WidgetT *w, int32_t newColCount) {
ListViewDataT *lv = (ListViewDataT *)w->data;
int32_t cellCount = (int32_t)arrlen(lv->ownedCells);
if (lv->rowCount <= 0 || newColCount <= 0) {
return;
} }
arrsetlen(lv->ownedCells, 0); int32_t oldColCount = cellCount / lv->rowCount;
for (int32_t i = 0; i < cellCount; i++) { if (oldColCount == newColCount) {
const char *src = lv->cellData ? lv->cellData[i] : NULL; return;
arrput(lv->ownedCells, strdup(src ? src : ""));
} }
lv->cellData = (const char **)lv->ownedCells; char **remapped = NULL;
lv->ownsCells = true;
for (int32_t r = 0; r < lv->rowCount; r++) {
for (int32_t c = 0; c < newColCount; c++) {
if (c < oldColCount) {
arrput(remapped, lv->ownedCells[r * oldColCount + c]);
} else {
arrput(remapped, strdup(""));
}
}
for (int32_t c = newColCount; c < oldColCount; c++) {
free(lv->ownedCells[r * oldColCount + c]);
}
}
arrfree(lv->ownedCells);
lv->ownedCells = remapped;
lv->cellData = (const char **)lv->ownedCells;
lv->nextCell = lv->rowCount * newColCount;
} }
@ -719,7 +765,8 @@ void wgtListViewSetCell(WidgetT *w, int32_t row, int32_t col, const char *text)
void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count) { void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count) {
VALIDATE_WIDGET_VOID(w, sTypeId); VALIDATE_WIDGET_VOID(w, sTypeId);
ListViewDataT *lv = (ListViewDataT *)w->data; ListViewDataT *lv = (ListViewDataT *)w->data;
int32_t oldColCount = lv->colCount;
// Grow resolvedColW as needed // Grow resolvedColW as needed
if (count > lv->resolvedColCap) { if (count > lv->resolvedColCap) {
@ -738,6 +785,27 @@ void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count)
lv->colCount = count; lv->colCount = count;
lv->totalColW = 0; lv->totalColW = 0;
// A different column count invalidates the row-major cell layout;
// without reconciliation, paint and autosize would index rowCount *
// colCount entries into a smaller cell array.
if (lv->ownsCells) {
listViewRemapOwnedCells(w, count);
} else if (oldColCount == 0) {
// First column layout: an external SetData array supplied before
// any columns existed is laid out for THIS count, so keep it.
// A reorderable list view deferred adoption in listViewAdoptCells
// while the column count was unknown; adopt now.
if (lv->reorderable && lv->rowCount > 0 && count > 0) {
listViewAdoptCells(w);
}
} else if (count != oldColCount && lv->rowCount > 0) {
// External SetData arrays were laid out for the previous column
// count and cannot be remapped here; drop the stale reference and
// require a fresh wgtListViewSetData for the new layout.
lv->cellData = NULL;
lv->rowCount = 0;
}
if (lv->rowCount > 0) { if (lv->rowCount > 0) {
AppContextT *ctx = wgtGetContext(w); AppContextT *ctx = wgtGetContext(w);
@ -2023,14 +2091,10 @@ static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t d
} }
int32_t trackLen = innerH - WGT_SB_W * 2; int32_t trackLen = innerH - WGT_SB_W * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, lv->rowCount, visibleRows, lv->scrollPos, &thumbPos, &thumbSize);
int32_t sbY = w->y + LISTVIEW_BORDER + headerH; int32_t sbY = w->y + LISTVIEW_BORDER + headerH;
int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff; int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff;
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
lv->scrollPos = clampInt(newScroll, 0, maxScroll); lv->scrollPos = widgetScrollbarThumbDragScroll(trackLen, lv->rowCount, visibleRows, relMouse, maxScroll);
} else if (orient == 1) { } else if (orient == 1) {
// Horizontal scrollbar drag // Horizontal scrollbar drag
int32_t maxScroll = totalColW - innerW; int32_t maxScroll = totalColW - innerW;
@ -2040,14 +2104,10 @@ static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t d
} }
int32_t trackLen = innerW - WGT_SB_W * 2; int32_t trackLen = innerW - WGT_SB_W * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalColW, innerW, lv->scrollPosH, &thumbPos, &thumbSize);
int32_t sbX = w->x + LISTVIEW_BORDER; int32_t sbX = w->x + LISTVIEW_BORDER;
int32_t relMouse = mouseX - sbX - WGT_SB_W - dragOff; int32_t relMouse = mouseX - sbX - WGT_SB_W - dragOff;
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
lv->scrollPosH = clampInt(newScroll, 0, maxScroll); lv->scrollPosH = widgetScrollbarThumbDragScroll(trackLen, totalColW, innerW, relMouse, maxScroll);
} }
} }

View file

@ -535,14 +535,10 @@ static void widgetScrollPaneOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mous
} }
int32_t trackLen = innerH - SP_SB_W * 2; int32_t trackLen = innerH - SP_SB_W * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, contentMinH, innerH, sp->scrollPosV, &thumbPos, &thumbSize);
int32_t sbY = w->y + spBorder(w); int32_t sbY = w->y + spBorder(w);
int32_t relMouse = mouseY - sbY - SP_SB_W - dragOff; int32_t relMouse = mouseY - sbY - SP_SB_W - dragOff;
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
sp->scrollPosV = clampInt(newScroll, 0, maxScroll); sp->scrollPosV = widgetScrollbarThumbDragScroll(trackLen, contentMinH, innerH, relMouse, maxScroll);
} else if (orient == 1) { } else if (orient == 1) {
// Horizontal scrollbar drag // Horizontal scrollbar drag
int32_t maxScroll = contentMinW - innerW; int32_t maxScroll = contentMinW - innerW;
@ -552,14 +548,10 @@ static void widgetScrollPaneOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mous
} }
int32_t trackLen = innerW - SP_SB_W * 2; int32_t trackLen = innerW - SP_SB_W * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, contentMinW, innerW, sp->scrollPosH, &thumbPos, &thumbSize);
int32_t sbX = w->x + spBorder(w); int32_t sbX = w->x + spBorder(w);
int32_t relMouse = mouseX - sbX - SP_SB_W - dragOff; int32_t relMouse = mouseX - sbX - SP_SB_W - dragOff;
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
sp->scrollPosH = clampInt(newScroll, 0, maxScroll); sp->scrollPosH = widgetScrollbarThumbDragScroll(trackLen, contentMinW, innerW, relMouse, maxScroll);
} }
} }

View file

@ -686,11 +686,13 @@ static void widgetSpinnerOnBlur(WidgetT *w) {
bool changed = d->useReal ? (d->realValue != oldRealValue) : (d->value != oldValue); bool changed = d->useReal ? (d->realValue != oldRealValue) : (d->value != oldValue);
// Mark dirty before onChange so it lands even if the handler
// destroys the widget.
wgtInvalidatePaint(w);
if (changed && w->onChange) { if (changed && w->onChange) {
w->onChange(w); w->onChange(w);
} }
wgtInvalidatePaint(w);
} }

View file

@ -135,6 +135,21 @@ typedef struct {
int32_t cachedGutterLines; // line count when cachedGutterW was computed int32_t cachedGutterLines; // line count when cachedGutterW was computed
} TextAreaDataT; } TextAreaDataT;
// Visible-region geometry for a text area, computed by textAreaComputeGeom.
// Centralizes the inner-size / visible-rows / horizontal-scrollbar
// reservation math that every scroll, hit-test, and paint site needs.
// Values are unclamped; callers apply their own visRows/visCols >= 1 floor
// where required.
typedef struct {
int32_t gutterW; // line-number gutter width, 0 when hidden
int32_t innerW; // content width inside border, pad, vscrollbar, gutter
int32_t innerH; // content height inside border and hscrollbar reserve
int32_t visCols; // whole columns that fit in innerW
int32_t visRows; // whole rows that fit in innerH
int32_t maxLL; // longest logical line length
bool needHSb; // horizontal scrollbar required (maxLL > visCols)
} TextAreaGeomT;
#include <ctype.h> #include <ctype.h>
#include <strings.h> #include <strings.h>
#include <time.h> #include <time.h>
@ -171,6 +186,7 @@ static int32_t maskFirstSlot(const char *mask);
static bool maskIsSlot(char ch); static bool maskIsSlot(char ch);
static int32_t maskNextSlot(const char *mask, int32_t pos); static int32_t maskNextSlot(const char *mask, int32_t pos);
static int32_t maskPrevSlot(const char *mask, int32_t pos); static int32_t maskPrevSlot(const char *mask, int32_t pos);
static void textAreaComputeGeom(WidgetT *w, const BitmapFontT *font, TextAreaGeomT *g);
static inline void textAreaDirtyCache(WidgetT *w); static inline void textAreaDirtyCache(WidgetT *w);
static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols); static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols);
static int32_t textAreaGetLineCount(WidgetT *w); static int32_t textAreaGetLineCount(WidgetT *w);
@ -833,6 +849,22 @@ static int32_t maskPrevSlot(const char *mask, int32_t pos) {
} }
// Compute the text area's visible-region geometry once. The horizontal
// scrollbar reservation depends on visCols (which depends on the gutter
// width), so the order here matters: gutter -> innerW -> visCols -> needHSb
// -> innerH -> visRows. Kept in one place so scroll, hit-test, and paint
// never disagree about the visible size (they had drifted before).
static void textAreaComputeGeom(WidgetT *w, const BitmapFontT *font, TextAreaGeomT *g) {
g->gutterW = textAreaGutterWidth(w, font);
g->innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - g->gutterW;
g->visCols = g->innerW / font->charWidth;
g->maxLL = textAreaGetMaxLineLen(w);
g->needHSb = (g->maxLL > g->visCols);
g->innerH = w->h - TEXTAREA_BORDER * 2 - (g->needHSb ? TEXTAREA_SB_W : 0);
g->visRows = g->innerH / font->charHeight;
}
static inline void textAreaDirtyCache(WidgetT *w) { static inline void textAreaDirtyCache(WidgetT *w) {
TextAreaDataT *ta = (TextAreaDataT *)w->data; TextAreaDataT *ta = (TextAreaDataT *)w->data;
textEditLineCacheDirty(&ta->te.lines); textEditLineCacheDirty(&ta->te.lines);
@ -1035,13 +1067,10 @@ bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, boo
if (ctx) { if (ctx) {
const BitmapFontT *font = &ctx->font; const BitmapFontT *font = &ctx->font;
int32_t gutterW = textAreaGutterWidth(w, font); TextAreaGeomT geom;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; textAreaComputeGeom(w, font, &geom);
int32_t visCols = innerW / font->charWidth; int32_t visCols = geom.visCols;
int32_t maxLL = textAreaGetMaxLineLen(w); int32_t visRows = geom.visRows;
bool needHSb = (maxLL > visCols);
int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0);
int32_t visRows = innerH / font->charHeight;
if (visCols < 1) { if (visCols < 1) {
visCols = 1; visCols = 1;
@ -1093,13 +1122,9 @@ void wgtTextAreaGoToLine(WidgetT *w, int32_t line) {
if (ctx) { if (ctx) {
const BitmapFontT *font = &ctx->font; const BitmapFontT *font = &ctx->font;
int32_t gutterW = textAreaGutterWidth(w, font); TextAreaGeomT geom;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; textAreaComputeGeom(w, font, &geom);
int32_t visCols = innerW / font->charWidth; visRows = geom.visRows;
int32_t maxLL = textAreaGetMaxLineLen(w);
bool needHSb = (maxLL > visCols);
int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0);
visRows = innerH / font->charHeight;
if (visRows < 1) { if (visRows < 1) {
visRows = 1; visRows = 1;
@ -1341,14 +1366,11 @@ static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int3
TextAreaDataT *ta = (TextAreaDataT *)w->data; TextAreaDataT *ta = (TextAreaDataT *)w->data;
AppContextT *ctx = wgtGetContext(w); AppContextT *ctx = wgtGetContext(w);
const BitmapFontT *font = &ctx->font; const BitmapFontT *font = &ctx->font;
int32_t gutterW = textAreaGutterWidth(w, font); TextAreaGeomT geom;
int32_t textX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD + gutterW; textAreaComputeGeom(w, font, &geom);
int32_t textY = w->y + TEXTAREA_BORDER; int32_t textX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD + geom.gutterW;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; int32_t textY = w->y + TEXTAREA_BORDER;
int32_t visCols = innerW / font->charWidth; int32_t visRows = geom.visRows;
int32_t maxLL = textAreaGetMaxLineLen(w);
bool needHSb = (maxLL > visCols);
int32_t visRows = (w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0)) / font->charHeight;
widgetTextEditMultiDragUpdateArea(&ta->te.lines, ta->te.buf, ta->te.len, vx, vy, textX, textY, font, &ta->te.scrollRow, ta->te.scrollCol, visRows, ta->tabWidth, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.selCursor); widgetTextEditMultiDragUpdateArea(&ta->te.lines, ta->te.buf, ta->te.len, vx, vy, textX, textY, font, &ta->te.scrollRow, ta->te.scrollCol, visRows, ta->tabWidth, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.selCursor);
} }
@ -1399,13 +1421,10 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
AppContextT *ctx = wgtGetContext(w); AppContextT *ctx = wgtGetContext(w);
const BitmapFontT *font = &ctx->font; const BitmapFontT *font = &ctx->font;
int32_t gutterW = textAreaGutterWidth(w, font); TextAreaGeomT geom;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; textAreaComputeGeom(w, font, &geom);
int32_t visCols = innerW / font->charWidth; int32_t visCols = geom.visCols;
int32_t maxLL = textAreaGetMaxLineLen(w); int32_t visRows = geom.visRows;
bool needHSb = (maxLL > visCols);
int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0);
int32_t visRows = innerH / font->charHeight;
if (visRows < 1) { if (visRows < 1) {
visRows = 1; visRows = 1;
@ -1446,15 +1465,16 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
AppContextT *ctx = (AppContextT *)root->userData; AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font; const BitmapFontT *font = &ctx->font;
int32_t gutterW = textAreaGutterWidth(w, font); TextAreaGeomT geom;
textAreaComputeGeom(w, font, &geom);
int32_t gutterW = geom.gutterW;
int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD + gutterW; int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD + gutterW;
int32_t innerY = w->y + TEXTAREA_BORDER; int32_t innerY = w->y + TEXTAREA_BORDER;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; int32_t visCols = geom.visCols;
int32_t visCols = innerW / font->charWidth; int32_t maxLL = geom.maxLL;
int32_t maxLL = textAreaGetMaxLineLen(w); bool needHSb = geom.needHSb;
bool needHSb = (maxLL > visCols); int32_t innerH = geom.innerH;
int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0); int32_t visRows = geom.visRows;
int32_t visRows = innerH / font->charHeight;
if (visRows < 1) { if (visRows < 1) {
visRows = 1; visRows = 1;
@ -1594,13 +1614,15 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
int32_t gutterW = textAreaGutterWidth(w, font); TextAreaGeomT geom;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; textAreaComputeGeom(w, font, &geom);
int32_t visCols = innerW / font->charWidth; int32_t gutterW = geom.gutterW;
int32_t maxLL = textAreaGetMaxLineLen(w); int32_t innerW = geom.innerW;
bool needHSb = (maxLL > visCols); int32_t visCols = geom.visCols;
int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0); int32_t maxLL = geom.maxLL;
int32_t visRows = innerH / font->charHeight; bool needHSb = geom.needHSb;
int32_t innerH = geom.innerH;
int32_t visRows = geom.visRows;
int32_t totalLines = textAreaGetLineCount(w); int32_t totalLines = textAreaGetLineCount(w);
bool needVSb = (totalLines > visRows); bool needVSb = (totalLines > visRows);
@ -1711,13 +1733,12 @@ static void widgetTextAreaScrollDragUpdate(WidgetT *w, int32_t orient, int32_t d
TextAreaDataT *ta = (TextAreaDataT *)w->data; TextAreaDataT *ta = (TextAreaDataT *)w->data;
AppContextT *ctx = wgtGetContext(w); AppContextT *ctx = wgtGetContext(w);
const BitmapFontT *font = &ctx->font; const BitmapFontT *font = &ctx->font;
int32_t gutterW = textAreaGutterWidth(w, font); TextAreaGeomT geom;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; textAreaComputeGeom(w, font, &geom);
int32_t visCols = innerW / font->charWidth; int32_t innerH = geom.innerH;
int32_t maxLL = textAreaGetMaxLineLen(w); int32_t visCols = geom.visCols;
bool needHSb = (maxLL > visCols); int32_t maxLL = geom.maxLL;
int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0); int32_t visRows = geom.visRows;
int32_t visRows = innerH / font->charHeight;
if (visRows < 1) { if (visRows < 1) {
visRows = 1; visRows = 1;

View file

@ -195,6 +195,13 @@ void wgtUpdateTimers(void) {
WidgetT *w = sActiveTimers[i]; WidgetT *w = sActiveTimers[i];
TimerDataT *d = (TimerDataT *)w->data; TimerDataT *d = (TimerDataT *)w->data;
// A timer on a deferred-destroyed window must not fire: the
// widget tree is still allocated, but its backing app state
// (e.g. the BASIC control in userData) may already be freed.
if (w->window && w->window->destroyPending) {
continue;
}
if (!d->running) { if (!d->running) {
arrdel(sActiveTimers, i); arrdel(sActiveTimers, i);
continue; continue;
@ -232,6 +239,13 @@ void wgtUpdateTimers(void) {
} }
i = idx; i = idx;
// The relocation matched by pointer only: the callback may
// have destroyed this timer and a newly created timer may
// have reused the same heap address. Re-fetch the data
// pointer so the repeat/running checks below act on the
// live TimerDataT, not the freed pre-callback one.
d = (TimerDataT *)w->data;
} }
if (!d->repeat) { if (!d->repeat) {

View file

@ -102,6 +102,7 @@ typedef struct {
typedef struct { typedef struct {
const char *text; const char *text;
WidgetT *treeView; // owning treeView, cached at creation for destroy cleanup
bool expanded; bool expanded;
bool selected; bool selected;
} TreeItemDataT; } TreeItemDataT;
@ -837,6 +838,17 @@ WidgetT *wgtTreeItem(WidgetT *parent, const char *text) {
w->data = ti; w->data = ti;
ti->text = text ? strdup(text) : NULL; ti->text = text ? strdup(text) : NULL;
ti->expanded = false; ti->expanded = false;
// Cache the owning treeView so widgetTreeItemDestroy can clear
// selection/drag pointers that reference this item. wgtDestroy
// unlinks the subtree root before the destroy callbacks run, so
// the parent chain cannot be walked at destroy time.
for (WidgetT *p = parent; p; p = p->parent) {
if (p->type == sTreeViewTypeId) {
ti->treeView = p;
break;
}
}
} }
invalidateTreeDims(w); invalidateTreeDims(w);
@ -1011,6 +1023,33 @@ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) {
static void widgetTreeItemDestroy(WidgetT *w) { static void widgetTreeItemDestroy(WidgetT *w) {
TreeItemDataT *ti = (TreeItemDataT *)w->data; TreeItemDataT *ti = (TreeItemDataT *)w->data;
// This item is being freed (unlike a mere visibility toggle, which
// must keep the selection), so drop any treeView pointer that would
// otherwise dangle at it. Every item of a destroyed subtree gets its
// own destroy call, so pointer equality covers whole subtrees. The
// treeView widget and its data outlive its items on every destroy
// path (children are destroyed depth-first before their ancestors).
if (ti->treeView && ti->treeView->data) {
TreeViewDataT *tv = (TreeViewDataT *)ti->treeView->data;
if (tv->selectedItem == w) {
tv->selectedItem = NULL;
}
if (tv->anchorItem == w) {
tv->anchorItem = NULL;
}
if (tv->dragItem == w) {
tv->dragItem = NULL;
}
if (tv->dropTarget == w) {
tv->dropTarget = NULL;
}
}
free((void *)ti->text); free((void *)ti->text);
free(w->data); free(w->data);
w->data = NULL; w->data = NULL;
@ -1097,10 +1136,8 @@ WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView) {
} }
// True if item is candidate or any descendant of candidate. Used to // True if item is candidate or any descendant of candidate. Used by
// detect when a removed subtree contains a tracked selection pointer. // reorder drop to refuse dropping an item into its own subtree.
// onChildChanged fires before wgtDestroy unlinks the child, so the
// parent chain is still intact here.
static bool treeItemInSubtree(WidgetT *item, WidgetT *candidate) { static bool treeItemInSubtree(WidgetT *item, WidgetT *candidate) {
for (WidgetT *p = item; p; p = p->parent) { for (WidgetT *p = item; p; p = p->parent) {
if (p == candidate) { if (p == candidate) {
@ -1113,31 +1150,16 @@ static bool treeItemInSubtree(WidgetT *item, WidgetT *candidate) {
void widgetTreeViewOnChildChanged(WidgetT *parent, WidgetT *child) { void widgetTreeViewOnChildChanged(WidgetT *parent, WidgetT *child) {
(void)child;
TreeViewDataT *tv = (TreeViewDataT *)parent->data; TreeViewDataT *tv = (TreeViewDataT *)parent->data;
tv->dimsValid = false; tv->dimsValid = false;
// A child (and its subtree) being destroyed must not stay referenced // This fires for BOTH wgtDestroy and wgtSetVisible, so it must not
// by the selection/drag pointers, or they dangle after the items are // clear the selection/drag pointers here: a mere visibility toggle
// freed -- a later paint/key/mouse would dereference freed memory. // on the selected item (or an ancestor of it) would silently discard
// wgtDestroy fires this before unlinking, so the subtree check is // the selection even though the item still exists. Dangling-pointer
// valid. Covers both BASIC TreeView.Clear and IDE tree rebuilds. // cleanup for destroyed items lives in widgetTreeItemDestroy, which
if (child) { // runs for every item of a destroyed subtree.
if (tv->selectedItem && treeItemInSubtree(tv->selectedItem, child)) {
tv->selectedItem = NULL;
}
if (tv->anchorItem && treeItemInSubtree(tv->anchorItem, child)) {
tv->anchorItem = NULL;
}
if (tv->dragItem && treeItemInSubtree(tv->dragItem, child)) {
tv->dragItem = NULL;
}
if (tv->dropTarget && treeItemInSubtree(tv->dropTarget, child)) {
tv->dropTarget = NULL;
}
}
} }
@ -1890,14 +1912,10 @@ static void widgetTreeViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t d
} }
int32_t trackLen = innerH - WGT_SB_W * 2; int32_t trackLen = innerH - WGT_SB_W * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalH, innerH, tv->scrollPos, &thumbPos, &thumbSize);
int32_t sbY = w->y + TREE_BORDER; int32_t sbY = w->y + TREE_BORDER;
int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff; int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff;
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
tv->scrollPos = clampInt(newScroll, 0, maxScroll); tv->scrollPos = widgetScrollbarThumbDragScroll(trackLen, totalH, innerH, relMouse, maxScroll);
} else if (orient == 1) { } else if (orient == 1) {
// Horizontal scrollbar drag // Horizontal scrollbar drag
int32_t maxScroll = totalW - innerW; int32_t maxScroll = totalW - innerW;
@ -1907,14 +1925,10 @@ static void widgetTreeViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t d
} }
int32_t trackLen = innerW - WGT_SB_W * 2; int32_t trackLen = innerW - WGT_SB_W * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalW, innerW, tv->scrollPosH, &thumbPos, &thumbSize);
int32_t sbX = w->x + TREE_BORDER; int32_t sbX = w->x + TREE_BORDER;
int32_t relMouse = mouseX - sbX - WGT_SB_W - dragOff; int32_t relMouse = mouseX - sbX - WGT_SB_W - dragOff;
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
tv->scrollPosH = clampInt(newScroll, 0, maxScroll); tv->scrollPosH = widgetScrollbarThumbDragScroll(trackLen, totalW, innerW, relMouse, maxScroll);
} }
} }