-
Notifications
You must be signed in to change notification settings - Fork 2
Documentation #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
0a537c4
first draft of core UI architecture
AmerM137 7df9bf9
simplify note about super constructor
AmerM137 b3f2dce
Add quick-start sections with plugin examples
AmerM137 2c12bf0
add notes about variable fps, coordinate systems, optimizations, and
AmerM137 fb90fd7
add note about not updating/views when they aren't visible
AmerM137 00eedc5
add note about C renderer
AmerM137 aa96456
add more details about config locations
AmerM137 fe907e0
documenting channels' API
AmerM137 447df53
add note about proper plugin naming convention
AmerM137 77e019d
create how to do X section
AmerM137 8787fb8
rename
AmerM137 f2d86d8
Clarify command palette usage in how-to-do-x guide
AmerM137 bd1ad1a
Remove channels API documentation from PR
AmerM137 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,360 @@ | ||
| --- | ||
| sidebar_position: 2 | ||
| description: An introduction about Pragtical's concepts | ||
| and how they tie in to plugin development. | ||
| --- | ||
|
|
||
| # Core UI Architecture | ||
|
|
||
| ## Overview | ||
|
|
||
| Pragtical's UI is built on four core components that work together: | ||
|
|
||
| 1. **Object** - OOP foundation | ||
| 2. **View** - UI component base class | ||
| 3. **Node** - Layout and container management | ||
| 4. **RootView** - Top-level coordinator | ||
|
|
||
| ## Object System | ||
|
|
||
| **File:** `data/core/object.lua` | ||
|
|
||
| Provides class-based inheritance for Lua. | ||
|
|
||
| ### Usage | ||
|
|
||
| ```lua | ||
| local Object = require "core.object" | ||
| local MyClass = Object:extend() | ||
|
|
||
| function MyClass:new(param) | ||
| -- `super` is a reference to the parent class, in this case `Object` | ||
| -- IMPORTANT: Call super constructor first, use . instead of : and pass | ||
| -- self explicitly, forgetting this call is a common mistake | ||
| -- that can lead to uninitialized state and unexpected behavior | ||
| MyClass.super.new(self) | ||
| self.value = param | ||
| end | ||
|
|
||
| function MyClass:method() | ||
| return self.value | ||
| end | ||
| ``` | ||
|
|
||
| ### Key Methods | ||
|
|
||
| - `Object:extend()` - Create a subclass | ||
| - `Object:new(...)` - Constructor (override this) | ||
| - `Object:is(Type)` - Check exact type | ||
| - `Object:extends(Type)` - Check inheritance | ||
|
|
||
| ## View System | ||
|
|
||
| **File:** `data/core/view.lua` | ||
|
|
||
| Base class for all UI components. Handles position, size, scrolling, events, and drawing. | ||
|
|
||
| ### Key Properties | ||
|
|
||
| ```lua | ||
| { | ||
| position = { x, y }, | ||
| size = { x, y }, | ||
| scroll = { x, y, to = { x, y } }, | ||
| scrollable = boolean, | ||
| cursor = "arrow" | "ibeam" | "sizeh" | "sizev" | "hand", | ||
| context = "application" | "session" | ||
| } | ||
| ``` | ||
|
|
||
| ### Lifecycle Methods | ||
|
|
||
| - `View:new()` - Constructor | ||
| - `View:update()` - Called each time a keyboard, mouse, resize event, etc... occurs, or core.redraw is set to true and the frames per second has not been exceeded | ||
| - `View:draw()` - Render the view | ||
| - `View:try_close(do_close)` - Handle close request | ||
|
|
||
| ### Event Handlers | ||
|
|
||
| - `View:on_mouse_pressed(button, x, y, clicks)` | ||
| - `View:on_mouse_released(button, x, y)` | ||
| - `View:on_mouse_moved(x, y, dx, dy)` | ||
| - `View:on_mouse_wheel(y, x)` | ||
| - `View:on_text_input(text)` | ||
| - `View:on_file_dropped(filename, x, y)` | ||
|
|
||
| ### Animation | ||
|
|
||
| ```lua | ||
| -- Smoothly animate values | ||
| self:move_towards(self.scroll, "y", target_y, 0.3) | ||
| ``` | ||
|
|
||
| ### Creating a Custom View | ||
|
|
||
| ```lua | ||
| local View = require "core.view" | ||
| local MyView = View:extend() | ||
|
|
||
| function MyView:new() | ||
| MyView.super.new(self) | ||
| self.scrollable = true | ||
| end | ||
|
|
||
| function MyView:draw() | ||
| self:draw_background(style.background) | ||
| -- Custom drawing here | ||
| self:draw_scrollbar() | ||
| end | ||
|
|
||
| function MyView:on_mouse_pressed(button, x, y, clicks) | ||
| -- Handle input | ||
| return true -- Consume event | ||
| end | ||
| ``` | ||
|
|
||
| ## Node System | ||
|
|
||
| **File:** `data/core/node.lua` | ||
|
|
||
| Manages layout using a tree structure. Nodes can contain views (tabs) or split into child nodes. | ||
|
|
||
| ### Node Types | ||
|
|
||
| - `"leaf"` - Contains views as tabs | ||
| - `"hsplit"` - Horizontal split (left/right) | ||
| - `"vsplit"` - Vertical split (top/bottom) | ||
|
|
||
| ### Key Properties | ||
|
|
||
| ```lua | ||
| { | ||
| type = "leaf" | "hsplit" | "vsplit", | ||
| views = {}, -- Array of views (leaf nodes) | ||
| active_view = View, -- Currently active tab | ||
| divider = 0.5, -- Split ratio (0-1) | ||
| a, b = Node, Node, -- Child nodes (splits) | ||
| locked = {x, y}, -- Fixed dimensions | ||
| resizable = boolean | ||
| } | ||
| ``` | ||
|
|
||
| ### Key Methods | ||
|
|
||
| - `Node:split(direction, view)` - Split node and add view | ||
| - Direction: `"left"`, `"right"`, `"up"`, `"down"` | ||
| - `Node:add_view(view, idx)` - Add view as tab | ||
| - `Node:remove_view(root, view)` - Remove view | ||
| - `Node:set_active_view(view)` - Switch active tab | ||
| - `Node:get_node_for_view(view)` - Find containing node | ||
|
|
||
| ### Usage | ||
|
|
||
| ```lua | ||
| -- Get active node | ||
| local node = core.root_view:get_active_node() | ||
|
|
||
| -- Add view as tab | ||
| node:add_view(MyView()) | ||
|
|
||
| -- Split and add view | ||
| node:split("right", MyView()) | ||
| ``` | ||
|
|
||
| ## RootView | ||
|
|
||
| **File:** `data/core/rootview.lua` | ||
|
|
||
| Top-level view that manages the entire UI. Routes events and handles global operations. | ||
|
|
||
| ### Key Methods | ||
|
|
||
| - `RootView:get_active_node()` - Node with focus | ||
| - `RootView:get_primary_node()` - Main document area | ||
| - `RootView:open_doc(doc)` - Open document | ||
| - `RootView:grab_mouse(button, view)` - Capture mouse input | ||
| - `RootView:ungrab_mouse(button)` - Release capture | ||
|
|
||
| ### Event Flow | ||
|
|
||
| ``` | ||
| OS Event | ||
| → RootView (routes to node/view under mouse) | ||
| → Node (delegates to active view) | ||
| → View (handles or returns false) | ||
| ``` | ||
|
|
||
| ## DocView | ||
|
|
||
| **File:** `data/core/docview.lua` | ||
|
|
||
| The text editor view. Most complex view implementation. | ||
|
|
||
| ### Features | ||
|
|
||
| - Syntax highlighting | ||
| - Multiple cursors | ||
| - Line numbers gutter | ||
| - Horizontal and vertical scrolling | ||
| - IME support | ||
|
|
||
| ### Key Methods | ||
|
|
||
| - `DocView:get_line_screen_position(line, col)` - Doc to screen coords | ||
| - `DocView:resolve_screen_position(x, y)` - Screen to doc coords | ||
| - `DocView:scroll_to_line(line)` - Scroll to line | ||
| - `DocView:scroll_to_make_visible(line, col)` - Ensure visible | ||
|
|
||
| ## Common View Implementations | ||
|
|
||
| - `EmptyView` (`data/core/emptyview.lua`) - Welcome screen | ||
| - `StatusView` (`data/core/statusview.lua`) - Status bar | ||
| - `CommandView` (`data/core/commandview.lua`) - Command palette | ||
| - `LogView` (`data/core/logview.lua`) - Log viewer | ||
| - `NagView` (`data/core/nagview.lua`) - Dialogs/modals | ||
| - `TitleView` (`data/core/titleview.lua`) - Title bar | ||
| - `TreeView` (`data/plugins/treeview.lua`) - File tree sidebar | ||
|
|
||
| ## Architecture Diagram | ||
|
|
||
| ``` | ||
| Object (OOP base) | ||
| │ | ||
| └─ View (UI component) | ||
| ├─ RootView (coordinator) | ||
| ├─ DocView (text editor) | ||
| ├─ StatusView | ||
| ├─ EmptyView | ||
| └─ ... | ||
|
|
||
| Node Tree (layout): | ||
| RootView.root_node | ||
| ├─ Node (hsplit) | ||
| │ ├─ Node (leaf) → [View1, View2] # Tabs | ||
| │ └─ Node (vsplit) | ||
| │ ├─ Node (leaf) → [View3] | ||
| │ └─ Node (leaf) → [View4] | ||
| ``` | ||
|
|
||
| ## Update/Draw Cycle | ||
|
|
||
| Every frame (typically 60 FPS): | ||
|
AmerM137 marked this conversation as resolved.
|
||
|
|
||
| ``` | ||
| 1. Update Phase: | ||
| RootView:update() | ||
| └─ Node:update() (recursive) | ||
| └─ View:update() | ||
|
|
||
| 2. Draw Phase: | ||
| RootView:draw() | ||
| └─ Node:draw() (recursive) | ||
| └─ View:draw() | ||
| ``` | ||
|
|
||
| ## Context System | ||
|
|
||
| Views have a `context` property: | ||
|
|
||
| - `"application"` - Persists across sessions (toolbar, status bar) | ||
| - `"session"` - Tied to project (document views, tree view) | ||
|
|
||
| When closing a project, only `"session"` context views are closed. | ||
|
|
||
| ## Mouse Grab Pattern | ||
|
|
||
| For drag operations, views can capture mouse input: | ||
|
|
||
| ```lua | ||
| function MyView:on_mouse_pressed(button, x, y, clicks) | ||
| core.root_view:grab_mouse("left", self) | ||
| return true | ||
| end | ||
|
|
||
| function MyView:on_mouse_released(button, x, y) | ||
| core.root_view:ungrab_mouse("left") | ||
| return true | ||
| end | ||
|
|
||
| -- While grabbed, all mouse events go to this view | ||
| ``` | ||
|
|
||
| ## Best Practices | ||
|
|
||
| 1. **Always call super methods** when overriding | ||
| ```lua | ||
| function MyView:update() | ||
| MyView.super.update(self) | ||
| -- Your update logic | ||
| end | ||
| ``` | ||
|
|
||
| 2. **Use move_towards for animations** instead of direct assignment | ||
| ```lua | ||
| self:move_towards(self.scroll, "y", target, 0.3) | ||
| ``` | ||
|
|
||
| 3. **Return true from event handlers** to consume events | ||
| ```lua | ||
| function MyView:on_mouse_pressed(...) | ||
| -- Handle event | ||
| return true -- Prevent event propagation | ||
| end | ||
| ``` | ||
|
|
||
| 4. **Draw background and scrollbar** in custom views | ||
| ```lua | ||
| function MyView:draw() | ||
| self:draw_background(style.background) | ||
| -- Content | ||
| self:draw_scrollbar() | ||
| end | ||
| ``` | ||
|
|
||
| 5. **Set scrollable property** if view needs scrolling | ||
| ```lua | ||
| function MyView:new() | ||
| MyView.super.new(self) | ||
| self.scrollable = true | ||
| end | ||
| ``` | ||
|
|
||
| ### Coordinate Systems | ||
|
|
||
| When working with positions, be aware of different coordinate spaces: | ||
|
|
||
| 1. **Screen coordinates**: Absolute pixel positions in the window (e.g., mouse x, y) | ||
| 2. **Content coordinates**: Positions relative to view content, accounting for scroll offset | ||
| 3. **Document coordinates**: Line and column positions in text (e.g., line 5, column 10) | ||
|
|
||
| **Conversion methods:** | ||
|
|
||
| Screen ↔ Document: | ||
| - `DocView:resolve_screen_position(x, y)` → `(line, col)` - Convert screen pixels to document position | ||
| - `DocView:get_line_screen_position(line, col)` → `(x, y)` - Convert document position to screen pixels | ||
|
|
||
| Content offset helpers: | ||
| - `View:get_content_offset()` → `(x, y)` - Get top-left corner of content area in screen coordinates (accounts for scroll) | ||
| - `View:get_content_bounds()` → `(x1, y1, x2, y2)` - Get visible content rectangle in content coordinates | ||
|
|
||
| Document column ↔ X offset within line: | ||
| - `DocView:get_col_x_offset(line, col)` → `xoffset` - Get horizontal pixel offset for a column position | ||
| - `DocView:get_x_offset_col(line, x)` → `col` - Get column position from horizontal pixel offset | ||
|
|
||
|
|
||
| ### Notes | ||
| - **Variable FPS** To lower cpu usage, the editor does not run at a constant FPS. The configured `config.fps` is basically the maximum allowed frames per second. Pragtical also prevents calling the update and draw methods of views when they aren't visible (by checking if their size is 0) to save cpu cycles. Pragtical implements cached rendering in C which skips draw commands for parts of the UI that have not changed (and for the most part those that go beyond the viewable area). | ||
| - **Line Caching**: DocView implements column-to-x-offset caching for lines longer than 500 characters (defined by `CACHE_LINE_LEN`). This significantly improves performance on extremely long lines. | ||
| - **Visible Range Optimization**: Views only draw and update content that's visible on screen. Use `DocView:get_visible_line_range()` and `DocView:get_visible_cols_range()` when implementing plugins that process document content. | ||
| - **Font Tab Size**: When working with fonts, remember to call `font:set_tab_size(indent_size)` to ensure proper tab rendering. DocView handles this automatically for syntax highlighting. | ||
|
|
||
| Debugging Tips | ||
|
|
||
| - Enable development tools in Settings → Development to see: | ||
| - Current FPS and frame timing | ||
| - Repainted screen regions | ||
| - View boundaries and clipping rectangle | ||
| - Log slow coroutines | ||
| - Use `core.log()` or `core.error()` for logging (visible in LogView) | ||
| - Check `core.active_view` to verify which view has focus | ||
| - Inspect `core.root_view.root_node` tree structure when debugging layout issue | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.