Skip to main content
Glama

Edit-MCP

tui.rs153 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! An immediate mode UI framework for terminals. //! //! # Why immediate mode? //! //! This uses an "immediate mode" design, similar to [ImGui](https://github.com/ocornut/imgui). //! The reason for this is that I expect the UI needs for any terminal application to be //! fairly minimal, and for that purpose an immediate mode design is much simpler to use. //! //! So what's "immediate mode"? The primary alternative is called "retained mode". //! The difference is that when you create a button in this framework in one frame, //! and you stop telling this framework in the next frame, the button will vanish. //! When you use a regular retained mode UI framework, you create the button once, //! set up callbacks for when it is clicked, and then stop worrying about it. //! //! The downside of immediate mode is that your UI code _may_ become cluttered. //! The upside however is that you cannot leak UI elements, you don't need to //! worry about lifetimes nor callbacks, and that simple UIs are simple to write. //! //! More importantly though, the primary reason for this is that the //! lack of callbacks means we can use this design across a plain C ABI, //! which we'll need once plugins come into play. GTK's `g_signal_connect` //! shows that the alternative can be rather cumbersome. //! //! # Design overview //! //! While this file is fairly lengthy, the overall algorithm is simple. //! On the first frame ever: //! * Prepare an empty `arena_next`. //! * Parse the incoming [`input::Input`] which should be a resize event. //! * Create a new [`Context`] instance and give it the caller. //! * Now the caller will draw their UI with the [`Context`] by calling the //! various [`Context`] UI methods, such as [`Context::block_begin()`] and //! [`Context::block_end()`]. These two are the basis which all other UI //! elements are built upon by the way. Each UI element that is created gets //! allocated onto `arena_next` and inserted into the UI tree. //! That tree works exactly like the DOM tree in HTML: Each node in the tree //! has a parent, children, and siblings. The tree layout at the end is then //! a direct mirror of the code "layout" that created it. //! * Once the caller is done and drops the [`Context`], it'll secretly call //! `report_context_completion`. This causes a number of things: //! * The DOM tree that was built is stored in `prev_tree`. //! * A hashmap of all nodes is built and stored in `prev_node_map`. //! * `arena_next` is swapped with `arena_prev`. //! * Each UI node is measured and laid out. //! * Now the caller is expected to repeat this process with a [`None`] //! input event until [`Tui::needs_settling()`] returns false. //! This is necessary, because when [`Context::button()`] returns `true` //! in one frame, it may change the state in the caller's code //! and require another frame to be drawn. //! * Finally a call to [`Tui::render()`] will render the UI tree into the //! framebuffer and return VT output. //! //! On every subsequent frame the process is similar, but one crucial element //! of any immediate mode UI framework is added: //! Now when the caller draws their UI, the various [`Context`] UI elements //! have access to `prev_node_map` and the previously built UI tree. //! This allows the UI framework to reuse the previously computed layout for //! hit tests, caching scroll offsets, and so on. //! //! In the end it looks very similar: //! * Prepare an empty `arena_next`. //! * Parse the incoming [`input::Input`]... //! * **BUT** now we can hit-test mouse clicks onto the previously built //! UI tree. This way we can delegate focus on left mouse clicks. //! * Create a new [`Context`] instance and give it the caller. //! * The caller draws their UI with the [`Context`]... //! * **BUT** we can preserve the UI state across frames. //! * Continue rendering until [`Tui::needs_settling()`] returns false. //! * And the final call to [`Tui::render()`]. //! //! # Classnames and node IDs //! //! So how do we find which node from the previous tree correlates to the //! current node? Each node needs to be constructed with a "classname". //! The classname is hashed with the parent node ID as the seed. This derived //! hash is then used as the new child node ID. Under the assumption that the //! collision likelihood of the hash function is low, this serves as true IDs. //! //! This has the nice added property that finding a node with the same ID //! guarantees that all of the parent nodes must have equivalent IDs as well. //! This turns "is the focus anywhere inside this subtree" into an O(1) check. //! //! The reason "classnames" are used is because I was hoping to add theming //! in the future with a syntax similar to CSS (simplified, however). //! //! # Example //! //! ``` //! use edit::helpers::Size; //! use edit::input::Input; //! use edit::tui::*; //! use edit::{arena, arena_format}; //! //! struct State { //! counter: i32, //! } //! //! fn main() { //! arena::init(128 * 1024 * 1024).unwrap(); //! //! // Create a `Tui` instance which holds state across frames. //! let mut tui = Tui::new().unwrap(); //! let mut state = State { counter: 0 }; //! let input = Input::Resize(Size { width: 80, height: 24 }); //! //! // Pass the input to the TUI. //! { //! let mut ctx = tui.create_context(Some(input)); //! draw(&mut ctx, &mut state); //! } //! //! // Continue until the layout has settled. //! while tui.needs_settling() { //! let mut ctx = tui.create_context(None); //! draw(&mut ctx, &mut state); //! } //! //! // Render the output. //! let scratch = arena::scratch_arena(None); //! let output = tui.render(&*scratch); //! println!("{}", output); //! } //! //! fn draw(ctx: &mut Context, state: &mut State) { //! ctx.table_begin("classname"); //! { //! ctx.table_next_row(); //! //! // Thanks to the lack of callbacks, we can use a primitive //! // if condition here, as well as in any potential C code. //! if ctx.button("button", "Click me!", ButtonStyle::default()) { //! state.counter += 1; //! } //! //! // Similarly, formatting and showing labels is straightforward. //! // It's impossible to forget updating the label this way. //! ctx.label("label", &arena_format!(ctx.arena(), "Counter: {}", state.counter)); //! } //! ctx.table_end(); //! } //! ``` use std::arch::breakpoint; #[cfg(debug_assertions)] use std::collections::HashSet; use std::fmt::Write as _; use std::{iter, mem, ptr, time}; use crate::arena::{Arena, ArenaString, scratch_arena}; use crate::buffer::{CursorMovement, RcTextBuffer, TextBuffer, TextBufferCell}; use crate::cell::*; use crate::document::WriteableDocument; use crate::framebuffer::{Attributes, Framebuffer, INDEXED_COLORS_COUNT, IndexedColor}; use crate::hash::*; use crate::helpers::*; use crate::input::{InputKeyMod, kbmod, vk}; use crate::{apperr, arena_format, input, simd, unicode}; const ROOT_ID: u64 = 0x14057B7EF767814F; // Knuth's MMIX constant const SHIFT_TAB: InputKey = vk::TAB.with_modifiers(kbmod::SHIFT); const KBMOD_FOR_WORD_NAV: InputKeyMod = if cfg!(target_os = "macos") { kbmod::ALT } else { kbmod::CTRL }; type Input<'input> = input::Input<'input>; type InputKey = input::InputKey; type InputMouseState = input::InputMouseState; type InputText<'input> = input::InputText<'input>; /// Since [`TextBuffer`] creation and management is expensive, /// we cache instances of them for reuse between frames. /// This is used for [`Context::editline()`]. struct CachedTextBuffer { node_id: u64, editor: RcTextBuffer, seen: bool, } /// Since [`Context::editline()`] and [`Context::textarea()`] /// do almost the same thing, this abstracts over the two. enum TextBufferPayload<'a> { Editline(&'a mut dyn WriteableDocument), Textarea(RcTextBuffer), } /// In order for the TUI to show the correct Ctrl/Alt/Shift /// translations, this struct lets you set them. pub struct ModifierTranslations { pub ctrl: &'static str, pub alt: &'static str, pub shift: &'static str, } /// Controls to which node the floater is anchored. #[derive(Default, Clone, Copy, PartialEq, Eq)] pub enum Anchor { /// The floater is attached relative to the node created last. #[default] Last, /// The floater is attached relative to the current node (= parent of new nodes). Parent, /// The floater is attached relative to the root node (= usually the viewport). Root, } /// Controls the position of the floater. See [`Context::attr_float`]. #[derive(Default)] pub struct FloatSpec { /// Controls to which node the floater is anchored. pub anchor: Anchor, // Specifies the origin of the container relative to the container size. [0, 1] pub gravity_x: f32, pub gravity_y: f32, // Specifies an offset from the origin in cells. pub offset_x: f32, pub offset_y: f32, } /// Informs you about the change that was made to the list selection. #[derive(Clone, Copy, PartialEq, Eq)] pub enum ListSelection { /// The selection wasn't changed. Unchanged, /// The selection was changed to the current list item. Selected, /// The selection was changed to the current list item /// *and* the item was also activated (Enter or Double-click). Activated, } /// Controls the position of a node relative to its parent. #[derive(Default)] pub enum Position { /// The child is stretched to fill the parent. #[default] Stretch, /// The child is positioned at the left edge of the parent. Left, /// The child is positioned at the center of the parent. Center, /// The child is positioned at the right edge of the parent. Right, } /// Controls the text overflow behavior of a label /// when the text doesn't fit the container. #[derive(Default, Clone, Copy, PartialEq, Eq)] pub enum Overflow { /// Text is simply cut off when it doesn't fit. #[default] Clip, /// An ellipsis is shown at the end of the text. TruncateHead, /// An ellipsis is shown in the middle of the text. TruncateMiddle, /// An ellipsis is shown at the beginning of the text. TruncateTail, } /// Controls the style with which a button label renders #[derive(Clone, Copy)] pub struct ButtonStyle { accelerator: Option<char>, checked: Option<bool>, bracketed: bool, } impl ButtonStyle { /// Draw an accelerator label: `[_E_xample button]` or `[Example button(X)]` /// /// Must provide an upper-case ASCII character. pub fn accelerator(self, char: char) -> Self { Self { accelerator: Some(char), ..self } } /// Draw a checkbox prefix: `[🗹 Example Button]` pub fn checked(self, checked: bool) -> Self { Self { checked: Some(checked), ..self } } /// Draw with or without brackets: `[Example Button]` or `Example Button` pub fn bracketed(self, bracketed: bool) -> Self { Self { bracketed, ..self } } } impl Default for ButtonStyle { fn default() -> Self { Self { accelerator: None, checked: None, bracketed: true, // Default style for most buttons. Brackets may be disabled e.g. for buttons in menus } } } /// There's two types of lifetimes the TUI code needs to manage: /// * Across frames /// * Per frame /// /// [`Tui`] manages the first one. It's also the entrypoint for /// everything else you may want to do. pub struct Tui { /// Arena used for the previous frame. arena_prev: Arena, /// Arena used for the current frame. arena_next: Arena, /// The UI tree built in the previous frame. /// This refers to memory in `arena_prev`. prev_tree: Tree<'static>, /// A hashmap of all nodes built in the previous frame. /// This refers to memory in `arena_prev`. prev_node_map: NodeMap<'static>, /// The framebuffer used for rendering. framebuffer: Framebuffer, modifier_translations: ModifierTranslations, floater_default_bg: u32, floater_default_fg: u32, modal_default_bg: u32, modal_default_fg: u32, /// Last known terminal size. /// /// This lives here instead of [`Context`], because we need to /// track the state across frames and input events. /// This also applies to the remaining members in this block below. size: Size, /// Last known mouse position. mouse_position: Point, /// Between mouse down and up, the position where the mouse was pressed. /// Otherwise, this contains Point::MIN. mouse_down_position: Point, /// Node ID of the node that was clicked on. /// Used for tracking drag targets. left_mouse_down_target: u64, /// Timestamp of the last mouse up event. /// Used for tracking double/triple clicks. mouse_up_timestamp: std::time::Instant, /// The current mouse state. mouse_state: InputMouseState, /// Whether the mouse is currently being dragged. mouse_is_drag: bool, /// The number of clicks that have happened in a row. /// Gets reset when the mouse was released for a while. mouse_click_counter: CoordType, /// The path to the node that was clicked on. mouse_down_node_path: Vec<u64>, /// The position of the first click in a double/triple click series. first_click_position: Point, /// The node ID of the node that was first clicked on /// in a double/triple click series. first_click_target: u64, /// Path to the currently focused node. focused_node_path: Vec<u64>, /// Contains the last element in [`Tui::focused_node_path`]. /// This way we can track if the focus changed, because then we /// need to scroll the node into view if it's within a scrollarea. focused_node_for_scrolling: u64, /// A list of cached text buffers used for [`Context::editline()`]. cached_text_buffers: Vec<CachedTextBuffer>, /// The clipboard contents. clipboard: Vec<u8>, /// A counter that is incremented every time the clipboard changes. /// Allows for tracking clipboard changes without comparing contents. clipboard_generation: u32, settling_have: i32, settling_want: i32, read_timeout: time::Duration, } impl Tui { /// Creates a new [`Tui`] instance for storing state across frames. pub fn new() -> apperr::Result<Self> { let arena_prev = Arena::new(128 * MEBI)?; let arena_next = Arena::new(128 * MEBI)?; // SAFETY: Since `prev_tree` refers to `arena_prev`/`arena_next`, from its POV the lifetime // is `'static`, requiring us to use `transmute` to circumvent the borrow checker. let prev_tree = Tree::new(unsafe { mem::transmute::<&Arena, &Arena>(&arena_next) }); let mut tui = Self { arena_prev, arena_next, prev_tree, prev_node_map: Default::default(), framebuffer: Framebuffer::new(), modifier_translations: ModifierTranslations { ctrl: "Ctrl", alt: "Alt", shift: "Shift", }, floater_default_bg: 0, floater_default_fg: 0, modal_default_bg: 0, modal_default_fg: 0, size: Size { width: 0, height: 0 }, mouse_position: Point::MIN, mouse_down_position: Point::MIN, left_mouse_down_target: 0, mouse_up_timestamp: std::time::Instant::now(), mouse_state: InputMouseState::None, mouse_is_drag: false, mouse_click_counter: 0, mouse_down_node_path: Vec::with_capacity(16), first_click_position: Point::MIN, first_click_target: 0, focused_node_path: Vec::with_capacity(16), focused_node_for_scrolling: ROOT_ID, cached_text_buffers: Vec::with_capacity(16), clipboard: Vec::new(), clipboard_generation: 0, settling_have: 0, settling_want: 0, read_timeout: time::Duration::MAX, }; Self::clean_node_path(&mut tui.mouse_down_node_path); Self::clean_node_path(&mut tui.focused_node_path); Ok(tui) } /// Sets up the framebuffer's color palette. pub fn setup_indexed_colors(&mut self, colors: [u32; INDEXED_COLORS_COUNT]) { self.framebuffer.set_indexed_colors(colors); } /// Set up translations for Ctrl/Alt/Shift modifiers. pub fn setup_modifier_translations(&mut self, translations: ModifierTranslations) { self.modifier_translations = translations; } /// Set the default background color for floaters (dropdowns, etc.). pub fn set_floater_default_bg(&mut self, color: u32) { self.floater_default_bg = color; } /// Set the default foreground color for floaters (dropdowns, etc.). pub fn set_floater_default_fg(&mut self, color: u32) { self.floater_default_fg = color; } /// Set the default background color for modals. pub fn set_modal_default_bg(&mut self, color: u32) { self.modal_default_bg = color; } /// Set the default foreground color for modals. pub fn set_modal_default_fg(&mut self, color: u32) { self.modal_default_fg = color; } /// If the TUI is currently running animations, etc., /// this will return a timeout smaller than [`time::Duration::MAX`]. pub fn read_timeout(&mut self) -> time::Duration { mem::replace(&mut self.read_timeout, time::Duration::MAX) } /// Returns the viewport size. pub fn size(&self) -> Size { // We don't use the size stored in the framebuffer, because until // `render()` is called, the framebuffer will use a stale size. self.size } /// Returns an indexed color from the framebuffer. #[inline] pub fn indexed(&self, index: IndexedColor) -> u32 { self.framebuffer.indexed(index) } /// Returns an indexed color from the framebuffer with the given alpha. /// See [`Framebuffer::indexed_alpha()`]. #[inline] pub fn indexed_alpha(&self, index: IndexedColor, numerator: u32, denominator: u32) -> u32 { self.framebuffer.indexed_alpha(index, numerator, denominator) } /// Returns a color in contrast with the given color. /// See [`Framebuffer::contrasted()`]. pub fn contrasted(&self, color: u32) -> u32 { self.framebuffer.contrasted(color) } /// Returns the current clipboard contents. pub fn clipboard(&self) -> &[u8] { &self.clipboard } /// Returns the current clipboard generation. /// The generation changes every time the clipboard contents change. /// This allows you to track clipboard changes. pub fn clipboard_generation(&self) -> u32 { self.clipboard_generation } /// Starts a new frame and returns a [`Context`] for it. pub fn create_context<'a, 'input>( &'a mut self, input: Option<Input<'input>>, ) -> Context<'a, 'input> { // SAFETY: Since we have a unique `&mut self`, nothing is holding onto `arena_prev`, // which will become `arena_next` and get reset. It's safe to reset and reuse its memory. mem::swap(&mut self.arena_prev, &mut self.arena_next); unsafe { self.arena_next.reset(0) }; // In the input handler below we transformed a mouse up into a release event. // Now, a frame later, we must reset it back to none, to stop it from triggering things. // Same for Scroll events. if self.mouse_state > InputMouseState::Right { self.mouse_down_position = Point::MIN; self.mouse_down_node_path.clear(); self.left_mouse_down_target = 0; self.mouse_state = InputMouseState::None; self.mouse_is_drag = false; } let now = std::time::Instant::now(); let mut input_text = None; let mut input_keyboard = None; let mut input_mouse_modifiers = kbmod::NONE; let mut input_mouse_click = 0; let mut input_scroll_delta = Point { x: 0, y: 0 }; // `input_consumed` should be `true` if we're in the settling phase which is indicated by // `self.needs_settling() == true`. However, there's a possibility for it being true from // a previous frame, and we do have fresh new input. In that case want `input_consumed` // to be false of course which is ensured by checking for `input.is_none()`. let input_consumed = self.needs_settling() && input.is_none(); if self.scroll_to_focused() { self.needs_more_settling(); } match input { None => {} Some(Input::Resize(resize)) => { assert!(resize.width > 0 && resize.height > 0); assert!(resize.width < 32768 && resize.height < 32768); self.size = resize; } Some(Input::Text(text)) => { input_text = Some(text); // TODO: the .len()==1 check causes us to ignore keyboard inputs that are faster than we process them. // For instance, imagine the user presses "A" twice and we happen to read it in a single chunk. // This causes us to ignore the keyboard input here. We need a way to inform the caller over // how much of the input text we actually processed in a single frame. Or perhaps we could use // the needs_settling logic? if !text.bracketed && text.text.len() == 1 { let ch = text.text.as_bytes()[0]; input_keyboard = InputKey::from_ascii(ch as char) } } Some(Input::Keyboard(keyboard)) => { input_keyboard = Some(keyboard); } Some(Input::Mouse(mouse)) => { let mut next_state = mouse.state; let next_position = mouse.position; let next_scroll = mouse.scroll; let mouse_down = self.mouse_state == InputMouseState::None && next_state != InputMouseState::None; let mouse_up = self.mouse_state != InputMouseState::None && next_state == InputMouseState::None; let is_drag = self.mouse_state == InputMouseState::Left && next_state == InputMouseState::Left && next_position != self.mouse_position; let mut hovered_node = None; // Needed for `mouse_down` let mut focused_node = None; // Needed for `mouse_down` and `is_click` if mouse_down || mouse_up { for root in self.prev_tree.iterate_roots() { Tree::visit_all(root, root, true, |node| { let n = node.borrow(); if !n.outer_clipped.contains(next_position) { // Skip the entire sub-tree, because it doesn't contain the cursor. return VisitControl::SkipChildren; } hovered_node = Some(node); if n.attributes.focusable { focused_node = Some(node); } VisitControl::Continue }); } } if mouse_down { // Transition from no mouse input to some mouse input --> Record the mouse down position. Self::build_node_path(hovered_node, &mut self.mouse_down_node_path); // On left-mouse-down we change focus. let mut target = 0; if next_state == InputMouseState::Left { target = focused_node.map_or(0, |n| n.borrow().id); Self::build_node_path(focused_node, &mut self.focused_node_path); self.needs_more_settling(); // See `needs_more_settling()`. } // Double-/Triple-/Etc.-clicks are triggered on mouse-down, // unlike the first initial click, which is triggered on mouse-up. if self.mouse_click_counter != 0 { if self.first_click_target != target || self.first_click_position != next_position || (now - self.mouse_up_timestamp) > std::time::Duration::from_millis(500) { // If the cursor moved / the focus changed in between, or if the user did a slow click, // we reset the click counter. On mouse-up it'll transition to a regular click. self.mouse_click_counter = 0; self.first_click_position = Point::MIN; self.first_click_target = 0; } else { self.mouse_click_counter += 1; input_mouse_click = self.mouse_click_counter; }; } // Gets reset at the start of this function. self.left_mouse_down_target = target; self.mouse_down_position = next_position; } else if mouse_up { // Transition from some mouse input to no mouse input --> The mouse button was released. next_state = InputMouseState::Release; let target = focused_node.map_or(0, |n| n.borrow().id); if self.left_mouse_down_target == 0 || self.left_mouse_down_target != target { // If `left_mouse_down_target == 0`, then it wasn't a left-click, in which case // the target gets reset. Same, if the focus changed in between any clicks. self.mouse_click_counter = 0; self.first_click_position = Point::MIN; self.first_click_target = 0; } else if self.mouse_click_counter == 0 { // No focus change, and no previous clicks? This is an initial, regular click. self.mouse_click_counter = 1; self.first_click_position = self.mouse_down_position; self.first_click_target = target; input_mouse_click = 1; } self.mouse_up_timestamp = now; } else if is_drag { self.mouse_is_drag = true; } input_mouse_modifiers = mouse.modifiers; input_scroll_delta = next_scroll; self.mouse_position = next_position; self.mouse_state = next_state; } } if !input_consumed { // Every time there's input, we naturally need to re-render at least once. self.settling_have = 0; self.settling_want = 1; } // TODO: There should be a way to do this without unsafe. // Allocating from the arena borrows the arena, and so allocating the tree here borrows self. // This conflicts with us passing a mutable reference to `self` into the struct below. let tree = Tree::new(unsafe { mem::transmute::<&Arena, &Arena>(&self.arena_next) }); Context { tui: self, input_text, input_keyboard, input_mouse_modifiers, input_mouse_click, input_scroll_delta, input_consumed, tree, last_modal: None, focused_node: None, next_block_id_mixin: 0, needs_settling: false, #[cfg(debug_assertions)] seen_ids: HashSet::new(), } } fn report_context_completion<'a>(&'a mut self, ctx: &mut Context<'a, '_>) { // If this hits, you forgot to block_end() somewhere. The best way to figure // out where is to do a binary search of commenting out code in main.rs. debug_assert!( ctx.tree.current_node.borrow().stack_parent.is_none(), "Dangling parent! Did you miss a block_end?" ); // End the root node. ctx.block_end(); // Ensure that focus doesn't escape the active modal. if let Some(node) = ctx.last_modal && !self.is_subtree_focused(&node.borrow()) { ctx.steal_focus_for(node); } // If nodes have appeared or disappeared, we need to re-render. // Same, if the focus has changed (= changes the highlight color, etc.). let mut needs_settling = ctx.needs_settling; needs_settling |= self.prev_tree.checksum != ctx.tree.checksum; // Adopt the new tree and recalculate the node hashmap. // // SAFETY: The memory used by the tree is owned by the `self.arena_next` right now. // Stealing the tree here thus doesn't need to copy any memory unless someone resets the arena. // (The arena is reset in `reset()` above.) unsafe { self.prev_tree = mem::transmute_copy(&ctx.tree); self.prev_node_map = NodeMap::new(mem::transmute(&self.arena_next), &self.prev_tree); } let mut focus_path_pop_min = 0; // If the user pressed Escape, we move the focus to a parent node. if !ctx.input_consumed && ctx.consume_shortcut(vk::ESCAPE) { focus_path_pop_min = 1; } // Remove any unknown nodes from the focus path. // It's important that we do this after the tree has been swapped out, // so that pop_focusable_node() has access to the newest version of the tree. needs_settling |= self.pop_focusable_node(focus_path_pop_min); // `needs_more_settling()` depends on the current value // of `settling_have` and so we increment it first. self.settling_have += 1; if needs_settling { self.needs_more_settling(); } // Remove cached text editors that are no longer in use. self.cached_text_buffers.retain(|c| c.seen); for root in Tree::iterate_siblings(Some(self.prev_tree.root_first)) { let mut root = root.borrow_mut(); root.compute_intrinsic_size(); } let viewport = self.size.as_rect(); for root in Tree::iterate_siblings(Some(self.prev_tree.root_first)) { let mut root = root.borrow_mut(); let root = &mut *root; if let Some(float) = &root.attributes.float { let mut x = 0; let mut y = 0; if let Some(node) = root.parent { let node = node.borrow(); x = node.outer.left; y = node.outer.top; } let size = root.intrinsic_to_outer(); x += (float.offset_x - float.gravity_x * size.width as f32) as CoordType; y += (float.offset_y - float.gravity_y * size.height as f32) as CoordType; root.outer.left = x; root.outer.top = y; root.outer.right = x + size.width; root.outer.bottom = y + size.height; root.outer = root.outer.intersect(viewport); } else { root.outer = viewport; } root.inner = root.outer_to_inner(root.outer); root.outer_clipped = root.outer; root.inner_clipped = root.inner; let outer = root.outer; root.layout_children(outer); } } fn build_node_path(node: Option<&NodeCell>, path: &mut Vec<u64>) { path.clear(); if let Some(mut node) = node { loop { let n = node.borrow(); path.push(n.id); node = match n.parent { Some(parent) => parent, None => break, }; } path.reverse(); } else { path.push(ROOT_ID); } } fn clean_node_path(path: &mut Vec<u64>) { Self::build_node_path(None, path); } /// After you finished processing all input, continue redrawing your UI until this returns false. pub fn needs_settling(&mut self) -> bool { self.settling_have <= self.settling_want } fn needs_more_settling(&mut self) { // If the focus has changed, the new node may need to be re-rendered. // Same, every time we encounter a previously unknown node via `get_prev_node`, // because that means it likely failed to get crucial information such as the layout size. if cfg!(debug_assertions) && self.settling_have == 15 { breakpoint(); } self.settling_want = (self.settling_have + 1).min(20); } /// Renders the last frame into the framebuffer and returns the VT output. pub fn render<'a>(&mut self, arena: &'a Arena) -> ArenaString<'a> { self.framebuffer.flip(self.size); for child in self.prev_tree.iterate_roots() { let mut child = child.borrow_mut(); self.render_node(&mut child); } self.framebuffer.render(arena) } /// Recursively renders each node and its children. #[allow(clippy::only_used_in_recursion)] fn render_node(&mut self, node: &mut Node) { let outer_clipped = node.outer_clipped; if outer_clipped.is_empty() { return; } let scratch = scratch_arena(None); if node.attributes.bordered { // ┌────┐ { let mut fill = ArenaString::new_in(&scratch); fill.push('┌'); fill.push_repeat('─', (outer_clipped.right - outer_clipped.left - 2) as usize); fill.push('┐'); self.framebuffer.replace_text( outer_clipped.top, outer_clipped.left, outer_clipped.right, &fill, ); } // │ │ { let mut fill = ArenaString::new_in(&scratch); fill.push('│'); fill.push_repeat(' ', (outer_clipped.right - outer_clipped.left - 2) as usize); fill.push('│'); for y in outer_clipped.top + 1..outer_clipped.bottom - 1 { self.framebuffer.replace_text( y, outer_clipped.left, outer_clipped.right, &fill, ); } } // └────┘ { let mut fill = ArenaString::new_in(&scratch); fill.push('└'); fill.push_repeat('─', (outer_clipped.right - outer_clipped.left - 2) as usize); fill.push('┘'); self.framebuffer.replace_text( outer_clipped.bottom - 1, outer_clipped.left, outer_clipped.right, &fill, ); } } if node.attributes.float.is_some() && node.attributes.bg & 0xff000000 == 0xff000000 { if !node.attributes.bordered { let mut fill = ArenaString::new_in(&scratch); fill.push_repeat(' ', (outer_clipped.right - outer_clipped.left) as usize); for y in outer_clipped.top..outer_clipped.bottom { self.framebuffer.replace_text( y, outer_clipped.left, outer_clipped.right, &fill, ); } } self.framebuffer.replace_attr(outer_clipped, Attributes::All, Attributes::None); } self.framebuffer.blend_bg(outer_clipped, node.attributes.bg); self.framebuffer.blend_fg(outer_clipped, node.attributes.fg); if node.attributes.reverse { self.framebuffer.reverse(outer_clipped); } let inner = node.inner; let inner_clipped = node.inner_clipped; if inner_clipped.is_empty() { return; } match &mut node.content { NodeContent::Modal(title) => { if !title.is_empty() { self.framebuffer.replace_text( node.outer.top, node.outer.left + 2, node.outer.right - 1, title, ); } } NodeContent::Text(content) => self.render_styled_text( inner, node.intrinsic_size.width, &content.text, &content.chunks, content.overflow, ), NodeContent::Textarea(tc) => { let mut tb = tc.buffer.borrow_mut(); let mut destination = Rect { left: inner_clipped.left, top: inner_clipped.top, right: inner_clipped.right, bottom: inner_clipped.bottom, }; if !tc.single_line { // Account for the scrollbar. destination.right -= 1; } if let Some(res) = tb.render(tc.scroll_offset, destination, tc.has_focus, &mut self.framebuffer) { tc.scroll_offset_x_max = res.visual_pos_x_max; } if !tc.single_line { // Render the scrollbar. let track = Rect { left: inner_clipped.right - 1, top: inner_clipped.top, right: inner_clipped.right, bottom: inner_clipped.bottom, }; tc.thumb_height = self.framebuffer.draw_scrollbar( inner_clipped, track, tc.scroll_offset.y, tb.visual_line_count() + inner.height() - 1, ); } } NodeContent::Scrollarea(sc) => { let content = node.children.first.unwrap().borrow(); let track = Rect { left: inner.right, top: inner.top, right: inner.right + 1, bottom: inner.bottom, }; sc.thumb_height = self.framebuffer.draw_scrollbar( outer_clipped, track, sc.scroll_offset.y, content.intrinsic_size.height, ); } _ => {} } for child in Tree::iterate_siblings(node.children.first) { let mut child = child.borrow_mut(); self.render_node(&mut child); } } fn render_styled_text( &mut self, target: Rect, actual_width: CoordType, text: &str, chunks: &[StyledTextChunk], overflow: Overflow, ) { let target_width = target.width(); // The section of `text` that is skipped by the ellipsis. let mut skipped = 0..0; // The number of columns skipped by the ellipsis. let mut skipped_cols = 0; if overflow == Overflow::Clip || target_width >= actual_width { self.framebuffer.replace_text(target.top, target.left, target.right, text); } else { let bytes = text.as_bytes(); let mut cfg = unicode::MeasurementConfig::new(&bytes); match overflow { Overflow::Clip => unreachable!(), Overflow::TruncateHead => { let beg = cfg.goto_visual(Point { x: actual_width - target_width + 1, y: 0 }); skipped = 0..beg.offset; skipped_cols = beg.visual_pos.x - 1; } Overflow::TruncateMiddle => { let mid_beg_x = (target_width - 1) / 2; let mid_end_x = actual_width - target_width / 2; let beg = cfg.goto_visual(Point { x: mid_beg_x, y: 0 }); let end = cfg.goto_visual(Point { x: mid_end_x, y: 0 }); skipped = beg.offset..end.offset; skipped_cols = end.visual_pos.x - beg.visual_pos.x - 1; } Overflow::TruncateTail => { let end = cfg.goto_visual(Point { x: target_width - 1, y: 0 }); skipped_cols = actual_width - end.visual_pos.x - 1; skipped = end.offset..text.len(); } } let scratch = scratch_arena(None); let mut modified = ArenaString::new_in(&scratch); modified.reserve(text.len() + 3); modified.push_str(&text[..skipped.start]); modified.push('…'); modified.push_str(&text[skipped.end..]); self.framebuffer.replace_text(target.top, target.left, target.right, &modified); } if !chunks.is_empty() { let bytes = text.as_bytes(); let mut cfg = unicode::MeasurementConfig::new(&bytes).with_cursor(unicode::Cursor { visual_pos: Point { x: target.left, y: 0 }, ..Default::default() }); let mut iter = chunks.iter().peekable(); while let Some(chunk) = iter.next() { let beg = chunk.offset; let end = iter.peek().map_or(text.len(), |c| c.offset); if beg >= skipped.start && end <= skipped.end { // Chunk is fully inside the text skipped by the ellipsis. // We don't need to render it at all. continue; } if beg < skipped.start { let beg = cfg.goto_offset(beg).visual_pos.x; let end = cfg.goto_offset(end.min(skipped.start)).visual_pos.x; let rect = Rect { left: beg, top: target.top, right: end, bottom: target.bottom }; self.framebuffer.blend_fg(rect, chunk.fg); self.framebuffer.replace_attr(rect, chunk.attr, chunk.attr); } if end > skipped.end { let beg = cfg.goto_offset(beg.max(skipped.end)).visual_pos.x - skipped_cols; let end = cfg.goto_offset(end).visual_pos.x - skipped_cols; let rect = Rect { left: beg, top: target.top, right: end, bottom: target.bottom }; self.framebuffer.blend_fg(rect, chunk.fg); self.framebuffer.replace_attr(rect, chunk.attr, chunk.attr); } } } } /// Outputs a debug string of the layout and focus tree. pub fn debug_layout<'a>(&mut self, arena: &'a Arena) -> ArenaString<'a> { let mut result = ArenaString::new_in(arena); result.push_str("general:\r\n- focus_path:\r\n"); for &id in &self.focused_node_path { _ = write!(result, " - {id:016x}\r\n"); } result.push_str("\r\ntree:\r\n"); for root in self.prev_tree.iterate_roots() { Tree::visit_all(root, root, true, |node| { let node = node.borrow(); let depth = node.depth; result.push_repeat(' ', depth * 2); _ = write!(result, "- id: {:016x}\r\n", node.id); result.push_repeat(' ', depth * 2); _ = write!(result, " classname: {}\r\n", node.classname); if depth == 0 && let Some(parent) = node.parent { let parent = parent.borrow(); result.push_repeat(' ', depth * 2); _ = write!(result, " parent: {:016x}\r\n", parent.id); } result.push_repeat(' ', depth * 2); _ = write!( result, " intrinsic: {{{}, {}}}\r\n", node.intrinsic_size.width, node.intrinsic_size.height ); result.push_repeat(' ', depth * 2); _ = write!( result, " outer: {{{}, {}, {}, {}}}\r\n", node.outer.left, node.outer.top, node.outer.right, node.outer.bottom ); result.push_repeat(' ', depth * 2); _ = write!( result, " inner: {{{}, {}, {}, {}}}\r\n", node.inner.left, node.inner.top, node.inner.right, node.inner.bottom ); if node.attributes.bordered { result.push_repeat(' ', depth * 2); result.push_str(" bordered: true\r\n"); } if node.attributes.bg != 0 { result.push_repeat(' ', depth * 2); _ = write!(result, " bg: #{:08x}\r\n", node.attributes.bg); } if node.attributes.fg != 0 { result.push_repeat(' ', depth * 2); _ = write!(result, " fg: #{:08x}\r\n", node.attributes.fg); } if self.is_node_focused(node.id) { result.push_repeat(' ', depth * 2); result.push_str(" focused: true\r\n"); } match &node.content { NodeContent::Text(content) => { result.push_repeat(' ', depth * 2); _ = write!(result, " text: \"{}\"\r\n", &content.text); } NodeContent::Textarea(content) => { let tb = content.buffer.borrow(); let tb = &*tb; result.push_repeat(' ', depth * 2); _ = write!(result, " textarea: {tb:p}\r\n"); } NodeContent::Scrollarea(..) => { result.push_repeat(' ', depth * 2); result.push_str(" scrollable: true\r\n"); } _ => {} } VisitControl::Continue }); } result } fn was_mouse_down_on_node(&self, id: u64) -> bool { self.mouse_down_node_path.last() == Some(&id) } fn was_mouse_down_on_subtree(&self, node: &Node) -> bool { self.mouse_down_node_path.get(node.depth) == Some(&node.id) } fn is_node_focused(&self, id: u64) -> bool { // We construct the focused_node_path always with at least 1 element (the root id). unsafe { *self.focused_node_path.last().unwrap_unchecked() == id } } fn is_subtree_focused(&self, node: &Node) -> bool { self.focused_node_path.get(node.depth) == Some(&node.id) } fn is_subtree_focused_alt(&self, id: u64, depth: usize) -> bool { self.focused_node_path.get(depth) == Some(&id) } fn pop_focusable_node(&mut self, pop_minimum: usize) -> bool { let last_before = self.focused_node_path.last().cloned().unwrap_or(0); // Remove `pop_minimum`-many nodes from the end of the focus path. let path = &self.focused_node_path[..]; let path = &path[..path.len().saturating_sub(pop_minimum)]; let mut len = 0; for (i, &id) in path.iter().enumerate() { // Truncate the path so that it only contains nodes that still exist. let Some(node) = self.prev_node_map.get(id) else { break; }; let n = node.borrow(); // If the caller requested upward movement, pop out of the current focus void, if any. // This is kind of janky, to be fair. if pop_minimum != 0 && n.attributes.focus_void { break; } // Skip over those that aren't focusable. if n.attributes.focusable { // At this point `n.depth == i` should be true, // but I kind of don't want to rely on that. len = i + 1; } } self.focused_node_path.truncate(len); // If it's empty now, push `ROOT_ID` because there must always be >=1 element. if self.focused_node_path.is_empty() { self.focused_node_path.push(ROOT_ID); } // Return true if the focus path changed. let last_after = self.focused_node_path.last().cloned().unwrap_or(0); last_before != last_after } // Scroll the focused node(s) into view inside scrollviews fn scroll_to_focused(&mut self) -> bool { let focused_id = self.focused_node_path.last().cloned().unwrap_or(0); if self.focused_node_for_scrolling == focused_id { return false; } let Some(node) = self.prev_node_map.get(focused_id) else { // Node not found because we're using the old layout tree. // Retry in the next rendering loop. return true; }; let mut node = node.borrow_mut(); let mut scroll_to = node.outer; while node.parent.is_some() && node.attributes.float.is_none() { let n = &mut *node; if let NodeContent::Scrollarea(sc) = &mut n.content { let off_y = sc.scroll_offset.y.max(0); let mut y = off_y; y = y.min(scroll_to.top - n.inner.top + off_y); y = y.max(scroll_to.bottom - n.inner.bottom + off_y); sc.scroll_offset.y = y; scroll_to = n.outer; } node = node.parent.unwrap().borrow_mut(); } self.focused_node_for_scrolling = focused_id; true } } /// Context is a temporary object that is created for each frame. /// Its primary purpose is to build a UI tree. pub struct Context<'a, 'input> { tui: &'a mut Tui, /// Current text input, if any. input_text: Option<InputText<'input>>, /// Current keyboard input, if any. input_keyboard: Option<InputKey>, input_mouse_modifiers: InputKeyMod, input_mouse_click: CoordType, /// By how much the mouse wheel was scrolled since the last frame. input_scroll_delta: Point, input_consumed: bool, tree: Tree<'a>, last_modal: Option<&'a NodeCell<'a>>, focused_node: Option<&'a NodeCell<'a>>, next_block_id_mixin: u64, needs_settling: bool, #[cfg(debug_assertions)] seen_ids: HashSet<u64>, } impl<'a> Drop for Context<'a, '_> { fn drop(&mut self) { let tui: &'a mut Tui = unsafe { mem::transmute(&mut *self.tui) }; tui.report_context_completion(self); } } impl<'a> Context<'a, '_> { /// Get an arena for temporary allocations such as for [`arena_format`]. pub fn arena(&self) -> &'a Arena { // TODO: // `Context` borrows `Tui` for lifetime 'a, so `self.tui` should be `&'a Tui`, right? // And if I do `&self.tui.arena` then that should be 'a too, right? // Searching for and failing to find a workaround for this was _very_ annoying. // // SAFETY: Both the returned reference and its allocations outlive &self. unsafe { mem::transmute::<&'_ Arena, &'a Arena>(&self.tui.arena_next) } } /// Returns the viewport size. pub fn size(&self) -> Size { self.tui.size() } /// Returns an indexed color from the framebuffer. #[inline] pub fn indexed(&self, index: IndexedColor) -> u32 { self.tui.framebuffer.indexed(index) } /// Returns an indexed color from the framebuffer with the given alpha. /// See [`Framebuffer::indexed_alpha()`]. #[inline] pub fn indexed_alpha(&self, index: IndexedColor, numerator: u32, denominator: u32) -> u32 { self.tui.framebuffer.indexed_alpha(index, numerator, denominator) } /// Returns a color in contrast with the given color. /// See [`Framebuffer::contrasted()`]. pub fn contrasted(&self, color: u32) -> u32 { self.tui.framebuffer.contrasted(color) } /// Returns the current clipboard contents. pub fn clipboard(&self) -> &[u8] { self.tui.clipboard() } /// Returns the current clipboard generation. /// The generation changes every time the clipboard contents change. /// This allows you to track clipboard changes. pub fn clipboard_generation(&self) -> u32 { self.tui.clipboard_generation() } /// Sets the clipboard contents. pub fn set_clipboard(&mut self, data: Vec<u8>) { if !data.is_empty() { self.tui.clipboard = data; self.tui.clipboard_generation = self.tui.clipboard_generation.wrapping_add(1); self.needs_rerender(); } } /// Tell the UI framework that your state changed and you need another layout pass. pub fn needs_rerender(&mut self) { // If this hits, the call stack is responsible is trying to deadlock you. debug_assert!(self.tui.settling_have < 15); self.needs_settling = true; } /// Begins a generic UI block (container) with a unique ID derived from the given `classname`. pub fn block_begin(&mut self, classname: &'static str) { let parent = self.tree.current_node; let mut id = hash_str(parent.borrow().id, classname); if self.next_block_id_mixin != 0 { id = hash(id, &self.next_block_id_mixin.to_ne_bytes()); self.next_block_id_mixin = 0; } // If this hits, you have tried to create a block with the same ID as a previous one // somewhere up this call stack. Change the classname, or use next_block_id_mixin(). // TODO: HashMap #[cfg(debug_assertions)] if !self.seen_ids.insert(id) { panic!("Duplicate node ID: {id:x}"); } let node = Tree::alloc_node(self.arena()); { let mut n = node.borrow_mut(); n.id = id; n.classname = classname; } self.tree.push_child(node); } /// Ends the current UI block, returning to its parent container. pub fn block_end(&mut self) { self.tree.pop_stack(); self.block_end_move_focus(); } fn block_end_move_focus(&mut self) { // At this point, it's more like "focus_well?" instead of "focus_well!". let focus_well = self.tree.last_node; // Remember the focused node, if any, because once the code below runs, // we need it for the `Tree::visit_all` call. if self.is_focused() { self.focused_node = Some(focus_well); } // The mere fact that there's a `focused_node` indicates that we're the // first `block_end()` call that's a focus well and also contains the focus. let Some(focused) = self.focused_node else { return; }; // Filter down to nodes that are focus wells and contain the focus. They're // basically the "tab container". We test for the node depth to ensure that // we don't accidentally pick a focus well next to or inside the focused node. { let n = focus_well.borrow(); if !n.attributes.focus_well || n.depth > focused.borrow().depth { return; } } // Filter down to Tab/Shift+Tab inputs. if self.input_consumed { return; } let Some(input) = self.input_keyboard else { return; }; if !matches!(input, SHIFT_TAB | vk::TAB) { return; } let forward = input == vk::TAB; let mut focused_start = focused; let mut focused_next = focused; // We may be in a focus void right now (= doesn't want to be tabbed into), // so first we must go up the tree until we're outside of it. loop { if ptr::eq(focused_start, focus_well) { // If we hit the root / focus well, we weren't in a focus void, // and can reset `focused_before` to the current focused node. focused_start = focused; break; } focused_start = focused_start.borrow().parent.unwrap(); if focused_start.borrow().attributes.focus_void { break; } } Tree::visit_all(focus_well, focused_start, forward, |node| { let n = node.borrow(); if n.attributes.focusable && !ptr::eq(node, focused_start) { focused_next = node; VisitControl::Stop } else if n.attributes.focus_void { VisitControl::SkipChildren } else { VisitControl::Continue } }); if ptr::eq(focused_next, focused_start) { return; } Tui::build_node_path(Some(focused_next), &mut self.tui.focused_node_path); self.set_input_consumed(); self.needs_rerender(); } /// Mixes in an extra value to the next UI block's ID for uniqueness. /// Use this when you build a list of items with the same classname. pub fn next_block_id_mixin(&mut self, id: u64) { self.next_block_id_mixin = id; } fn attr_focusable(&mut self) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.focusable = true; } /// If this is the first time the current node is being drawn, /// it'll steal the active focus. pub fn focus_on_first_present(&mut self) { let steal = { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.focusable = true; self.tui.prev_node_map.get(last_node.id).is_none() }; if steal { self.steal_focus(); } } /// Steals the focus unconditionally. pub fn steal_focus(&mut self) { self.steal_focus_for(self.tree.last_node); } fn steal_focus_for(&mut self, node: &NodeCell<'a>) { if !self.tui.is_node_focused(node.borrow().id) { Tui::build_node_path(Some(node), &mut self.tui.focused_node_path); self.needs_rerender(); } } /// If the current node owns the focus, it'll be given to the parent. pub fn toss_focus_up(&mut self) { if self.tui.pop_focusable_node(1) { self.needs_rerender(); } } /// If the parent node owns the focus, it'll be given to the current node. pub fn inherit_focus(&mut self) { let mut last_node = self.tree.last_node.borrow_mut(); let Some(parent) = last_node.parent else { return; }; last_node.attributes.focusable = true; // Mark the parent as focusable, so that if the user presses Escape, // and `block_end` bubbles the focus up the tree, it'll stop on our parent, // which will then focus us on the next iteration. let mut parent = parent.borrow_mut(); parent.attributes.focusable = true; if self.tui.is_node_focused(parent.id) { self.needs_rerender(); self.tui.focused_node_path.push(last_node.id); } } /// Causes keyboard focus to be unable to escape this node and its children. /// It's a "well" because if the focus is inside it, it can't escape. pub fn attr_focus_well(&mut self) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.focus_well = true; } /// Explicitly sets the intrinsic size of the current node. /// The intrinsic size is the size the node ideally wants to be. pub fn attr_intrinsic_size(&mut self, size: Size) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.intrinsic_size = size; last_node.intrinsic_size_set = true; } /// Turns the current node into a floating node, /// like a popup, modal or a tooltip. pub fn attr_float(&mut self, spec: FloatSpec) { let last_node = self.tree.last_node; let anchor = { let ln = last_node.borrow(); match spec.anchor { Anchor::Last if ln.siblings.prev.is_some() => ln.siblings.prev, Anchor::Last | Anchor::Parent => ln.parent, // By not giving such floats a parent, they get the same origin as the original root node, // but they also gain their own "root id" in the tree. That way, their focus path is totally unique, // which means that we can easily check if a modal is open by calling `is_focused()` on the original root. Anchor::Root => None, } }; self.tree.move_node_to_root(last_node, anchor); let mut ln = last_node.borrow_mut(); ln.attributes.focus_well = true; ln.attributes.float = Some(FloatAttributes { gravity_x: spec.gravity_x.clamp(0.0, 1.0), gravity_y: spec.gravity_y.clamp(0.0, 1.0), offset_x: spec.offset_x, offset_y: spec.offset_y, }); ln.attributes.bg = self.tui.floater_default_bg; ln.attributes.fg = self.tui.floater_default_fg; } /// Gives the current node a border. pub fn attr_border(&mut self) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.bordered = true; } /// Sets the current node's position inside the parent. pub fn attr_position(&mut self, align: Position) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.position = align; } /// Assigns padding to the current node. pub fn attr_padding(&mut self, padding: Rect) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.padding = Self::normalize_rect(padding); } fn normalize_rect(rect: Rect) -> Rect { Rect { left: rect.left.max(0), top: rect.top.max(0), right: rect.right.max(0), bottom: rect.bottom.max(0), } } /// Assigns a sRGB background color to the current node. pub fn attr_background_rgba(&mut self, bg: u32) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.bg = bg; } /// Assigns a sRGB foreground color to the current node. pub fn attr_foreground_rgba(&mut self, fg: u32) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.fg = fg; } /// Applies reverse-video to the current node: /// Background and foreground colors are swapped. pub fn attr_reverse(&mut self) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.reverse = true; } /// Checks if the current keyboard input matches the given shortcut, /// consumes it if it is and returns true in that case. pub fn consume_shortcut(&mut self, shortcut: InputKey) -> bool { if !self.input_consumed && self.input_keyboard == Some(shortcut) { self.set_input_consumed(); true } else { false } } /// Returns current keyboard input, if any. /// Returns None if the input was already consumed. pub fn keyboard_input(&self) -> Option<InputKey> { if self.input_consumed { None } else { self.input_keyboard } } #[inline] pub fn set_input_consumed(&mut self) { debug_assert!(!self.input_consumed); self.set_input_consumed_unchecked(); } #[inline] fn set_input_consumed_unchecked(&mut self) { self.input_consumed = true; } /// Returns whether the mouse was pressed down on the current node. pub fn was_mouse_down(&mut self) -> bool { let last_node = self.tree.last_node.borrow(); self.tui.was_mouse_down_on_node(last_node.id) } /// Returns whether the mouse was pressed down on the current node's subtree. pub fn contains_mouse_down(&mut self) -> bool { let last_node = self.tree.last_node.borrow(); self.tui.was_mouse_down_on_subtree(&last_node) } /// Returns whether the current node is focused. pub fn is_focused(&mut self) -> bool { let last_node = self.tree.last_node.borrow(); self.tui.is_node_focused(last_node.id) } /// Returns whether the current node's subtree is focused. pub fn contains_focus(&mut self) -> bool { let last_node = self.tree.last_node.borrow(); self.tui.is_subtree_focused(&last_node) } /// Begins a modal window. Call [`Context::modal_end()`]. pub fn modal_begin(&mut self, classname: &'static str, title: &str) { self.block_begin(classname); self.attr_float(FloatSpec { anchor: Anchor::Root, ..Default::default() }); self.attr_intrinsic_size(Size { width: self.tui.size.width, height: self.tui.size.height }); self.attr_background_rgba(self.indexed_alpha(IndexedColor::Background, 1, 2)); self.attr_foreground_rgba(self.indexed_alpha(IndexedColor::Background, 1, 2)); self.attr_focus_well(); self.block_begin("window"); self.attr_float(FloatSpec { anchor: Anchor::Last, gravity_x: 0.5, gravity_y: 0.5, offset_x: self.tui.size.width as f32 * 0.5, offset_y: self.tui.size.height as f32 * 0.5, }); self.attr_border(); self.attr_background_rgba(self.tui.modal_default_bg); self.attr_foreground_rgba(self.tui.modal_default_fg); self.inherit_focus(); self.focus_on_first_present(); let mut last_node = self.tree.last_node.borrow_mut(); let title = if title.is_empty() { ArenaString::new_in(self.arena()) } else { arena_format!(self.arena(), " {} ", title) }; last_node.content = NodeContent::Modal(title); self.last_modal = Some(self.tree.last_node); } /// Ends the current modal window block. /// Returns true if the user pressed Escape (a request to close). pub fn modal_end(&mut self) -> bool { self.block_end(); self.block_end(); // Consume the input unconditionally, so that the root (the "main window") // doesn't accidentally receive any input via `consume_shortcut()`. if self.contains_focus() { let exit = !self.input_consumed && self.input_keyboard == Some(vk::ESCAPE); self.set_input_consumed_unchecked(); exit } else { false } } /// Begins a table block. Call [`Context::table_end()`]. /// Tables are the primary way to create a grid layout, /// and to layout controls on a single row (= a table with 1 row). pub fn table_begin(&mut self, classname: &'static str) { self.block_begin(classname); let mut last_node = self.tree.last_node.borrow_mut(); last_node.content = NodeContent::Table(TableContent { columns: Vec::new_in(self.arena()), cell_gap: Default::default(), }); } /// Assigns widths to the columns of the current table. /// By default, the table will left-align all columns. pub fn table_set_columns(&mut self, columns: &[CoordType]) { let mut last_node = self.tree.last_node.borrow_mut(); if let NodeContent::Table(spec) = &mut last_node.content { spec.columns.clear(); spec.columns.extend_from_slice(columns); } else { debug_assert!(false); } } /// Assigns the gap between cells in the current table. pub fn table_set_cell_gap(&mut self, cell_gap: Size) { let mut last_node = self.tree.last_node.borrow_mut(); if let NodeContent::Table(spec) = &mut last_node.content { spec.cell_gap = cell_gap; } else { debug_assert!(false); } } /// Starts the next row in the current table. pub fn table_next_row(&mut self) { { let current_node = self.tree.current_node.borrow(); // If this is the first call to table_next_row() inside a new table, the // current_node will refer to the table. Otherwise, it'll refer to the current row. if !matches!(current_node.content, NodeContent::Table(_)) { let Some(parent) = current_node.parent else { return; }; let parent = parent.borrow(); // Neither the current nor its parent nodes are a table? // You definitely called this outside of a table block. debug_assert!(matches!(parent.content, NodeContent::Table(_))); self.block_end(); self.table_end_row(); self.next_block_id_mixin(parent.child_count as u64); } } self.block_begin("row"); } fn table_end_row(&mut self) { self.table_move_focus(vk::LEFT, vk::RIGHT); } /// Ends the current table block. pub fn table_end(&mut self) { let current_node = self.tree.current_node.borrow(); // If this is the first call to table_next_row() inside a new table, the // current_node will refer to the table. Otherwise, it'll refer to the current row. if !matches!(current_node.content, NodeContent::Table(_)) { self.block_end(); self.table_end_row(); } self.block_end(); // table self.table_move_focus(vk::UP, vk::DOWN); } fn table_move_focus(&mut self, prev_key: InputKey, next_key: InputKey) { // Filter down to table rows that are focused. if !self.contains_focus() { return; } // Filter down to our prev/next inputs. if self.input_consumed { return; } let Some(input) = self.input_keyboard else { return; }; if input != prev_key && input != next_key { return; } let container = self.tree.last_node; let Some(&focused_id) = self.tui.focused_node_path.get(container.borrow().depth + 1) else { return; }; let mut prev_next = NodeSiblings { prev: None, next: None }; let mut focused = None; // Iterate through the cells in the row / the rows in the table, looking for focused_id. // Take note of the previous and next focusable cells / rows around the focused one. for cell in Tree::iterate_siblings(container.borrow().children.first) { let n = cell.borrow(); if n.id == focused_id { focused = Some(cell); } else if n.attributes.focusable { if focused.is_none() { prev_next.prev = Some(cell); } else { prev_next.next = Some(cell); break; } } } if focused.is_none() { return; } let forward = input == next_key; let children_idx = if forward { NodeChildren::FIRST } else { NodeChildren::LAST }; let siblings_idx = if forward { NodeSiblings::NEXT } else { NodeSiblings::PREV }; let Some(focused_next) = prev_next.get(siblings_idx).or_else(|| container.borrow().children.get(children_idx)) else { return; }; Tui::build_node_path(Some(focused_next), &mut self.tui.focused_node_path); self.set_input_consumed(); self.needs_rerender(); } /// Creates a simple text label. pub fn label(&mut self, classname: &'static str, text: &str) { self.styled_label_begin(classname); self.styled_label_add_text(text); self.styled_label_end(); } /// Creates a styled text label. /// /// # Example /// ``` /// use edit::framebuffer::IndexedColor; /// use edit::tui::Context; /// /// fn draw(ctx: &mut Context) { /// ctx.styled_label_begin("label"); /// // Shows "Hello" in the inherited foreground color. /// ctx.styled_label_add_text("Hello"); /// // Shows ", World!" next to "Hello" in red. /// ctx.styled_label_set_foreground(ctx.indexed(IndexedColor::Red)); /// ctx.styled_label_add_text(", World!"); /// } /// ``` pub fn styled_label_begin(&mut self, classname: &'static str) { self.block_begin(classname); self.tree.last_node.borrow_mut().content = NodeContent::Text(TextContent { text: ArenaString::new_in(self.arena()), chunks: Vec::with_capacity_in(4, self.arena()), overflow: Overflow::Clip, }); } /// Changes the active pencil color of the current label. pub fn styled_label_set_foreground(&mut self, fg: u32) { let mut node = self.tree.last_node.borrow_mut(); let NodeContent::Text(content) = &mut node.content else { unreachable!(); }; let last = content.chunks.last().unwrap_or(&INVALID_STYLED_TEXT_CHUNK); if last.offset != content.text.len() && last.fg != fg { content.chunks.push(StyledTextChunk { offset: content.text.len(), fg, attr: last.attr, }); } } /// Changes the active pencil attributes of the current label. pub fn styled_label_set_attributes(&mut self, attr: Attributes) { let mut node = self.tree.last_node.borrow_mut(); let NodeContent::Text(content) = &mut node.content else { unreachable!(); }; let last = content.chunks.last().unwrap_or(&INVALID_STYLED_TEXT_CHUNK); if last.offset != content.text.len() && last.attr != attr { content.chunks.push(StyledTextChunk { offset: content.text.len(), fg: last.fg, attr }); } } /// Adds text to the current label. pub fn styled_label_add_text(&mut self, text: &str) { let mut node = self.tree.last_node.borrow_mut(); let NodeContent::Text(content) = &mut node.content else { unreachable!(); }; content.text.push_str(text); } /// Ends the current label block. pub fn styled_label_end(&mut self) { { let mut last_node = self.tree.last_node.borrow_mut(); let NodeContent::Text(content) = &last_node.content else { return; }; let cursor = unicode::MeasurementConfig::new(&content.text.as_bytes()) .goto_visual(Point { x: CoordType::MAX, y: 0 }); last_node.intrinsic_size.width = cursor.visual_pos.x; last_node.intrinsic_size.height = 1; last_node.intrinsic_size_set = true; } self.block_end(); } /// Sets the overflow behavior of the current label. pub fn attr_overflow(&mut self, overflow: Overflow) { let mut last_node = self.tree.last_node.borrow_mut(); let NodeContent::Text(content) = &mut last_node.content else { return; }; content.overflow = overflow; } /// Creates a button with the given text. /// Returns true if the button was activated. pub fn button(&mut self, classname: &'static str, text: &str, style: ButtonStyle) -> bool { self.button_label(classname, text, style); self.attr_focusable(); if self.is_focused() { self.attr_reverse(); } self.button_activated() } /// Creates a checkbox with the given text. /// Returns true if the checkbox was activated. pub fn checkbox(&mut self, classname: &'static str, text: &str, checked: &mut bool) -> bool { self.styled_label_begin(classname); self.attr_focusable(); if self.is_focused() { self.attr_reverse(); } self.styled_label_add_text(if *checked { "[🗹 " } else { "[☐ " }); self.styled_label_add_text(text); self.styled_label_add_text("]"); self.styled_label_end(); let activated = self.button_activated(); if activated { *checked = !*checked; } activated } fn button_activated(&mut self) -> bool { if !self.input_consumed && ((self.input_mouse_click != 0 && self.contains_mouse_down()) || self.input_keyboard == Some(vk::RETURN) || self.input_keyboard == Some(vk::SPACE)) && self.is_focused() { self.set_input_consumed(); true } else { false } } /// Creates a text input field. /// Returns true if the text contents changed. pub fn editline<'s, 'b: 's>( &'s mut self, classname: &'static str, text: &'b mut dyn WriteableDocument, ) -> bool { self.textarea_internal(classname, TextBufferPayload::Editline(text)) } /// Creates a text area. pub fn textarea(&mut self, classname: &'static str, tb: RcTextBuffer) { self.textarea_internal(classname, TextBufferPayload::Textarea(tb)); } fn textarea_internal(&mut self, classname: &'static str, payload: TextBufferPayload) -> bool { self.block_begin(classname); self.block_end(); let mut node = self.tree.last_node.borrow_mut(); let node = &mut *node; let single_line = match &payload { TextBufferPayload::Editline(_) => true, TextBufferPayload::Textarea(_) => false, }; let buffer = { let buffers = &mut self.tui.cached_text_buffers; let cached = match buffers.iter_mut().find(|t| t.node_id == node.id) { Some(cached) => { if let TextBufferPayload::Textarea(tb) = &payload { cached.editor = tb.clone(); }; cached.seen = true; cached } None => { // If the node is not in the cache, we need to create a new one. buffers.push(CachedTextBuffer { node_id: node.id, editor: match &payload { TextBufferPayload::Editline(_) => TextBuffer::new_rc(true).unwrap(), TextBufferPayload::Textarea(tb) => tb.clone(), }, seen: true, }); buffers.last_mut().unwrap() } }; // SAFETY: *Assuming* that there are no duplicate node IDs in the tree that // would cause this cache slot to be overwritten, then this operation is safe. // The text buffer cache will keep the buffer alive for us long enough. unsafe { mem::transmute(&*cached.editor) } }; node.content = NodeContent::Textarea(TextareaContent { buffer, scroll_offset: Default::default(), scroll_offset_y_drag_start: CoordType::MIN, scroll_offset_x_max: 0, thumb_height: 0, preferred_column: 0, single_line, has_focus: self.tui.is_node_focused(node.id), }); let content = match node.content { NodeContent::Textarea(ref mut content) => content, _ => unreachable!(), }; if let TextBufferPayload::Editline(text) = &payload { content.buffer.borrow_mut().copy_from_str(*text); } if let Some(node_prev) = self.tui.prev_node_map.get(node.id) { let node_prev = node_prev.borrow(); if let NodeContent::Textarea(content_prev) = &node_prev.content { content.scroll_offset = content_prev.scroll_offset; content.scroll_offset_y_drag_start = content_prev.scroll_offset_y_drag_start; content.scroll_offset_x_max = content_prev.scroll_offset_x_max; content.thumb_height = content_prev.thumb_height; content.preferred_column = content_prev.preferred_column; let mut text_width = node_prev.inner.width(); if !single_line { // Subtract -1 to account for the scrollbar. text_width -= 1; } let mut make_cursor_visible; { let mut tb = content.buffer.borrow_mut(); make_cursor_visible = tb.take_cursor_visibility_request(); make_cursor_visible |= tb.set_width(text_width); } make_cursor_visible |= self.textarea_handle_input(content, &node_prev, single_line); if make_cursor_visible { self.textarea_make_cursor_visible(content, &node_prev); } } else { debug_assert!(false); } } let dirty; { let mut tb = content.buffer.borrow_mut(); dirty = tb.is_dirty(); if dirty && let TextBufferPayload::Editline(text) = payload { tb.save_as_string(text); } } self.textarea_adjust_scroll_offset(content); if single_line { node.attributes.fg = self.indexed(IndexedColor::Foreground); node.attributes.bg = self.indexed(IndexedColor::Background); if !content.has_focus { node.attributes.fg = self.contrasted(node.attributes.bg); node.attributes.bg = self.indexed_alpha(IndexedColor::Background, 1, 2); } } node.attributes.focusable = true; node.intrinsic_size.height = content.buffer.borrow().visual_line_count(); node.intrinsic_size_set = true; dirty } fn textarea_handle_input( &mut self, tc: &mut TextareaContent, node_prev: &Node, single_line: bool, ) -> bool { if self.input_consumed { return false; } let mut tb = tc.buffer.borrow_mut(); let tb = &mut *tb; let mut make_cursor_visible = false; let mut change_preferred_column = false; if self.tui.mouse_state != InputMouseState::None && self.tui.was_mouse_down_on_node(node_prev.id) { // Scrolling works even if the node isn't focused. if self.tui.mouse_state == InputMouseState::Scroll { tc.scroll_offset.x += self.input_scroll_delta.x; tc.scroll_offset.y += self.input_scroll_delta.y; self.set_input_consumed(); } else if self.tui.is_node_focused(node_prev.id) { let mouse = self.tui.mouse_position; let inner = node_prev.inner; let text_rect = Rect { left: inner.left + tb.margin_width(), top: inner.top, right: inner.right - !single_line as CoordType, bottom: inner.bottom, }; let track_rect = Rect { left: text_rect.right, top: inner.top, right: inner.right, bottom: inner.bottom, }; let pos = Point { x: mouse.x - inner.left - tb.margin_width() + tc.scroll_offset.x, y: mouse.y - inner.top + tc.scroll_offset.y, }; if text_rect.contains(self.tui.mouse_down_position) { if self.tui.mouse_is_drag { tb.selection_update_visual(pos); tc.preferred_column = tb.cursor_visual_pos().x; let height = inner.height(); // If the editor is only 1 line tall we can't possibly scroll up or down. if height >= 2 { fn calc(min: CoordType, max: CoordType, mouse: CoordType) -> CoordType { // Otherwise, the scroll zone is up to 3 lines at the top/bottom. let zone_height = ((max - min) / 2).min(3); // The .y positions where the scroll zones begin: // Mouse coordinates above top and below bottom respectively. let scroll_min = min + zone_height; let scroll_max = max - zone_height - 1; // Calculate the delta for scrolling up or down. let delta_min = (mouse - scroll_min).clamp(-zone_height, 0); let delta_max = (mouse - scroll_max).clamp(0, zone_height); // If I didn't mess up my logic here, only one of the two values can possibly be !=0. let idx = 3 + delta_min + delta_max; const SPEEDS: [CoordType; 7] = [-9, -3, -1, 0, 1, 3, 9]; let idx = idx.clamp(0, SPEEDS.len() as CoordType) as usize; SPEEDS[idx] } let delta_x = calc(text_rect.left, text_rect.right, mouse.x); let delta_y = calc(text_rect.top, text_rect.bottom, mouse.y); tc.scroll_offset.x += delta_x; tc.scroll_offset.y += delta_y; if delta_x != 0 || delta_y != 0 { self.tui.read_timeout = time::Duration::from_millis(25); } } } else { match self.input_mouse_click { 5.. => {} 4 => tb.select_all(), 3 => tb.select_line(), 2 => tb.select_word(), _ => match self.tui.mouse_state { InputMouseState::Left => { if self.input_mouse_modifiers.contains(kbmod::SHIFT) { // TODO: Untested because Windows Terminal surprisingly doesn't support Shift+Click. tb.selection_update_visual(pos); } else { tb.cursor_move_to_visual(pos); } tc.preferred_column = tb.cursor_visual_pos().x; make_cursor_visible = true; } _ => return false, }, } } } else if track_rect.contains(self.tui.mouse_down_position) { if self.tui.mouse_state == InputMouseState::Release { tc.scroll_offset_y_drag_start = CoordType::MIN; } else if self.tui.mouse_is_drag { if tc.scroll_offset_y_drag_start == CoordType::MIN { tc.scroll_offset_y_drag_start = tc.scroll_offset.y; } // The textarea supports 1 height worth of "scrolling beyond the end". // `track_height` is the same as the viewport height. let scrollable_height = tb.visual_line_count() - 1; if scrollable_height > 0 { let trackable = track_rect.height() - tc.thumb_height; let delta_y = mouse.y - self.tui.mouse_down_position.y; tc.scroll_offset.y = tc.scroll_offset_y_drag_start + (delta_y as i64 * scrollable_height as i64 / trackable as i64) as CoordType; } } } self.set_input_consumed(); } return make_cursor_visible; } if !tc.has_focus { return false; } let mut write: &[u8] = b""; let mut write_raw = false; if let Some(input) = &self.input_text { write = input.text.as_bytes(); write_raw = input.bracketed; tc.preferred_column = tb.cursor_visual_pos().x; make_cursor_visible = true; } else if let Some(input) = &self.input_keyboard { let key = input.key(); let modifiers = input.modifiers(); make_cursor_visible = true; match key { vk::BACK => { let granularity = if modifiers == kbmod::CTRL { CursorMovement::Word } else { CursorMovement::Grapheme }; tb.delete(granularity, -1); } vk::TAB => { if single_line { // If this is just a simple input field, don't consume Tab (= early return). return false; } if modifiers == kbmod::SHIFT { tb.unindent(); } else { write = b"\t"; } } vk::RETURN => { if single_line { // If this is just a simple input field, don't consume Enter (= early return). return false; } write = b"\n"; } vk::ESCAPE => { // If there was a selection, clear it and show the cursor (= fallthrough). if !tb.clear_selection() { if single_line { // If this is just a simple input field, don't consume the escape key // (early return) and don't show the cursor (= return false). return false; } // If this is a textarea, don't show the cursor if // the escape key was pressed and nothing happened. make_cursor_visible = false; } } vk::PRIOR => { let height = node_prev.inner.height() - 1; // If the cursor was already on the first line, // move it to the start of the buffer. if tb.cursor_visual_pos().y == 0 { tc.preferred_column = 0; } if modifiers == kbmod::SHIFT { tb.selection_update_visual(Point { x: tc.preferred_column, y: tb.cursor_visual_pos().y - height, }); } else { tb.cursor_move_to_visual(Point { x: tc.preferred_column, y: tb.cursor_visual_pos().y - height, }); } } vk::NEXT => { let height = node_prev.inner.height() - 1; // If the cursor was already on the last line, // move it to the end of the buffer. if tb.cursor_visual_pos().y >= tb.visual_line_count() - 1 { tc.preferred_column = CoordType::MAX; } if modifiers == kbmod::SHIFT { tb.selection_update_visual(Point { x: tc.preferred_column, y: tb.cursor_visual_pos().y + height, }); } else { tb.cursor_move_to_visual(Point { x: tc.preferred_column, y: tb.cursor_visual_pos().y + height, }); } if tc.preferred_column == CoordType::MAX { tc.preferred_column = tb.cursor_visual_pos().x; } } vk::END => { let logical_before = tb.cursor_logical_pos(); let destination = if modifiers.contains(kbmod::CTRL) { Point::MAX } else { Point { x: CoordType::MAX, y: tb.cursor_visual_pos().y } }; if modifiers.contains(kbmod::SHIFT) { tb.selection_update_visual(destination); } else { tb.cursor_move_to_visual(destination); } if !modifiers.contains(kbmod::CTRL) { let logical_after = tb.cursor_logical_pos(); // If word-wrap is enabled and the user presses End the first time, // it moves to the start of the visual line. The second time they // press it, it moves to the start of the logical line. if tb.is_word_wrap_enabled() && logical_after == logical_before { if modifiers == kbmod::SHIFT { tb.selection_update_logical(Point { x: CoordType::MAX, y: tb.cursor_logical_pos().y, }); } else { tb.cursor_move_to_logical(Point { x: CoordType::MAX, y: tb.cursor_logical_pos().y, }); } } } } vk::HOME => { let logical_before = tb.cursor_logical_pos(); let destination = if modifiers.contains(kbmod::CTRL) { Default::default() } else { Point { x: 0, y: tb.cursor_visual_pos().y } }; if modifiers.contains(kbmod::SHIFT) { tb.selection_update_visual(destination); } else { tb.cursor_move_to_visual(destination); } if !modifiers.contains(kbmod::CTRL) { let mut logical_after = tb.cursor_logical_pos(); // If word-wrap is enabled and the user presses Home the first time, // it moves to the start of the visual line. The second time they // press it, it moves to the start of the logical line. if tb.is_word_wrap_enabled() && logical_after == logical_before { if modifiers == kbmod::SHIFT { tb.selection_update_logical(Point { x: 0, y: tb.cursor_logical_pos().y, }); } else { tb.cursor_move_to_logical(Point { x: 0, y: tb.cursor_logical_pos().y, }); } logical_after = tb.cursor_logical_pos(); } // If the line has some indentation and the user pressed Home, // the first time it'll stop at the indentation. The second time // they press it, it'll move to the true start of the line. // // If the cursor is already at the start of the line, // we move it back to the end of the indentation. if logical_after.x == 0 && let indent_end = tb.indent_end_logical_pos() && (logical_before > indent_end || logical_before.x == 0) { if modifiers == kbmod::SHIFT { tb.selection_update_logical(indent_end); } else { tb.cursor_move_to_logical(indent_end); } } } } vk::LEFT => { let granularity = if modifiers.contains(KBMOD_FOR_WORD_NAV) { CursorMovement::Word } else { CursorMovement::Grapheme }; if modifiers.contains(kbmod::SHIFT) { tb.selection_update_delta(granularity, -1); } else if let Some((beg, _)) = tb.selection_range() { unsafe { tb.set_cursor(beg) }; } else { tb.cursor_move_delta(granularity, -1); } } vk::UP => { match modifiers { kbmod::NONE => { let mut x = tc.preferred_column; let mut y = tb.cursor_visual_pos().y - 1; // If there's a selection we put the cursor above it. if let Some((beg, _)) = tb.selection_range() { x = beg.visual_pos.x; y = beg.visual_pos.y - 1; tc.preferred_column = x; } // If the cursor was already on the first line, // move it to the start of the buffer. if y < 0 { x = 0; tc.preferred_column = 0; } tb.cursor_move_to_visual(Point { x, y }); } kbmod::CTRL => { tc.scroll_offset.y -= 1; make_cursor_visible = false; } kbmod::SHIFT => { // If the cursor was already on the first line, // move it to the start of the buffer. if tb.cursor_visual_pos().y == 0 { tc.preferred_column = 0; } tb.selection_update_visual(Point { x: tc.preferred_column, y: tb.cursor_visual_pos().y - 1, }); } kbmod::CTRL_ALT => { // TODO: Add cursor above } _ => return false, } } vk::RIGHT => { let granularity = if modifiers.contains(KBMOD_FOR_WORD_NAV) { CursorMovement::Word } else { CursorMovement::Grapheme }; if modifiers.contains(kbmod::SHIFT) { tb.selection_update_delta(granularity, 1); } else if let Some((_, end)) = tb.selection_range() { unsafe { tb.set_cursor(end) }; } else { tb.cursor_move_delta(granularity, 1); } } vk::DOWN => match modifiers { kbmod::NONE => { let mut x = tc.preferred_column; let mut y = tb.cursor_visual_pos().y + 1; // If there's a selection we put the cursor below it. if let Some((_, end)) = tb.selection_range() { x = end.visual_pos.x; y = end.visual_pos.y + 1; tc.preferred_column = x; } // If the cursor was already on the last line, // move it to the end of the buffer. if y >= tb.visual_line_count() { x = CoordType::MAX; } tb.cursor_move_to_visual(Point { x, y }); // If we fell into the `if y >= tb.get_visual_line_count()` above, we wanted to // update the `preferred_column` but didn't know yet what it was. Now we know! if x == CoordType::MAX { tc.preferred_column = tb.cursor_visual_pos().x; } } kbmod::CTRL => { tc.scroll_offset.y += 1; make_cursor_visible = false; } kbmod::SHIFT => { // If the cursor was already on the last line, // move it to the end of the buffer. if tb.cursor_visual_pos().y >= tb.visual_line_count() - 1 { tc.preferred_column = CoordType::MAX; } tb.selection_update_visual(Point { x: tc.preferred_column, y: tb.cursor_visual_pos().y + 1, }); if tc.preferred_column == CoordType::MAX { tc.preferred_column = tb.cursor_visual_pos().x; } } kbmod::CTRL_ALT => { // TODO: Add cursor above } _ => return false, }, vk::INSERT => match modifiers { kbmod::SHIFT => { write = &self.tui.clipboard; write_raw = true; } kbmod::CTRL => self.set_clipboard(tb.extract_selection(false)), _ => tb.set_overtype(!tb.is_overtype()), }, vk::DELETE => match modifiers { kbmod::SHIFT => self.set_clipboard(tb.extract_selection(true)), kbmod::CTRL => tb.delete(CursorMovement::Word, 1), _ => tb.delete(CursorMovement::Grapheme, 1), }, vk::A => match modifiers { kbmod::CTRL => tb.select_all(), _ => return false, }, vk::B => match modifiers { kbmod::ALT if cfg!(target_os = "macos") => { // On macOS, terminals commonly emit the Emacs style // Alt+B (ESC b) sequence for Alt+Left. tb.cursor_move_delta(CursorMovement::Word, -1); } _ => return false, }, vk::F => match modifiers { kbmod::ALT if cfg!(target_os = "macos") => { // On macOS, terminals commonly emit the Emacs style // Alt+F (ESC f) sequence for Alt+Right. tb.cursor_move_delta(CursorMovement::Word, 1); } _ => return false, }, vk::H => match modifiers { kbmod::CTRL => tb.delete(CursorMovement::Word, -1), _ => return false, }, vk::X => match modifiers { kbmod::CTRL => self.set_clipboard(tb.extract_selection(true)), _ => return false, }, vk::C => match modifiers { kbmod::CTRL => self.set_clipboard(tb.extract_selection(false)), _ => return false, }, vk::V => match modifiers { kbmod::CTRL => { write = &self.tui.clipboard; write_raw = true; } _ => return false, }, vk::Y => match modifiers { kbmod::CTRL => tb.redo(), _ => return false, }, vk::Z => match modifiers { kbmod::CTRL => tb.undo(), kbmod::CTRL_SHIFT => tb.redo(), kbmod::ALT => tb.set_word_wrap(!tb.is_word_wrap_enabled()), _ => return false, }, _ => return false, } change_preferred_column = !matches!(key, vk::PRIOR | vk::NEXT | vk::UP | vk::DOWN); } else { return false; } if single_line && !write.is_empty() { let (end, _) = simd::lines_fwd(write, 0, 0, 1); write = unicode::strip_newline(&write[..end]); } if !write.is_empty() { tb.write(write, write_raw); change_preferred_column = true; } if change_preferred_column { tc.preferred_column = tb.cursor_visual_pos().x; } self.set_input_consumed(); make_cursor_visible } fn textarea_make_cursor_visible(&self, tc: &mut TextareaContent, node_prev: &Node) { let tb = tc.buffer.borrow(); let mut scroll_x = tc.scroll_offset.x; let mut scroll_y = tc.scroll_offset.y; let text_width = tb.text_width(); let cursor_x = tb.cursor_visual_pos().x; scroll_x = scroll_x.min(cursor_x - 10); scroll_x = scroll_x.max(cursor_x - text_width + 10); let viewport_height = node_prev.inner.height(); let cursor_y = tb.cursor_visual_pos().y; // Scroll up if the cursor is above the visible area. scroll_y = scroll_y.min(cursor_y); // Scroll down if the cursor is below the visible area. scroll_y = scroll_y.max(cursor_y - viewport_height + 1); tc.scroll_offset.x = scroll_x; tc.scroll_offset.y = scroll_y; } fn textarea_adjust_scroll_offset(&self, tc: &mut TextareaContent) { let tb = tc.buffer.borrow(); let mut scroll_x = tc.scroll_offset.x; let mut scroll_y = tc.scroll_offset.y; scroll_x = scroll_x.min(tc.scroll_offset_x_max.max(tb.cursor_visual_pos().x) - 10); scroll_x = scroll_x.max(0); scroll_y = scroll_y.clamp(0, tb.visual_line_count() - 1); if tb.is_word_wrap_enabled() { scroll_x = 0; } tc.scroll_offset.x = scroll_x; tc.scroll_offset.y = scroll_y; } /// Creates a scrollable area. pub fn scrollarea_begin(&mut self, classname: &'static str, intrinsic_size: Size) { self.block_begin(classname); let container_node = self.tree.last_node; { let mut container = self.tree.last_node.borrow_mut(); container.content = NodeContent::Scrollarea(ScrollareaContent { scroll_offset: Point::MIN, scroll_offset_y_drag_start: CoordType::MIN, thumb_height: 0, }); if intrinsic_size.width > 0 || intrinsic_size.height > 0 { container.intrinsic_size.width = intrinsic_size.width.max(0); container.intrinsic_size.height = intrinsic_size.height.max(0); container.intrinsic_size_set = true; } } self.block_begin("content"); self.inherit_focus(); // Ensure that attribute modifications apply to the outer container. self.tree.last_node = container_node; } /// Scrolls the current scrollable area to the given position. pub fn scrollarea_scroll_to(&mut self, pos: Point) { let mut container = self.tree.last_node.borrow_mut(); if let NodeContent::Scrollarea(sc) = &mut container.content { sc.scroll_offset = pos; } else { debug_assert!(false); } } /// Ends the current scrollarea block. pub fn scrollarea_end(&mut self) { self.block_end(); // content block self.block_end(); // outer container let mut container = self.tree.last_node.borrow_mut(); let container_id = container.id; let container_depth = container.depth; let Some(prev_container) = self.tui.prev_node_map.get(container_id) else { return; }; let prev_container = prev_container.borrow(); let NodeContent::Scrollarea(sc) = &mut container.content else { unreachable!(); }; if sc.scroll_offset == Point::MIN && let NodeContent::Scrollarea(sc_prev) = &prev_container.content { *sc = sc_prev.clone(); } if !self.input_consumed { if self.tui.mouse_state != InputMouseState::None { let container_rect = prev_container.inner; match self.tui.mouse_state { InputMouseState::Left => { if self.tui.mouse_is_drag { // We don't need to look up the previous track node, // since it has a fixed size based on the container size. let track_rect = Rect { left: container_rect.right, top: container_rect.top, right: container_rect.right + 1, bottom: container_rect.bottom, }; if track_rect.contains(self.tui.mouse_down_position) { if sc.scroll_offset_y_drag_start == CoordType::MIN { sc.scroll_offset_y_drag_start = sc.scroll_offset.y; } let content = prev_container.children.first.unwrap().borrow(); let content_rect = content.inner; let content_height = content_rect.height(); let track_height = track_rect.height(); let scrollable_height = content_height - track_height; if scrollable_height > 0 { let trackable = track_height - sc.thumb_height; let delta_y = self.tui.mouse_position.y - self.tui.mouse_down_position.y; sc.scroll_offset.y = sc.scroll_offset_y_drag_start + (delta_y as i64 * scrollable_height as i64 / trackable as i64) as CoordType; } self.set_input_consumed(); } } } InputMouseState::Release => { sc.scroll_offset_y_drag_start = CoordType::MIN; } InputMouseState::Scroll => { if container_rect.contains(self.tui.mouse_position) { sc.scroll_offset.x += self.input_scroll_delta.x; sc.scroll_offset.y += self.input_scroll_delta.y; self.set_input_consumed(); } } _ => {} } } else if self.tui.is_subtree_focused_alt(container_id, container_depth) && let Some(key) = self.input_keyboard { match key { vk::PRIOR => sc.scroll_offset.y -= prev_container.inner_clipped.height(), vk::NEXT => sc.scroll_offset.y += prev_container.inner_clipped.height(), vk::END => sc.scroll_offset.y = CoordType::MAX, vk::HOME => sc.scroll_offset.y = 0, _ => return, } self.set_input_consumed(); } } } /// Creates a list where exactly one item is selected. pub fn list_begin(&mut self, classname: &'static str) { self.block_begin(classname); self.attr_focusable(); let mut last_node = self.tree.last_node.borrow_mut(); let content = self .tui .prev_node_map .get(last_node.id) .and_then(|node| match &node.borrow().content { NodeContent::List(content) => { Some(ListContent { selected: content.selected, selected_node: None }) } _ => None, }) .unwrap_or(ListContent { selected: 0, selected_node: None }); last_node.attributes.focus_void = true; last_node.content = NodeContent::List(content); } /// Creates a list item with the given text. pub fn list_item(&mut self, select: bool, text: &str) -> ListSelection { self.styled_list_item_begin(); self.styled_label_add_text(text); self.styled_list_item_end(select) } /// Creates a list item consisting of a styled label. /// See [`Context::styled_label_begin`]. pub fn styled_list_item_begin(&mut self) { let list = self.tree.current_node; let idx = list.borrow().child_count; self.next_block_id_mixin(idx as u64); self.styled_label_begin("item"); self.styled_label_add_text(" "); self.attr_focusable(); } /// Ends the current styled list item. pub fn styled_list_item_end(&mut self, select: bool) -> ListSelection { self.styled_label_end(); let list = self.tree.current_node; let selected_before; let selected_now; let focused; { let mut list = list.borrow_mut(); let content = match &mut list.content { NodeContent::List(content) => content, _ => unreachable!(), }; let item = self.tree.last_node.borrow(); let item_id = item.id; selected_before = content.selected == item_id; focused = self.is_focused(); // Inherit the default selection & Click changes selection selected_now = selected_before || (select && content.selected == 0) || focused; // Note down the selected node for keyboard navigation. if selected_now { content.selected_node = Some(self.tree.last_node); if !selected_before { content.selected = item_id; self.needs_rerender(); } } } // Clicking an item activates it let clicked = !self.input_consumed && (self.input_mouse_click == 2 && self.was_mouse_down()); // Pressing Enter on a selected item activates it as well let entered = focused && selected_before && !self.input_consumed && matches!(self.input_keyboard, Some(vk::RETURN)); let activated = clicked || entered; if activated { self.set_input_consumed(); } if selected_before && activated { ListSelection::Activated } else if selected_now && !selected_before { ListSelection::Selected } else { ListSelection::Unchanged } } /// Ends the current list block. pub fn list_end(&mut self) { self.block_end(); let contains_focus; let selected_now; let mut selected_next; { let list = self.tree.last_node.borrow(); contains_focus = self.tui.is_subtree_focused(&list); selected_now = match &list.content { NodeContent::List(content) => content.selected_node, _ => unreachable!(), }; selected_next = match selected_now.or(list.children.first) { Some(node) => node, None => return, }; } if contains_focus && !self.input_consumed && let Some(key) = self.input_keyboard && let Some(selected_now) = selected_now { let list = self.tree.last_node.borrow(); if let Some(prev_container) = self.tui.prev_node_map.get(list.id) { let mut consumed = true; match key { vk::PRIOR => { selected_next = selected_now; for _ in 0..prev_container.borrow().inner_clipped.height() - 1 { let node = selected_next.borrow(); selected_next = match node.siblings.prev { Some(node) => node, None => break, }; } } vk::NEXT => { selected_next = selected_now; for _ in 0..prev_container.borrow().inner_clipped.height() - 1 { let node = selected_next.borrow(); selected_next = match node.siblings.next { Some(node) => node, None => break, }; } } vk::END => { selected_next = list.children.last.unwrap_or(selected_next); } vk::HOME => { selected_next = list.children.first.unwrap_or(selected_next); } vk::UP => { selected_next = selected_now .borrow() .siblings .prev .or(list.children.last) .unwrap_or(selected_next); } vk::DOWN => { selected_next = selected_now .borrow() .siblings .next .or(list.children.first) .unwrap_or(selected_next); } _ => consumed = false, } if consumed { self.set_input_consumed(); } } } // Now that we know which item is selected we can mark it as such. if !opt_ptr_eq(selected_now, Some(selected_next)) && let NodeContent::List(content) = &mut self.tree.last_node.borrow_mut().content { content.selected_node = Some(selected_next); } // Now that we know which item is selected we can mark it as such. if let NodeContent::Text(content) = &mut selected_next.borrow_mut().content { unsafe { content.text.as_bytes_mut()[0] = b'>'; } } // If the list has focus, we also delegate focus to the selected item and colorize it. if contains_focus { { let mut node = selected_next.borrow_mut(); node.attributes.bg = self.indexed(IndexedColor::Green); node.attributes.fg = self.contrasted(self.indexed(IndexedColor::Green)); } self.steal_focus_for(selected_next); } } /// Creates a menubar, to be shown at the top of the screen. pub fn menubar_begin(&mut self) { self.table_begin("menubar"); self.attr_focus_well(); self.table_next_row(); } /// Appends a menu to the current menubar. /// /// Returns true if the menu is open. Continue appending items to it in that case. pub fn menubar_menu_begin(&mut self, text: &str, accelerator: char) -> bool { let accelerator = if cfg!(target_os = "macos") { '\0' } else { accelerator }; let mixin = self.tree.current_node.borrow().child_count as u64; self.next_block_id_mixin(mixin); self.button_label( "menu_button", text, ButtonStyle::default().accelerator(accelerator).bracketed(false), ); self.attr_focusable(); self.attr_padding(Rect::two(0, 1)); let contains_focus = self.contains_focus(); let keyboard_focus = accelerator != '\0' && !contains_focus && self.consume_shortcut(kbmod::ALT | InputKey::new(accelerator as u32)); if contains_focus || keyboard_focus { self.attr_background_rgba(self.tui.floater_default_bg); self.attr_foreground_rgba(self.tui.floater_default_fg); if self.is_focused() { self.attr_background_rgba(self.indexed(IndexedColor::Green)); self.attr_foreground_rgba(self.contrasted(self.indexed(IndexedColor::Green))); } self.next_block_id_mixin(mixin); self.table_begin("flyout"); self.attr_float(FloatSpec { anchor: Anchor::Last, gravity_x: 0.0, gravity_y: 0.0, offset_x: 0.0, offset_y: 1.0, }); self.attr_border(); self.attr_focus_well(); if keyboard_focus { self.steal_focus(); } true } else { false } } /// Appends a button to the current menu. pub fn menubar_menu_button( &mut self, text: &str, accelerator: char, shortcut: InputKey, ) -> bool { self.menubar_menu_checkbox(text, accelerator, shortcut, false) } /// Appends a checkbox to the current menu. /// Returns true if the checkbox was activated. pub fn menubar_menu_checkbox( &mut self, text: &str, accelerator: char, shortcut: InputKey, checked: bool, ) -> bool { self.table_next_row(); self.attr_focusable(); // First menu item? Steal focus. if self.tree.current_node.borrow_mut().siblings.prev.is_none() { self.inherit_focus(); } if self.is_focused() { self.attr_background_rgba(self.indexed(IndexedColor::Green)); self.attr_foreground_rgba(self.contrasted(self.indexed(IndexedColor::Green))); } let clicked = self.button_activated() || self.consume_shortcut(InputKey::new(accelerator as u32)); self.button_label( "menu_checkbox", text, ButtonStyle::default().bracketed(false).checked(checked).accelerator(accelerator), ); self.menubar_shortcut(shortcut); if clicked { // TODO: This should reassign the previous focused path. self.needs_rerender(); Tui::clean_node_path(&mut self.tui.focused_node_path); } clicked } /// Ends the current menu. pub fn menubar_menu_end(&mut self) { self.table_end(); if !self.input_consumed && let Some(key) = self.input_keyboard && matches!(key, vk::ESCAPE | vk::UP | vk::DOWN) { if matches!(key, vk::UP | vk::DOWN) { // If the focus is on the menubar, and the user presses up/down, // focus the first/last item of the flyout respectively. let ln = self.tree.last_node.borrow(); if self.tui.is_node_focused(ln.parent.map_or(0, |n| n.borrow().id)) { let selected_next = if key == vk::UP { ln.children.last } else { ln.children.first }; if let Some(selected_next) = selected_next { self.steal_focus_for(selected_next); self.set_input_consumed(); } } } else if self.contains_focus() { // Otherwise, if the menu is the focused one and the // user presses Escape, pass focus back to the menubar. self.tui.pop_focusable_node(1); } } } /// Ends the current menubar. pub fn menubar_end(&mut self) { self.table_end(); } /// Renders a button label with an optional accelerator character /// May also renders a checkbox or square brackets for inline buttons fn button_label(&mut self, classname: &'static str, text: &str, style: ButtonStyle) { // Label prefix self.styled_label_begin(classname); if style.bracketed { self.styled_label_add_text("["); } if let Some(checked) = style.checked { self.styled_label_add_text(if checked { "🗹 " } else { " " }); } // Label text match style.accelerator { Some(accelerator) if accelerator.is_ascii_uppercase() => { // Complex case: // Locate the offset of the accelerator character in the label text let mut off = text.len(); for (i, c) in text.bytes().enumerate() { // Perfect match (uppercase character) --> stop if c as char == accelerator { off = i; break; } // Inexact match (lowercase character) --> use first hit if (c & !0x20) as char == accelerator && off == text.len() { off = i; } } if off < text.len() { // Add an underline to the accelerator. self.styled_label_add_text(&text[..off]); self.styled_label_set_attributes(Attributes::Underlined); self.styled_label_add_text(&text[off..off + 1]); self.styled_label_set_attributes(Attributes::None); self.styled_label_add_text(&text[off + 1..]); } else { // Add the accelerator in parentheses and underline it. let ch = accelerator as u8; self.styled_label_add_text(text); self.styled_label_add_text("("); self.styled_label_set_attributes(Attributes::Underlined); self.styled_label_add_text(unsafe { str_from_raw_parts(&ch, 1) }); self.styled_label_set_attributes(Attributes::None); self.styled_label_add_text(")"); } } _ => { // Simple case: // no accelerator character self.styled_label_add_text(text); } } // Label postfix if style.bracketed { self.styled_label_add_text("]"); } self.styled_label_end(); } fn menubar_shortcut(&mut self, shortcut: InputKey) { let shortcut_letter = shortcut.value() as u8 as char; if shortcut_letter.is_ascii_uppercase() { let mut shortcut_text = ArenaString::new_in(self.arena()); if shortcut.modifiers_contains(kbmod::CTRL) { shortcut_text.push_str(self.tui.modifier_translations.ctrl); shortcut_text.push('+'); } if shortcut.modifiers_contains(kbmod::ALT) { shortcut_text.push_str(self.tui.modifier_translations.alt); shortcut_text.push('+'); } if shortcut.modifiers_contains(kbmod::SHIFT) { shortcut_text.push_str(self.tui.modifier_translations.shift); shortcut_text.push('+'); } shortcut_text.push(shortcut_letter); self.label("shortcut", &shortcut_text); } else { self.block_begin("shortcut"); self.block_end(); } self.attr_padding(Rect { left: 2, top: 0, right: 2, bottom: 0 }); } } /// See [`Tree::visit_all`]. #[derive(Clone, Copy)] enum VisitControl { Continue, SkipChildren, Stop, } /// Stores the root of the "DOM" tree of the UI. struct Tree<'a> { tail: &'a NodeCell<'a>, root_first: &'a NodeCell<'a>, root_last: &'a NodeCell<'a>, last_node: &'a NodeCell<'a>, current_node: &'a NodeCell<'a>, count: usize, checksum: u64, } impl<'a> Tree<'a> { /// Creates a new tree inside the given arena. /// A single root node is added for the main contents. fn new(arena: &'a Arena) -> Self { let root = Self::alloc_node(arena); { let mut r = root.borrow_mut(); r.id = ROOT_ID; r.classname = "root"; r.attributes.focusable = true; r.attributes.focus_well = true; } Self { tail: root, root_first: root, root_last: root, last_node: root, current_node: root, count: 1, checksum: ROOT_ID, } } fn alloc_node(arena: &'a Arena) -> &'a NodeCell<'a> { arena.alloc_uninit().write(Default::default()) } /// Appends a child node to the current node. fn push_child(&mut self, node: &'a NodeCell<'a>) { let mut n = node.borrow_mut(); n.parent = Some(self.current_node); n.stack_parent = Some(self.current_node); { let mut p = self.current_node.borrow_mut(); n.siblings.prev = p.children.last; n.depth = p.depth + 1; if let Some(child_last) = p.children.last { let mut child_last = child_last.borrow_mut(); child_last.siblings.next = Some(node); } if p.children.first.is_none() { p.children.first = Some(node); } p.children.last = Some(node); p.child_count += 1; } n.prev = Some(self.tail); { let mut tail = self.tail.borrow_mut(); tail.next = Some(node); } self.tail = node; self.last_node = node; self.current_node = node; self.count += 1; // wymix is weak, but both checksum and node.id are proper random, so... it's not *that* bad. self.checksum = wymix(self.checksum, n.id); } /// Removes the current node from its parent and appends it as a new root. /// Used for [`Context::attr_float`]. fn move_node_to_root(&mut self, node: &'a NodeCell<'a>, anchor: Option<&'a NodeCell<'a>>) { let mut n = node.borrow_mut(); let Some(parent) = n.parent else { return; }; if let Some(sibling_prev) = n.siblings.prev { let mut sibling_prev = sibling_prev.borrow_mut(); sibling_prev.siblings.next = n.siblings.next; } if let Some(sibling_next) = n.siblings.next { let mut sibling_next = sibling_next.borrow_mut(); sibling_next.siblings.prev = n.siblings.prev; } { let mut p = parent.borrow_mut(); if opt_ptr_eq(p.children.first, Some(node)) { p.children.first = n.siblings.next; } if opt_ptr_eq(p.children.last, Some(node)) { p.children.last = n.siblings.prev; } p.child_count -= 1; } n.parent = anchor; n.depth = anchor.map_or(0, |n| n.borrow().depth + 1); n.siblings.prev = Some(self.root_last); n.siblings.next = None; self.root_last.borrow_mut().siblings.next = Some(node); self.root_last = node; } /// Completes the current node and moves focus to the parent. fn pop_stack(&mut self) { let current_node = self.current_node.borrow(); if let Some(stack_parent) = current_node.stack_parent { self.last_node = self.current_node; self.current_node = stack_parent; } } fn iterate_siblings( mut node: Option<&'a NodeCell<'a>>, ) -> impl Iterator<Item = &'a NodeCell<'a>> + use<'a> { iter::from_fn(move || { let n = node?; node = n.borrow().siblings.next; Some(n) }) } fn iterate_roots(&self) -> impl Iterator<Item = &'a NodeCell<'a>> + use<'a> { Self::iterate_siblings(Some(self.root_first)) } /// Visits all nodes under and including `root` in depth order. /// Starts with node `start`. /// /// WARNING: Breaks in hilarious ways if `start` is not within `root`. fn visit_all<T: FnMut(&'a NodeCell<'a>) -> VisitControl>( root: &'a NodeCell<'a>, start: &'a NodeCell<'a>, forward: bool, mut cb: T, ) { let root_depth = root.borrow().depth; let mut node = start; let children_idx = if forward { NodeChildren::FIRST } else { NodeChildren::LAST }; let siblings_idx = if forward { NodeSiblings::NEXT } else { NodeSiblings::PREV }; while { 'traverse: { match cb(node) { VisitControl::Continue => { // Depth first search: It has a child? Go there. if let Some(child) = node.borrow().children.get(children_idx) { node = child; break 'traverse; } } VisitControl::SkipChildren => {} VisitControl::Stop => return, } loop { // If we hit the root while going up, we restart the traversal at // `root` going down again until we hit `start` again. let n = node.borrow(); if n.depth <= root_depth { break 'traverse; } // Go to the parent's next sibling. --> Next subtree. if let Some(sibling) = n.siblings.get(siblings_idx) { node = sibling; break; } // Out of children? Go back to the parent. node = n.parent.unwrap(); } } // We're done once we wrapped around to the `start`. !ptr::eq(node, start) } {} } } /// A hashmap of node IDs to nodes. /// /// This map uses a simple open addressing scheme with linear probing. /// It's fast, simple, and sufficient for the small number of nodes we have. struct NodeMap<'a> { slots: &'a [Option<&'a NodeCell<'a>>], shift: usize, mask: u64, } impl Default for NodeMap<'static> { fn default() -> Self { Self { slots: &[None, None], shift: 63, mask: 0 } } } impl<'a> NodeMap<'a> { /// Creates a new node map for the given tree. fn new(arena: &'a Arena, tree: &Tree<'a>) -> Self { // Since we aren't expected to have millions of nodes, // we allocate 4x the number of slots for a 25% fill factor. let width = (4 * tree.count + 1).ilog2().max(1) as usize; let slots = 1 << width; let shift = 64 - width; let mask = (slots - 1) as u64; let slots = arena.alloc_uninit_slice(slots).write_filled(None); let mut node = tree.root_first; loop { let n = node.borrow(); let mut slot = n.id >> shift; loop { if slots[slot as usize].is_none() { slots[slot as usize] = Some(node); break; } slot = (slot + 1) & mask; } node = match n.next { Some(node) => node, None => break, }; } Self { slots, shift, mask } } /// Gets a node by its ID. fn get(&mut self, id: u64) -> Option<&'a NodeCell<'a>> { let shift = self.shift; let mask = self.mask; let mut slot = id >> shift; loop { let node = self.slots[slot as usize]?; if node.borrow().id == id { return Some(node); } slot = (slot + 1) & mask; } } } struct FloatAttributes { // Specifies the origin of the container relative to the container size. [0, 1] gravity_x: f32, gravity_y: f32, // Specifies an offset from the origin in cells. offset_x: f32, offset_y: f32, } /// NOTE: Must not contain items that require drop(). #[derive(Default)] struct NodeAttributes { float: Option<FloatAttributes>, position: Position, padding: Rect, bg: u32, fg: u32, reverse: bool, bordered: bool, focusable: bool, focus_well: bool, // Prevents focus from leaving via Tab focus_void: bool, // Prevents focus from entering via Tab } /// NOTE: Must not contain items that require drop(). struct ListContent<'a> { selected: u64, // Points to the Node that holds this ListContent instance, if any>. selected_node: Option<&'a NodeCell<'a>>, } /// NOTE: Must not contain items that require drop(). struct TableContent<'a> { columns: Vec<CoordType, &'a Arena>, cell_gap: Size, } /// NOTE: Must not contain items that require drop(). struct StyledTextChunk { offset: usize, fg: u32, attr: Attributes, } const INVALID_STYLED_TEXT_CHUNK: StyledTextChunk = StyledTextChunk { offset: usize::MAX, fg: 0, attr: Attributes::None }; /// NOTE: Must not contain items that require drop(). struct TextContent<'a> { text: ArenaString<'a>, chunks: Vec<StyledTextChunk, &'a Arena>, overflow: Overflow, } /// NOTE: Must not contain items that require drop(). struct TextareaContent<'a> { buffer: &'a TextBufferCell, // Carries over between frames. scroll_offset: Point, scroll_offset_y_drag_start: CoordType, scroll_offset_x_max: CoordType, thumb_height: CoordType, preferred_column: CoordType, single_line: bool, has_focus: bool, } /// NOTE: Must not contain items that require drop(). #[derive(Clone)] struct ScrollareaContent { scroll_offset: Point, scroll_offset_y_drag_start: CoordType, thumb_height: CoordType, } /// NOTE: Must not contain items that require drop(). #[derive(Default)] enum NodeContent<'a> { #[default] None, List(ListContent<'a>), Modal(ArenaString<'a>), // title Table(TableContent<'a>), Text(TextContent<'a>), Textarea(TextareaContent<'a>), Scrollarea(ScrollareaContent), } /// NOTE: Must not contain items that require drop(). #[derive(Default)] struct NodeSiblings<'a> { prev: Option<&'a NodeCell<'a>>, next: Option<&'a NodeCell<'a>>, } impl<'a> NodeSiblings<'a> { const PREV: usize = 0; const NEXT: usize = 1; fn get(&self, off: usize) -> Option<&'a NodeCell<'a>> { match off & 1 { 0 => self.prev, 1 => self.next, _ => unreachable!(), } } } /// NOTE: Must not contain items that require drop(). #[derive(Default)] struct NodeChildren<'a> { first: Option<&'a NodeCell<'a>>, last: Option<&'a NodeCell<'a>>, } impl<'a> NodeChildren<'a> { const FIRST: usize = 0; const LAST: usize = 1; fn get(&self, off: usize) -> Option<&'a NodeCell<'a>> { match off & 1 { 0 => self.first, 1 => self.last, _ => unreachable!(), } } } type NodeCell<'a> = SemiRefCell<Node<'a>>; /// A node in the UI tree. /// /// NOTE: Must not contain items that require drop(). #[derive(Default)] struct Node<'a> { prev: Option<&'a NodeCell<'a>>, next: Option<&'a NodeCell<'a>>, stack_parent: Option<&'a NodeCell<'a>>, id: u64, classname: &'static str, parent: Option<&'a NodeCell<'a>>, depth: usize, siblings: NodeSiblings<'a>, children: NodeChildren<'a>, child_count: usize, attributes: NodeAttributes, content: NodeContent<'a>, intrinsic_size: Size, intrinsic_size_set: bool, outer: Rect, // in screen-space, calculated during layout inner: Rect, // in screen-space, calculated during layout outer_clipped: Rect, // in screen-space, calculated during layout, restricted to the viewport inner_clipped: Rect, // in screen-space, calculated during layout, restricted to the viewport } impl Node<'_> { /// Given an outer rectangle (including padding and borders) of this node, /// this returns the inner rectangle (excluding padding and borders). fn outer_to_inner(&self, mut outer: Rect) -> Rect { let l = self.attributes.bordered; let t = self.attributes.bordered; let r = self.attributes.bordered || matches!(self.content, NodeContent::Scrollarea(..)); let b = self.attributes.bordered; outer.left += self.attributes.padding.left + l as CoordType; outer.top += self.attributes.padding.top + t as CoordType; outer.right -= self.attributes.padding.right + r as CoordType; outer.bottom -= self.attributes.padding.bottom + b as CoordType; outer } /// Given an intrinsic size (excluding padding and borders) of this node, /// this returns the outer size (including padding and borders). fn intrinsic_to_outer(&self) -> Size { let l = self.attributes.bordered; let t = self.attributes.bordered; let r = self.attributes.bordered || matches!(self.content, NodeContent::Scrollarea(..)); let b = self.attributes.bordered; let mut size = self.intrinsic_size; size.width += self.attributes.padding.left + self.attributes.padding.right + l as CoordType + r as CoordType; size.height += self.attributes.padding.top + self.attributes.padding.bottom + t as CoordType + b as CoordType; size } /// Computes the intrinsic size of this node and its children. fn compute_intrinsic_size(&mut self) { match &mut self.content { NodeContent::Table(spec) => { // Calculate each row's height and the maximum width of each of its columns. for row in Tree::iterate_siblings(self.children.first) { let mut row = row.borrow_mut(); let mut row_height = 0; for (column, cell) in Tree::iterate_siblings(row.children.first).enumerate() { let mut cell = cell.borrow_mut(); cell.compute_intrinsic_size(); let size = cell.intrinsic_to_outer(); // If the spec.columns[] value is positive, it's an absolute width. // Otherwise, it's a fraction of the remaining space. // // TODO: The latter is computed incorrectly. // Example: If the items are "a","b","c" then the intrinsic widths are [1,1,1]. // If the column spec is [0,-3,-1], then this code assigns an intrinsic row // width of 3, but it should be 5 (1+1+3), because the spec says that the // last column (flexible 1/1) must be 3 times as wide as the 2nd one (1/3rd). // It's not a big deal yet, because such functionality isn't needed just yet. if column >= spec.columns.len() { spec.columns.push(0); } spec.columns[column] = spec.columns[column].max(size.width); row_height = row_height.max(size.height); } row.intrinsic_size.height = row_height; } // Assuming each column has the width of the widest cell in that column, // calculate the total width of the table. let total_gap_width = spec.cell_gap.width * spec.columns.len().saturating_sub(1) as CoordType; let total_inner_width = spec.columns.iter().sum::<CoordType>() + total_gap_width; let mut total_width = 0; let mut total_height = 0; // Assign the total width to each row. for row in Tree::iterate_siblings(self.children.first) { let mut row = row.borrow_mut(); row.intrinsic_size.width = total_inner_width; row.intrinsic_size_set = true; let size = row.intrinsic_to_outer(); total_width = total_width.max(size.width); total_height += size.height; } let total_gap_height = spec.cell_gap.height * self.child_count.saturating_sub(1) as CoordType; total_height += total_gap_height; // Assign the total width/height to the table. if !self.intrinsic_size_set { self.intrinsic_size.width = total_width; self.intrinsic_size.height = total_height; self.intrinsic_size_set = true; } } _ => { let mut max_width = 0; let mut total_height = 0; for child in Tree::iterate_siblings(self.children.first) { let mut child = child.borrow_mut(); child.compute_intrinsic_size(); let size = child.intrinsic_to_outer(); max_width = max_width.max(size.width); total_height += size.height; } if !self.intrinsic_size_set { self.intrinsic_size.width = max_width; self.intrinsic_size.height = total_height; self.intrinsic_size_set = true; } } } } /// Lays out the children of this node. /// The clip rect restricts "rendering" to a certain area (the viewport). fn layout_children(&mut self, clip: Rect) { if self.children.first.is_none() || self.inner.is_empty() { return; } match &mut self.content { NodeContent::Table(spec) => { let width = self.inner.right - self.inner.left; let mut x = self.inner.left; let mut y = self.inner.top; for row in Tree::iterate_siblings(self.children.first) { let mut row = row.borrow_mut(); let mut size = row.intrinsic_to_outer(); size.width = width; row.outer.left = x; row.outer.top = y; row.outer.right = x + size.width; row.outer.bottom = y + size.height; row.outer = row.outer.intersect(self.inner); row.inner = row.outer_to_inner(row.outer); row.outer_clipped = row.outer.intersect(clip); row.inner_clipped = row.inner.intersect(clip); let mut row_height = 0; for (column, cell) in Tree::iterate_siblings(row.children.first).enumerate() { let mut cell = cell.borrow_mut(); let mut size = cell.intrinsic_to_outer(); size.width = spec.columns[column]; cell.outer.left = x; cell.outer.top = y; cell.outer.right = x + size.width; cell.outer.bottom = y + size.height; cell.outer = cell.outer.intersect(self.inner); cell.inner = cell.outer_to_inner(cell.outer); cell.outer_clipped = cell.outer.intersect(clip); cell.inner_clipped = cell.inner.intersect(clip); x += size.width + spec.cell_gap.width; row_height = row_height.max(size.height); cell.layout_children(clip); } x = self.inner.left; y += row_height + spec.cell_gap.height; } } NodeContent::Scrollarea(sc) => { let mut content = self.children.first.unwrap().borrow_mut(); // content available viewport size (-1 for the track) let sx = self.inner.right - self.inner.left; let sy = self.inner.bottom - self.inner.top; // actual content size let cx = sx; let cy = content.intrinsic_size.height.max(sy); // scroll offset let ox = 0; let oy = sc.scroll_offset.y.clamp(0, cy - sy); sc.scroll_offset.x = ox; sc.scroll_offset.y = oy; content.outer.left = self.inner.left - ox; content.outer.top = self.inner.top - oy; content.outer.right = content.outer.left + cx; content.outer.bottom = content.outer.top + cy; content.inner = content.outer_to_inner(content.outer); content.outer_clipped = content.outer.intersect(self.inner_clipped); content.inner_clipped = content.inner.intersect(self.inner_clipped); let clip = content.inner_clipped; content.layout_children(clip); } _ => { let width = self.inner.right - self.inner.left; let x = self.inner.left; let mut y = self.inner.top; for child in Tree::iterate_siblings(self.children.first) { let mut child = child.borrow_mut(); let size = child.intrinsic_to_outer(); let remaining = (width - size.width).max(0); child.outer.left = x + match child.attributes.position { Position::Stretch | Position::Left => 0, Position::Center => remaining / 2, Position::Right => remaining, }; child.outer.right = child.outer.left + match child.attributes.position { Position::Stretch => width, _ => size.width, }; child.outer.top = y; child.outer.bottom = y + size.height; child.outer = child.outer.intersect(self.inner); child.inner = child.outer_to_inner(child.outer); child.outer_clipped = child.outer.intersect(clip); child.inner_clipped = child.inner.intersect(clip); y += size.height; } for child in Tree::iterate_siblings(self.children.first) { let mut child = child.borrow_mut(); child.layout_children(clip); } } } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mixelpixx/microsoft-edit-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server