├── .gitignore ├── source ├── demo.d └── qui │ ├── termwrap.d │ ├── widgets.d │ └── qui.d ├── dub.sdl ├── docs ├── qterminal.md ├── qcontainer.md ├── viewport.md ├── qlayout.md ├── qwidget.md ├── events.md └── qparent.md ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | .vscode/ 5 | libqui.a 6 | quidemo 7 | qui.so 8 | qui.dylib 9 | qui.dll 10 | qui.a 11 | qui.lib 12 | qui-test-* 13 | *.exe 14 | *.o 15 | *.obj 16 | *.lst 17 | *.pdb 18 | dub.selections.json -------------------------------------------------------------------------------- /source/demo.d: -------------------------------------------------------------------------------- 1 | module demo; 2 | version(demo){ 3 | import qui.qui, 4 | qui.widgets; 5 | 6 | import std.stdio; 7 | 8 | void main (){ 9 | QTerminal term = new QTerminal; 10 | term.widget = new ScrollTestingWidget(true); 11 | term.widget.heightConstraint(80); 12 | 13 | term.run; 14 | .destroy(term); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "qui" 2 | description "A Text User Interface library." 3 | copyright "Copyright © 2017-2024, Nafees Hassan" 4 | authors "Nafees Hassan" 5 | license "MIT" 6 | dependency "arsd-official:terminal" version="~>10.9.10" 7 | configuration "default"{ 8 | targetType "library" 9 | targetName "qui" 10 | } 11 | configuration "quidemo"{ 12 | targetType "executable" 13 | targetName "quidemo" 14 | versions "demo" 15 | } 16 | -------------------------------------------------------------------------------- /docs/qterminal.md: -------------------------------------------------------------------------------- 1 | # QTerminal 2 | 3 | This is a QContainer over the entire terminal. 4 | 5 | ## Properties 6 | 7 | * `bool stopOnInterrupt` - whether to stop UI loop (run function) on hangup 8 | interrupt (Ctrl+C) 9 | * `ushort timerMsecs` - amount of milliseconds between timerEvents 10 | * `ushort updateMsecs` - minimum number of milliseconds to wait before a 11 | consecutive update event is triggered. Used as a fps limiter. Default value 12 | of 50 results in 20 fps. 13 | 14 | ## Functions 15 | 16 | * `stop()` - Stops UI loop. It is not instant. 17 | * `run()` - runs the UI loop. 18 | -------------------------------------------------------------------------------- /docs/qcontainer.md: -------------------------------------------------------------------------------- 1 | # QContainer 2 | 3 | A fancy alternative to QLayout that houses only 1 child widget, but is able to 4 | create space to fit any size widget. Intended to be used as a base class for 5 | creating scrollable containers. 6 | 7 | ## Properties 8 | 9 | The following public properties are available: 10 | 11 | * `QWidget widget` _setter/getter_ - the child widget 12 | * `uint scrollX` _setter/getter_ - how many rows scrolled out of view 13 | * `uint scrollY` _setter/getter_ - how many columns scrolled out of view 14 | * `uint scrollbarVisibleForMsecs` _setter/getter_ - how many milliseconds the 15 | scrollbar is visible for, after scrolling 16 | 17 | ## Events 18 | 19 | QContainer overrides all the events from QWidget, along with the `disownEvent` 20 | from QParent. 21 | 22 | These events just forward the events to the child class, with the exception of 23 | resize and scroll event, which will fix the viewport for the child before 24 | forwarding. 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2024 Nafees Hassan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/viewport.md: -------------------------------------------------------------------------------- 1 | # Viewport 2 | 3 | Used to draw to terminal by widgets. 4 | 5 | ## Properties 6 | * `view.width` - width of the viewport 7 | * `view.height` - height of the viewport 8 | * `view.x` - offset on x coordinates. This number of columns are not visible 9 | i.e: scrolled, or overlapping etc. Drawing only allowed at 10 | `view.x .. view.x + view.width` 11 | * `view.y` - offset on y coordinates. This number of rows are not visible 12 | i.e: scrolled, or overlapping etc. Drawing only allowed at 13 | `view.y .. view.y + view.height` 14 | 15 | ## Functions 16 | * `isWritable(x, y)` - Whether x and y are inside the limits written above. 17 | * `moveTo(x, y)` - Move to x,y for next write. Returns true if isWritable 18 | * `write(dchar, foreground, background)` - writes a single character with 19 | colors. Returns true if written, false if outside bounds. 20 | * `write(dstring, foreground, background)` - same but for a string. 21 | Returns number of characters written. 22 | * `fillLine(dchar, foregroumd, background, max)` - fills line, starting from 23 | where the cursor is right now. if max is not zero, will only write maximum of 24 | that number of characters. Returns number of characters written. 25 | -------------------------------------------------------------------------------- /docs/qlayout.md: -------------------------------------------------------------------------------- 1 | # QLayout 2 | 3 | This is a very basic container widget, which basically acts as if the widgets 4 | it is containing, were 1 widget. 5 | 6 | It will either place the widgets in a horizontal or vertical order. 7 | It attempts to best mimic the sizing properties of its child widgets combined. 8 | 9 | To create a horizontal layout, use the `QHorizontalLayout` widget, for 10 | vertical, use `QVerticalLayout`. 11 | 12 | ## Sizing 13 | QLayout will attempt to size all its child widgets equally. 14 | For example, a `QVerticalLayout` will try to divide its height equally among 15 | it's children, and will try to set its own width to it's children. It does so 16 | while respecting it's children's size constraints. 17 | 18 | The sizing algorithm is somewhat simple: 19 | ```psuedocode 20 | Assign size across common axis to all widgets 21 | 22 | Divide size across other axis among all (remaining) widgets. 23 | 24 | If any widget's size constraints triggered, assign it the constrained size, 25 | reduce its size from total available size, start over from step 2. 26 | ``` 27 | 28 | ## Managing Child Widgets 29 | 30 | QLayout provides these functions for managing child widgets: 31 | 32 | * `widgetAdd(QWidget)` - adopts and adds a widget to the end 33 | * `bool widgetRemove(QWidget)` - disowns and removes a widget. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QUI 2 | QUI is a Text User Interface library for the [D Language](http://dlang.org/). 3 | 4 | --- 5 | 6 | ## Features 7 | 8 | 1. OOP based 9 | 1. Support for Mouse events 10 | 1. Abstracted scrolling 11 | 1. Tries to optimise drawing 12 | 13 | --- 14 | 15 | ## Setting it up 16 | To use qui in your dub package, run this in your dub package's directory: 17 | ```bash 18 | dub add qui 19 | ``` 20 | 21 | --- 22 | 23 | ## Getting Started 24 | Some built in widgets are contained in `qui.widgets`, and the base classes are 25 | in `qui.qui`. 26 | 27 | You should also read through `docs/*.md` for a quick start on how to use and 28 | write new widgets. 29 | 30 | ### Building demo 31 | The included demo configuration (`source/demo.d`) demonstrates the usage of 32 | some of the included widgets. To build & run it, run the following: 33 | ```bash 34 | dub fetch qui 35 | dub run qui -b=release -c=quidemo 36 | ``` 37 | 38 | --- 39 | 40 | ## Documentation 41 | See `docs/` for documentation on how to use qui and how to write widgets. 42 | 43 | Additionally, you could also see `source/qui/widgets.d` and see some existing 44 | widgets, this can be helpful in writing new widgets. 45 | 46 | --- 47 | 48 | ## Known Issues 49 | See the issues tab. 50 | 51 | --- 52 | 53 | ## `TODO` for upcoming versions 54 | See issues marked as `enhancement`. 55 | 56 | --- 57 | 58 | ## License 59 | QUI is licensed under the MIT license - see [LICENSE](LICENSE). 60 | 61 | QUI uses [Adam D. Ruppe](https://github.com/adamdruppe)'s 62 | [terminal.d](https://github.com/adamdruppe/arsd/blob/master/terminal.d) which 63 | is licensed under the Boost License - see `source/arsd/LICENSE`. 64 | -------------------------------------------------------------------------------- /docs/qwidget.md: -------------------------------------------------------------------------------- 1 | # QUI Widgets 2 | 3 | This document will help you understand how to write/use QUI widgets. 4 | 5 | ## Protected 6 | 7 | These are never accessed directly outside of the QWidget class, 8 | only through their respective getters: 9 | * `uint _minWidth` - Minimum width specifier. Accessed through setter/getter 10 | * `uint _maxWdith` - Maximum width specifier. Accessed through setter/getter 11 | * `uint _minHeight` - Minimum height specifier. Accessed through setter/getter 12 | * `uint _maxHeight` - Maximum height specifier. Accessed through setter/getter 13 | 14 | The following can be used to send requests to the parent widget this widget 15 | resides in, only if this widget is the active widget: 16 | * `bool scrollToX(uint)` - Request parent to scroll to an X coordinate 17 | * `bool scrollToY(uint)` - Request parent to scroll to an Y coordinate 18 | * `void cursorPos(x, y)` - Request parent to draw cursor at x, y. 19 | Sending negative values for either x or y will hide the cursor. 20 | 21 | The following work even if this widget is not the active widget: 22 | * `bool update()` - requests parent to update this widget. Without calling 23 | this, the `updateEvent` will never occur. 24 | * `bool resize()` - requests parent to resize widgets. This should be called 25 | when the widget wishes to be resized. **Do not call in a resizeEvent** 26 | 27 | The `view` property, of type `Viewport`, can be used to draw on the area 28 | designated to the widget. See `docs/viewport.md`. 29 | 30 | Event handling functions are also protected. See `docs/events.md` 31 | 32 | ## Public 33 | 34 | Functions: 35 | * `bool heightConstraint(uint min, uint max)` - For setting min/maxHeight 36 | * `bool widthConstraint(uint min, uint max)` - For setting min/maxWidth 37 | * `bool sizeConstraint(minWidth, maxWidth, minHeight, maxHeight)` 38 | 39 | Properties: 40 | * `uint width` _final_ - Width of this widget. 41 | * `uint height` _final_ - Height of this widget. 42 | * `uint minWidth` - Minimum width. 43 | * `uint maxWdith` - Maximum width. 44 | * `uint minHeight` - Minimum height. 45 | * `uint maxHeight` - Maximum height. 46 | * `bool heightConstrained` _final_ - Whether the min/max height constraints 47 | apply. This is determined by if either min/max is non-zero, and if both are 48 | non-zero, then `min <= max` 49 | * `bool widthConstrained` _final_ - Same as heightConstrained but for width. 50 | * `bool sizeConstrained` _final_ - if either width or height is constrained. 51 | * `bool wantsFocus` - Should return true if this widget wants keyboard input. 52 | By default it always returns false, should be overrided accordingly. 53 | 54 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | # QUI Events 2 | 3 | All event functions are protected boolean functions. They must return true if 4 | the Widget handled the event, else, false. 5 | 6 | This is especially useful for keyboard events. Where returning false will give 7 | up focus, and the parent will look for another widget to send further keyboard 8 | input to. 9 | 10 | Also for mouse events. Return false behaviour may be used to help in scrolling. 11 | 12 | ## `bool adoptEvent(bool)` 13 | Called when this widget is adopted by a parent, or disowned. The boolean flag 14 | will be true when it is adopted, false when disowned. 15 | 16 | Usually, you will want to `requestResize` in this. 17 | 18 | ## `bool mouseEvent(MouseEvent)` 19 | Called when a mouse event occurs while the mouse cursor was on top of this 20 | widget. 21 | 22 | The coordinates in MouseEvent are relative to top-left (0,0) corner of this 23 | widget. 24 | 25 | ## `bool keyboardEvent(KeyboardEvent, bool)` 26 | Called when this widget is the active widget, and a keyboard event occurs. 27 | 28 | It also receives a flag, which is true in case the parent wants to cycle 29 | focus to next possible active widget. In most scenarios, you will want to do 30 | nothing and `return false` in case the flag is true. 31 | 32 | Note that by returning false, whether or not the flag is true, will move focus 33 | away from this widget. 34 | 35 | ## `bool resizeEvent()` 36 | Called when the parent underwent resizing, and this widget may have been resized 37 | as well. 38 | 39 | Usually you will want to `requestUpdate` in this. 40 | 41 | ## `bool scrollEvent()` 42 | Called when this widget or its parent has been scrolled. 43 | 44 | Usually you will want to `requestUpdate` in this. 45 | 46 | ## `bool activateEvent(bool)` 47 | This is called whenever this widget is made the active widget, or is un-made the 48 | active widget. 49 | 50 | If it was made the active widget, the flag will be true, else it will be false. 51 | 52 | ## `bool timerEvent(uint msecs)` 53 | Called every time `msecs` amount of milliseconds have passed. 54 | 55 | ## `bool updateEvent()` 56 | This is the event in which your widget shold draw itself. 57 | 58 | # Custom Event Handlers 59 | 60 | In case you are not writing a widget, but using one, and you want a function to 61 | be called each time a specific event handler for a widget is called, you can 62 | use custom events. 63 | 64 | These can be assigned like: 65 | ```D 66 | QWidget.onInitEvent = delegate(QWidget callerWidget){ 67 | // do stuff 68 | } 69 | QWidget.onTimerEvent = delegate(QWidget callerWidget, uint msecs){ 70 | // do stuff 71 | } 72 | QWidget.onMouseEvent = delegate(QWidget callerWidget, MouseEvent mouse){ 73 | // do stuff 74 | } 75 | ``` 76 | All events can be assigned custom event handlers in this manner. 77 | 78 | These functions are called before the event handler of that widget is called. 79 | 80 | -------------------------------------------------------------------------------- /docs/qparent.md: -------------------------------------------------------------------------------- 1 | # QParent 2 | 3 | This is to be used as a base class for creating containers or parent widgets. 4 | 5 | It provides several functions to access private properties of a child widget. 6 | 7 | ## Events 8 | 9 | These events, on top of the ones from QWidget, are available to QParent. 10 | 11 | ### `disownEvent(QWidget)` 12 | Called right before this is made to disown a child. 13 | 14 | ## Functions 15 | 16 | All these functions first perform a check to make sure the child widget is 17 | a child of the QParent calling the function. 18 | 19 | ### `final widgetPositionX(QWidget, uint)` 20 | Sets the position along x coordinate of a child widget. 21 | 22 | ### `final widgetPositionY(QWidget, uint)` 23 | Sets the position along y coordinate of a child widget. 24 | 25 | ### `final widgetPosition(QWidget, uint x, uint y)` 26 | Sets the position along x and y coordinate of a child widget. 27 | 28 | ### `final bool widgetSizeWidth(QWidget, uint)` 29 | Attempts to set the width of a child widget. If the width is constrained, 30 | appropriate value will be selected. 31 | 32 | Returns true if paramter value (a.k.a natural size) was used, false if a 33 | constrained value was used. 34 | 35 | ### `final bool widgetSizeHeight(QWidget, uint)` 36 | Attempts to set the height of a child widget. If the height is constrained, 37 | appropriate value will be selected. 38 | 39 | Returns true if paramter value (a.k.a natural size) was used, false if a 40 | constrained value was used. 41 | 42 | ### `final bool widgetSize(QWidget, uint width, uint height)` 43 | Sets both width and height of widget. 44 | 45 | Returns true if paramter values were used, false if constrained values were 46 | used. 47 | 48 | ### `final bool widgetViewportAssign(QWidget, width, height, scrollX, scrollY)` 49 | Assigns viewport to a child, of size width x height. The default width & height 50 | of 0 will cause it to use the widget's own width & height. 51 | 52 | scrollX and scrollY can be used to add to the x and y offsets in the viewport, 53 | which can give an effect of scrolling to the child widget. 54 | 55 | ### `final adopt(QWidget)` 56 | Adopts a widget. If the widget currently has a parent, the current parent will 57 | disown it, the current parent's disown event will be called, and the child's 58 | `adoptEvent(false)` will be called. 59 | 60 | Child's `adoptEvent(true)` will be called at end. 61 | 62 | ### `final bool disown(QWidget)` 63 | Disowns a widget. Will return false if the widget is its child. 64 | The widget's viewport will be reset, caller's disownEvent will be called, and 65 | child's `adoptEvent(false)` will be called. 66 | 67 | ## Event Triggers 68 | The following functions are used to send events to child widget. 69 | As usual, they perform a check to ensure the caller is the child's parent, if 70 | not, they return false. 71 | 72 | In the case the event is sent, the return value is what was returned from the 73 | child. 74 | 75 | Any appropriate custom event on the child is called before calling the event 76 | handler function. 77 | 78 | For description on what these events are, see `docs/events.md` 79 | 80 | * `bool adoptEventCall(QWidget, bool)` 81 | * `bool mouseEventCall(QWidget, MouseEvent)` 82 | * `bool keyboardEventCall(QWidget, KeyboardEvent, bool)` 83 | * `bool resizeEventCall(QWidget)` 84 | * `bool scrollEventCall(QWidget)` 85 | * `bool activateEvent(QWidget, bool)` 86 | * `bool timerEventCall(QWidget, uint)` 87 | * `bool updateEventCall(QWidget)` 88 | -------------------------------------------------------------------------------- /source/qui/termwrap.d: -------------------------------------------------------------------------------- 1 | /++ 2 | This module just tries to simplify arsd.terminal.d by removing some features that aren't needed (yet, at least) 3 | +/ 4 | module qui.termwrap; 5 | 6 | import arsd.terminal; 7 | import std.datetime.stopwatch; 8 | import std.conv : to; 9 | 10 | package enum Color : ushort{ 11 | Default = 256, 12 | Black = arsd.terminal.Color.black, 13 | BlackBright = arsd.terminal.Color.black | 0x08, 14 | Red = arsd.terminal.Color.red, 15 | RedBright = arsd.terminal.Color.red | 0x08, 16 | Green = arsd.terminal.Color.green, 17 | GreenBright = arsd.terminal.Color.green | 0x08, 18 | Blue = arsd.terminal.Color.blue, 19 | BlueBright = arsd.terminal.Color.blue | 0x08, 20 | Yellow = arsd.terminal.Color.yellow, 21 | YellowBright = arsd.terminal.Color.yellow | 0x08, 22 | Magenta = arsd.terminal.Color.magenta, 23 | MagentaBright = arsd.terminal.Color.magenta | 0x08, 24 | Cyan = arsd.terminal.Color.cyan, 25 | CyanBright = arsd.terminal.Color.cyan | 0x08, 26 | White = arsd.terminal.Color.white, 27 | WhiteBright = arsd.terminal.Color.white | 0x08, 28 | } 29 | 30 | /// Input events 31 | package struct Event{ 32 | /// Keyboard Event 33 | struct Keyboard{ 34 | private this(KeyboardEvent event){ 35 | key = event.which; 36 | mod = event.modifierStateFiltered; 37 | // check for 1-26 ctrl keys 38 | if (key != 0 && key <= 26 && (key < 8 || key > 10)){ 39 | // ctrl was pressed with letter 40 | mod |= Modifier.Control; 41 | key += 'a' - 1; 42 | } 43 | } 44 | this (dchar key, uint mod = 0){ 45 | this.key = key; 46 | this.mod = mod; 47 | } 48 | 49 | this (Key key, uint mod = 0){ 50 | this.key = key; 51 | this.mod = mod; 52 | } 53 | 54 | //// what key was pressed 55 | dchar key; 56 | /// what modifiers were pressed (`&` with Modifier enum) 57 | uint mod; 58 | 59 | /// Non character keys (can match against `this.key`) 60 | /// 61 | /// copied from arsd.terminal 62 | enum Key : dchar{ 63 | Escape = 0x1b + 0xF0000, 64 | F1 = 0x70 + 0xF0000, 65 | F2 = 0x71 + 0xF0000, 66 | F3 = 0x72 + 0xF0000, 67 | F4 = 0x73 + 0xF0000, 68 | F5 = 0x74 + 0xF0000, 69 | F6 = 0x75 + 0xF0000, 70 | F7 = 0x76 + 0xF0000, 71 | F8 = 0x77 + 0xF0000, 72 | F9 = 0x78 + 0xF0000, 73 | F10 = 0x79 + 0xF0000, 74 | F11 = 0x7A + 0xF0000, 75 | F12 = 0x7B + 0xF0000, 76 | LeftArrow = 0x25 + 0xF0000, 77 | RightArrow = 0x27 + 0xF0000, 78 | UpArrow = 0x26 + 0xF0000, 79 | DownArrow = 0x28 + 0xF0000, 80 | Insert = 0x2d + 0xF0000, 81 | Delete = 0x2e + 0xF0000, 82 | Home = 0x24 + 0xF0000, 83 | End = 0x23 + 0xF0000, 84 | PageUp = 0x21 + 0xF0000, 85 | PageDown = 0x22 + 0xF0000, 86 | } 87 | 88 | /// Modifier Keys 89 | enum Modifier : uint{ 90 | Shift = 4, 91 | Alt = 2, 92 | Control = 16, 93 | Meta = 8, 94 | } 95 | 96 | /// Returns: true if the key pressed is a character 97 | /// backspace, space, and tab are characters! 98 | @property bool isChar() const { 99 | return !(key >= Key.min && key <= Key.max); 100 | } 101 | /// Returns: a string representation of the key pressed 102 | @property string toString() const { 103 | string modStr; 104 | if (mod & Modifier.Control) 105 | modStr ~= "Control"; 106 | if (mod & Modifier.Alt) 107 | modStr ~= modStr.length ? ", Alt" : "Alt"; 108 | if (mod & Modifier.Shift) 109 | modStr ~= modStr.length ? ", Shift" : "Shift"; 110 | if (mod & Modifier.Meta) 111 | modStr ~= modStr.length ? ", Meta" : "Meta"; 112 | if (isChar()) 113 | return "{key:\'" ~ to!string(key) ~ "\', mod:" ~ modStr ~ "}"; 114 | return 115 | "{key:\'" ~ to!string(cast(Key)key) ~ "\', mod:" ~ modStr ~"}"; 116 | } 117 | } 118 | 119 | /// Mouse Event 120 | struct Mouse{ 121 | /// Buttons 122 | enum Button : ubyte{ 123 | Left = 0x00, /// Left mouse btn clicked 124 | Right = 0x10, /// Right mouse btn clicked 125 | Middle = 0x20, /// Middle mouse btn clicked 126 | ScrollUp = 0x30, /// Scroll up clicked 127 | ScrollDown = 0x40, /// Scroll Down clicked 128 | None = 0x50, /// . 129 | } 130 | 131 | /// State 132 | enum State : ubyte{ 133 | Click = 1, /// Clicked 134 | Release = 1 << 1, /// Released 135 | Hover = 1 << 2, /// Hovered 136 | } 137 | 138 | /// x and y position of cursor 139 | int x, y; 140 | /// button and type (press/release/hover) 141 | /// access this using `this.button` and `this.state` 142 | ubyte type; 143 | 144 | /// what button was clicked 145 | @property Button button(){ 146 | return cast(Button)(type & 0xF0); 147 | } 148 | /// ditto 149 | @property Button button(Button newVal){ 150 | type = this.state | newVal; 151 | return newVal; 152 | } 153 | 154 | /// State (Clicked/Released/...) 155 | @property State state(){ 156 | return cast(State)(type & 0x0F); 157 | } 158 | /// ditto 159 | @property State state(State newVal){ 160 | type = this.button | newVal; 161 | return newVal; 162 | } 163 | 164 | /// constructor 165 | this (Button btn, int xPos, int yPos){ 166 | x = xPos; 167 | y = yPos; 168 | btn = button; 169 | } 170 | 171 | /// constructor, from arsd.terminal.MouseEvent 172 | private this(MouseEvent mouseE){ 173 | if (mouseE.buttons & MouseEvent.Button.Left) 174 | this.button = this.Button.Left; 175 | else if (mouseE.buttons & MouseEvent.Button.Right) 176 | this.button = this.Button.Right; 177 | else if (mouseE.buttons & MouseEvent.Button.Middle) 178 | this.button = this.Button.Middle; 179 | else if (mouseE.buttons & MouseEvent.Button.ScrollUp) 180 | this.button = this.Button.ScrollUp; 181 | else if (mouseE.buttons & MouseEvent.Button.ScrollDown) 182 | this.button = this.Button.ScrollDown; 183 | else 184 | this.button = this.Button.None; 185 | if (mouseE.eventType == mouseE.Type.Clicked || 186 | mouseE.eventType == mouseE.Type.Pressed) 187 | this.state = State.Click; 188 | else if (mouseE.eventType == mouseE.Type.Released) 189 | this.state = State.Release; 190 | else 191 | this.state = State.Hover; 192 | this.x = mouseE.x; 193 | this.y = mouseE.y; 194 | } 195 | 196 | /// Returns: string representation of this 197 | @property string tostring(){ 198 | return "{button:" ~ button.to!string ~ ", state:" ~ state.to!string ~ 199 | ", x:" ~ x.to!string ~ ", y:" ~ y.to!string ~ "}"; 200 | } 201 | } 202 | 203 | /// Resize event 204 | struct Resize{ 205 | /// the new width and height after resize 206 | int width, height; 207 | } 208 | 209 | /// types of events 210 | enum Type{ 211 | Keyboard, /// Keyboard event 212 | Mouse, /// Mouse event 213 | Resize, /// Resize event 214 | HangupInterrupt, /// terminal closed or interrupt (Ctrl+C) 215 | } 216 | 217 | /// stores the type of event 218 | private Type _type; 219 | 220 | /// union to store events 221 | union{ 222 | Keyboard _key; /// stores keyboard event 223 | Mouse _mouse; /// stores mouse event 224 | Resize _resize; /// stores resize event 225 | } 226 | 227 | /// Returns: type of event 228 | @property Type type(){ 229 | return _type; 230 | } 231 | /// Returns: keyboard event. Make sure you check this.type so the wrong event 232 | /// isn't read 233 | @property Keyboard keyboard(){ 234 | return _key; 235 | } 236 | /// Returns: mouse event. Make sure you check this.type so the wrong event 237 | /// isn't read 238 | @property Mouse mouse(){ 239 | return _mouse; 240 | } 241 | /// Returns: resize event. Make sure you check this.type so the wrong event 242 | /// isn't read 243 | @property Resize resize(){ 244 | return _resize; 245 | } 246 | 247 | private this(Keyboard key){ 248 | this._type = Type.Keyboard; 249 | this._key = key; 250 | } 251 | 252 | private this(Mouse mouse){ 253 | this._type = Type.Mouse; 254 | this._mouse = mouse; 255 | } 256 | 257 | private this(Resize rsize){ 258 | this._type = Type.Resize; 259 | this._resize = rsize; 260 | } 261 | 262 | /// constructor, by default, its a hangupInterrupt 263 | this(Type eType){ 264 | this._type = eType; 265 | } 266 | } 267 | 268 | /// Wrapper to arsd.terminal to make it bit easier to manage 269 | package class TermWrapper{ 270 | private: 271 | Terminal _term; 272 | RealTimeConsoleInput _input; 273 | public: 274 | /// constructor 275 | this(){ 276 | _term = Terminal(ConsoleOutputType.cellular); 277 | _input = RealTimeConsoleInput(&_term, ConsoleInputFlags.allInputEvents | 278 | ConsoleInputFlags.raw); 279 | } 280 | ~this(){ 281 | _term.clear; 282 | _term.reset; 283 | } 284 | 285 | /// Returns: width of termial 286 | @property int width(){ 287 | return _term.width; 288 | } 289 | /// Returns: height of terminal 290 | @property int height(){ 291 | return _term.height; 292 | } 293 | 294 | /// flush to terminal 295 | void flush(){ 296 | _term.flush(); 297 | } 298 | 299 | /// writes a character `ch` at a position `(x, y)` 300 | void put(int x, int y, dchar ch){ 301 | _term.moveTo(x, y); 302 | _term.write(ch); 303 | } 304 | 305 | /// sets colors 306 | void color(Color fg, Color bg){ 307 | _term.color(fg, bg); 308 | } 309 | 310 | /// moves cursor to position 311 | void moveCursor(int x, int y){ 312 | _term.moveTo(x, y); 313 | } 314 | 315 | /// Set true to show cursor, false to hide cursor 316 | @property bool cursorVisible(bool visibility){ 317 | if (visibility) 318 | _term.showCursor; 319 | else 320 | _term.hideCursor; 321 | return visibility; 322 | } 323 | 324 | /// waits `msecTimeout` msecs for event to occur. Returns as soon as it 325 | /// occurs (or if one had occurred before calling it) 326 | /// 327 | /// Returns: true if event occured 328 | bool getEvent(int msecTimeout, ref Event event){ 329 | StopWatch sw; 330 | sw.start; 331 | while (msecTimeout - cast(int)sw.peek.total!"msecs" > 0){ 332 | if (_input.timedCheckForInput(msecTimeout - 333 | cast(int)sw.peek.total!"msecs")){ 334 | InputEvent e = _input.nextEvent; 335 | if (e.type == InputEvent.Type.HangupEvent || 336 | e.type == InputEvent.Type.UserInterruptionEvent){ 337 | event = Event(Event.Type.HangupInterrupt); 338 | return true; 339 | } 340 | if (e.type == InputEvent.Type.KeyboardEvent){ 341 | event = Event(Event.Keyboard(e.get!(InputEvent.Type.KeyboardEvent))); 342 | // fix for issue #16 343 | // ("Escape key registered as a character event as well") 344 | if (event._key.key == 27) 345 | continue; 346 | return true; 347 | } 348 | if (e.type == InputEvent.Type.MouseEvent){ 349 | MouseEvent mouseE = e.get!(InputEvent.Type.MouseEvent); 350 | event = Event(Event.Mouse(mouseE)); 351 | return true; 352 | }else if (e.type == InputEvent.Type.SizeChangedEvent){ 353 | SizeChangedEvent resize = e.get!(InputEvent.Type.SizeChangedEvent); 354 | event = Event(Event.Resize(resize.newWidth, resize.newHeight)); 355 | return true; 356 | } 357 | } 358 | } 359 | sw.stop; 360 | return false; 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /source/qui/widgets.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Some widgets that are included in the package. 3 | +/ 4 | module qui.widgets; 5 | 6 | import qui.qui; 7 | 8 | import std.conv, 9 | std.algorithm; 10 | 11 | debug import std.stdio; 12 | 13 | /// for testing only. 14 | class ScrollTestingWidget : QWidget{ 15 | protected: 16 | /// whether to display debug info 17 | bool _debugInfo; 18 | 19 | override bool adoptEvent(bool adopted){ 20 | if (!adopted) 21 | return false; 22 | resize; 23 | return true; 24 | } 25 | 26 | override bool scrollEvent(){ 27 | update; 28 | return true; 29 | } 30 | 31 | override bool resizeEvent(){ 32 | update; 33 | return true; 34 | } 35 | 36 | override bool updateEvent(){ 37 | foreach (y; view.y .. view.y + view.height){ 38 | view.moveTo(view.x, y); 39 | foreach (x; view.x .. view.x + view.width) 40 | view.write(((x+y) % 10).to!dstring[0]); 41 | } 42 | if (!_debugInfo) 43 | return true; 44 | view.moveTo(view.x, view.y); 45 | view.write("size, width x height: " ~ 46 | to!dstring(width) ~ "x" ~ to!dstring(height) ~ '|'); 47 | 48 | view.moveTo(view.x, view.y + 1); 49 | view.write("view X, Y: " ~ 50 | to!dstring(view.x) ~ "," ~ to!dstring(view.y) ~ '|'); 51 | 52 | view.moveTo(view.x, view.y + 2); 53 | view.write("view width x height: " ~ 54 | to!dstring(view.width) ~ "x" ~ to!dstring(view.height) ~ '|'); 55 | return true; 56 | } 57 | public: 58 | /// constructor 59 | this(bool debugInfo = false, 60 | Color textColor = Color.Default, Color backgroundColor = Color.Default){ 61 | _debugInfo = debugInfo; 62 | } 63 | } 64 | 65 | /// Displays some text 66 | /// 67 | /// doesnt handle new-line characters 68 | class TextLabelWidget : QWidget{ 69 | private: 70 | /// text and background colors 71 | Color _fg = Color.Default, _bg = Color.Default; 72 | /// the text to display 73 | dstring _caption; 74 | protected: 75 | 76 | override bool adoptEvent(bool adopted){ 77 | if (!adopted) 78 | return false; 79 | resize; 80 | return true; 81 | } 82 | 83 | override bool scrollEvent(){ 84 | update; 85 | return true; 86 | } 87 | 88 | override bool resizeEvent(){ 89 | update; 90 | return true; 91 | } 92 | 93 | override bool updateEvent(){ 94 | view.moveTo(view.x, view.y); 95 | if (view.x > _caption.length) 96 | view.fillLine(' ', _fg, _bg); 97 | else 98 | view.write(_caption[view.x .. $], _fg, _bg); 99 | return true; 100 | } 101 | public: 102 | /// constructor 103 | this(dstring caption = ""){ 104 | _caption = caption; 105 | _maxHeight = 1; 106 | _maxWidth = cast(uint)caption.length; 107 | _minWidth = _maxWidth; 108 | } 109 | 110 | /// the text to display 111 | @property dstring caption(){ 112 | return _caption; 113 | } 114 | /// ditto 115 | @property dstring caption(dstring newCaption){ 116 | _caption = newCaption; 117 | _maxWidth = cast(uint)caption.length; 118 | _minWidth = _maxWidth; 119 | update; 120 | return _caption; 121 | } 122 | 123 | /// text color 124 | @property Color textColor(){ 125 | return _fg; 126 | } 127 | /// ditto 128 | @property Color textColor(Color newColor){ 129 | _fg = newColor; 130 | update; 131 | return _fg; 132 | } 133 | 134 | /// background color 135 | @property Color backColor(){ 136 | return _bg; 137 | } 138 | /// ditto 139 | @property Color backColor(Color newColor){ 140 | _bg = newColor; 141 | update; 142 | return _bg; 143 | } 144 | 145 | override @property uint minWidth(){ 146 | return cast(uint)_caption.length; 147 | } 148 | override @property uint maxWidth(){ 149 | return cast(uint)_caption.length; 150 | } 151 | override @property uint minHeight(){ 152 | return 1; 153 | } 154 | override @property uint maxHeight(){ 155 | return 1; 156 | } 157 | } 158 | 159 | /// To get single-line input from keyboard 160 | class EditLineWidget : QWidget{ 161 | private: 162 | /// text that's been input-ed 163 | dchar[] _text; 164 | /// position of cursor. Next write happens at `_text[_x]`, 165 | /// next deletion happens at `_text[_x - 1]` 166 | uint _x; 167 | /// Foreground and background colors 168 | Color _fg = Color.Default, _bg = Color.Default; 169 | 170 | protected: 171 | /// moves cursor to left by n characters 172 | void cursorMoveLeft(uint n = 1){ 173 | if (n > _x) 174 | _x = 0; 175 | else 176 | _x -= n; 177 | } 178 | 179 | /// moves cursor to right by n characters 180 | void cursorMoveRight(uint n = 1){ 181 | _x += n; 182 | if (_x > _text.length) 183 | _x = cast(uint)_text.length; 184 | } 185 | 186 | /// does backspace at current cursor position 187 | void cursorBackspace(){ 188 | if (_x == 0) 189 | return; 190 | if (_x < _text.length){ 191 | foreach (i; _x .. _text.length) 192 | _text[i - 1] = _text[i]; 193 | } 194 | if (_text.length) 195 | _text = _text[0 .. $ - 1]; 196 | _x --; 197 | } 198 | 199 | /// does delete at current cursor position 200 | void cursorDelete(){ 201 | if (_x >= _text.length) 202 | return; 203 | _text[_x .. $ - 1] = _text[_x + 1 .. $]; 204 | _text.length --; 205 | } 206 | 207 | /// Writes a character at current cursor position 208 | void cursorInsert(dchar key){ 209 | if (_x == _text.length) 210 | _text ~= key; 211 | else 212 | _text = _text[0 .. _x] ~ key ~ _text[_x .. $]; 213 | cursorMoveRight; 214 | } 215 | 216 | /// Ensures cursor position is valid, if not, fixes it 217 | void cursorCorrect(){ 218 | if (_x > _text.length) 219 | _x = cast(uint)text.length; 220 | } 221 | 222 | override bool activateEvent(bool isActive){ 223 | if (!isActive) 224 | return false; 225 | cursorPos(_x, 0); 226 | update; 227 | return true; 228 | } 229 | 230 | override bool adoptEvent(bool adopted){ 231 | if (!adopted) 232 | return false; 233 | resize; 234 | return true; 235 | } 236 | 237 | override bool resizeEvent(){ 238 | update; 239 | return true; 240 | } 241 | 242 | override bool scrollEvent(){ 243 | update; 244 | return true; 245 | } 246 | 247 | override bool mouseEvent(MouseEvent mouse){ 248 | if (mouse.button != MouseEvent.Button.Left || 249 | mouse.state != MouseEvent.State.Click) 250 | return false; 251 | _x = mouse.x; 252 | cursorCorrect; 253 | update; 254 | return true; 255 | } 256 | 257 | override bool keyboardEvent(KeyboardEvent key, bool cycle){ 258 | if (cycle || key.key == '\n') 259 | return false; 260 | if (!key.isChar){ 261 | switch (key.key){ 262 | case Key.Delete: 263 | cursorDelete; break; 264 | case Key.LeftArrow: 265 | cursorMoveLeft; break; 266 | case Key.RightArrow: 267 | cursorMoveRight; break; 268 | default: 269 | break; 270 | } 271 | }else{ 272 | switch (key.key){ 273 | case '\b': 274 | cursorBackspace; break; 275 | default: 276 | cursorInsert(key.key); 277 | } 278 | } 279 | update; 280 | return true; 281 | } 282 | 283 | override bool updateEvent(){ 284 | view.moveTo(view.x, view.y); 285 | if (view.x < _text.length) 286 | view.write(cast(dstring)_text[view.x .. $], _fg, _bg); 287 | view.fillLine(' ', _fg, _bg); 288 | cursorPos(_x, 0); 289 | return true; 290 | } 291 | public: 292 | /// constructor 293 | this(dstring text = ""){ 294 | this._text = cast(dchar[])text.dup; 295 | _maxHeight = 1; 296 | } 297 | 298 | /// text 299 | @property dstring text(){ 300 | return cast(dstring)_text.dup; 301 | } 302 | /// ditto 303 | @property dstring text(dstring newText){ 304 | _text = cast(dchar[])newText.dup; 305 | update; 306 | return cast(dstring)newText; 307 | } 308 | 309 | override @property bool wantsFocus() const { 310 | return true; 311 | } 312 | 313 | /// text color 314 | @property Color textColor(){ 315 | return _fg; 316 | } 317 | /// ditto 318 | @property Color textColor(Color newColor){ 319 | _fg = newColor; 320 | update; 321 | return _fg; 322 | } 323 | 324 | /// background color 325 | @property Color backColor(){ 326 | return _bg; 327 | } 328 | /// ditto 329 | @property Color backColor(Color newColor){ 330 | _bg = newColor; 331 | update; 332 | return _bg; 333 | } 334 | 335 | override @property uint minWidth(){ 336 | return cast(uint)_text.length; 337 | } 338 | override @property uint maxHeight(){ 339 | return 1; 340 | } 341 | } 342 | 343 | /// a Memo Widget, mutli line readonly text 344 | class MemoWidget : QWidget{ 345 | private: 346 | /// text buffer 347 | dstring[] _lines; 348 | /// Colors 349 | Color _fg = Color.Default, _bg = Color.Default; 350 | 351 | /// updates minimum/maximum width/height based on _lines 352 | void _size(){ 353 | uint w = uint.max; 354 | foreach (line; _lines) 355 | w = max(w, cast(uint)line.length); 356 | _minWidth = _maxWidth = w; 357 | _minHeight = _maxHeight = cast(uint)_lines.length; 358 | } 359 | 360 | protected: 361 | override bool activateEvent(bool isActive){ 362 | if (isActive) 363 | update; 364 | return true; 365 | } 366 | 367 | override bool adoptEvent(bool adopted){ 368 | if (adopted) 369 | update; 370 | return true; 371 | } 372 | 373 | override bool resizeEvent(){ 374 | update; 375 | return true; 376 | } 377 | 378 | override bool scrollEvent(){ 379 | update; 380 | return true; 381 | } 382 | 383 | override bool mouseEvent(MouseEvent){ 384 | // we dont do that here 385 | return false; 386 | } 387 | 388 | override bool keyboardEvent(KeyboardEvent, bool){ 389 | // we dont do that here either 390 | return false; 391 | } 392 | 393 | override bool updateEvent(){ 394 | uint vw = view.width, vh = view.height, 395 | vx = view.x, vy = view.y; 396 | foreach (y; vy .. vy + vh){ 397 | view.moveTo(vx, y); 398 | if (y >= _lines.length || _lines[y].length < vx){ 399 | view.fillLine(' ', _fg, _bg); 400 | continue; 401 | } 402 | dstring line = _lines[y][vx .. $]; 403 | view.write(line, _fg, _bg); 404 | if (line.length < vw) 405 | view.fillLine(' ', _fg, _bg); 406 | //cursorPos(-1, -1); 407 | } 408 | return true; 409 | } 410 | 411 | public: 412 | /// constructor 413 | this(dstring[] buffer = null){ 414 | _lines = buffer.dup; 415 | _size; 416 | } 417 | 418 | /// lines 419 | @property const(dstring[]) lines(){ 420 | return _lines; 421 | } 422 | /// ditto 423 | @property const(dstring[]) lines(dstring[] newVal){ 424 | _lines = newVal.dup; 425 | _size; 426 | resize; 427 | update; 428 | return _lines; 429 | } 430 | 431 | /// text color 432 | @property Color textColor(){ 433 | return _fg; 434 | } 435 | /// ditto 436 | @property Color textColor(Color newVal){ 437 | return _fg = newVal; 438 | } 439 | 440 | /// background color 441 | @property Color backColor(){ 442 | return _bg; 443 | } 444 | /// ditto 445 | @property Color backColor(Color newVal){ 446 | return _bg = newVal; 447 | } 448 | } 449 | 450 | /// Displays an un-scrollable log 451 | class LogWidget : QWidget{ 452 | private: 453 | /// stores the logs 454 | dstring[] _logs; 455 | /// The index in _logs where the oldest added line is 456 | uint _startIndex; 457 | /// the maximum number of logs to store 458 | uint _maxLogs; 459 | /// background and text color 460 | Color _bg, _fg; 461 | 462 | /// Returns: log at an index 463 | dstring log(uint index){ 464 | if (index >= _logs.length) 465 | return ""; 466 | return _logs[(index + _startIndex) % _maxLogs]; 467 | } 468 | 469 | /// wrap a line, and modify str to exclude the first row 470 | /// Returns: the first row in wrapped lines 471 | dstring wrapLine(ref dstring str){ 472 | dstring ret; 473 | if (str.length > width){ 474 | ret = str[0 .. width]; 475 | str = str[width + 1 .. $]; 476 | }else{ 477 | ret = str; 478 | str = null; 479 | } 480 | return ret; 481 | } 482 | 483 | /// Returns: height of a log (due to wrapping) 484 | uint logHeight(dstring str){ 485 | if (!width) 486 | return 0; 487 | return cast(uint)(str.length + width - 1) / width; 488 | } 489 | protected: 490 | override bool updateEvent(){ 491 | if (!view.height || !view.width) 492 | return false; 493 | int y = view.height; 494 | for (int i = cast(int)_logs.length - 1; i >= 0 && y >= 0; i --){ 495 | dstring line = _logs[i]; 496 | immutable uint logHeight = logHeight(line); 497 | foreach (wrapI; y - logHeight .. y){ 498 | dstring wrapped = wrapLine(line); 499 | if (wrapI < 0) 500 | continue; 501 | view.moveTo(0, wrapI); 502 | view.write(wrapped, _fg, _bg); 503 | view.fillLine(' ', textColor, backColor); 504 | } 505 | y -= logHeight; 506 | } 507 | foreach (i; 0 .. y){ 508 | view.moveTo(0, i); 509 | view.fillLine(' ', _fg, _bg); 510 | } 511 | return true; 512 | } 513 | 514 | override bool resizeEvent(){ 515 | update; 516 | return true; 517 | } 518 | 519 | override bool scrollEvent(){ 520 | update; 521 | return true; 522 | } 523 | public: 524 | /// constructor 525 | this(uint maxLogs = 100){ 526 | _maxLogs = maxLogs; 527 | _startIndex = 0; 528 | _fg = Color.Default; 529 | _bg = Color.Default; 530 | } 531 | ~this(){ 532 | .destroy(_logs); 533 | } 534 | 535 | /// adds string to the log 536 | void add(dstring item){ 537 | if (_logs.length > _maxLogs){ 538 | _startIndex = (_startIndex + 1) % _maxLogs; 539 | _logs[_startIndex] = item; 540 | }else{ 541 | _logs ~= item; 542 | } 543 | update; 544 | } 545 | /// ditto 546 | void add(string item){ 547 | add(item.to!dstring); 548 | } 549 | 550 | /// clears the log 551 | void clear(){ 552 | _logs.length = 0; 553 | _startIndex = 0; 554 | update; 555 | } 556 | 557 | /// text color 558 | @property Color textColor(){ 559 | return _fg; 560 | } 561 | /// ditto 562 | @property Color textColor(Color newColor){ 563 | _fg = newColor; 564 | update; 565 | return _fg; 566 | } 567 | /// background color 568 | @property Color backColor(){ 569 | return _bg; 570 | } 571 | /// ditto 572 | @property Color backColor(Color newColor){ 573 | _bg = newColor; 574 | update; 575 | return _bg; 576 | } 577 | } 578 | 579 | /// Just occupies some space. Use this to put space between widgets. 580 | /// Allows you to set the color filled by its space 581 | class SpacerWidget : QWidget{ 582 | private: 583 | Color _color; 584 | 585 | protected: 586 | override bool resizeEvent(){ 587 | update; 588 | return true; 589 | } 590 | 591 | override bool scrollEvent(){ 592 | update; 593 | return true; 594 | } 595 | 596 | override bool updateEvent(){ 597 | foreach (y; view.y .. view.y + view.height){ 598 | view.moveTo(view.x, y); 599 | view.fillLine(' ', Color.Default, _color); 600 | } 601 | return true; 602 | } 603 | 604 | public: 605 | /// constructor 606 | this(){ 607 | _color = Color.Default; 608 | } 609 | 610 | /// color 611 | @property Color color(){ 612 | return _color; 613 | } 614 | /// ditto 615 | @property Color color(Color newColor){ 616 | _color = newColor; 617 | update; 618 | return _color; 619 | } 620 | } 621 | -------------------------------------------------------------------------------- /source/qui/qui.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Contains all the classes that make up qui. 3 | +/ 4 | module qui.qui; 5 | 6 | import std.datetime.stopwatch, 7 | std.conv, 8 | std.process, 9 | std.algorithm; 10 | 11 | import qui.termwrap; 12 | 13 | /// default active widget cycling key, tab 14 | enum dchar WIDGET_CYCLE_KEY = '\t'; 15 | 16 | /// Colors 17 | alias Color = qui.termwrap.Color; 18 | /// Availabe Keys (keyboard) for input 19 | alias Key = qui.termwrap.Event.Keyboard.Key; 20 | /// Mouse Event 21 | alias MouseEvent = Event.Mouse; 22 | /// Keyboard Event 23 | alias KeyboardEvent = Event.Keyboard; 24 | 25 | /// MouseEvent function. 26 | alias MouseEventFuction = void delegate(QWidget, MouseEvent); 27 | /// KeyboardEvent function. 28 | alias KeyboardEventFunction = void delegate(QWidget, KeyboardEvent, bool); 29 | /// ResizeEvent function. 30 | alias ResizeEventFunction = void delegate(QWidget); 31 | /// ScrollEvent function. 32 | alias ScrollEventFunction = void delegate(QWidget); 33 | /// ActivateEvent function. 34 | alias ActivateEventFunction = void delegate(QWidget, bool); 35 | /// TimerEvent function. 36 | alias TimerEventFunction = void delegate(QWidget, uint); 37 | /// AdoptEvent function. 38 | alias AdoptEventFunction = void delegate(QWidget, bool); 39 | /// UpdateEvent function. 40 | alias UpdateEventFunction = void delegate(QWidget); 41 | 42 | /// Display buffer 43 | struct Viewport{ 44 | private: 45 | struct Cell{ 46 | /// character 47 | dchar c; 48 | /// foreground color 49 | Color fg; 50 | /// background colors 51 | Color bg; 52 | /// if colors are same with another Cell 53 | bool colorsSame(Cell b){ 54 | return this.fg == b.fg && this.bg == b.fg; 55 | } 56 | } 57 | 58 | Cell[][] _buffer; 59 | /// where cursor is right now (offsets applied) 60 | uint _seekX, _seekY; 61 | /// these are subtracted, only when seek is set, not after 62 | uint _offX, _offY; 63 | 64 | /// reset 65 | void _reset(){ 66 | _buffer = null; 67 | _seekX = _seekY = _offX = _offY = 0; 68 | } 69 | 70 | /// increments seekX, if overflowing, increments seekY 71 | void _incSeekX(uint inc = 1){ 72 | _seekX += inc; 73 | if (width && _seekX >= width){ 74 | _seekY += _seekX / width; 75 | _seekX %= width; 76 | } 77 | } 78 | 79 | /// Resizes 80 | void _resize(uint w, uint h){ 81 | _buffer.length = h; 82 | foreach (ref row; _buffer) 83 | row.length = w; 84 | } 85 | 86 | /// set another Viewport so that it is a rectangular slice of this 87 | void _getSlice(ref Viewport sub, uint x, uint y, uint width, uint height, 88 | uint offX = 0, uint offY = 0){ 89 | sub._reset; 90 | // if zero size, or sub completely outside viewport, do nothing 91 | if (width == 0 || height == 0 || x + width < _offX || y + height < _offY || 92 | x > this.width + _offX || y > this.height + _offY) 93 | return; 94 | 95 | // apply offsets to x and y 96 | if (x < _offX){ 97 | sub._offX = _offX - x; 98 | width -= sub._offX; 99 | x = 0; 100 | }else{ 101 | x -= _offX; 102 | } 103 | if (y < _offY){ 104 | sub._offY = _offY - y; 105 | height -= sub._offY; 106 | y = 0; 107 | }else{ 108 | y -= _offY; 109 | } 110 | 111 | // apply offX/Y to sub 112 | sub._offX += offX; 113 | sub._offY += offY; 114 | 115 | // if width or height overflowing, reduce them to fit 116 | if (width + x > this.width) 117 | width = this.width - x; 118 | if (height + y > this.height) 119 | height = this.height - y; 120 | 121 | // now set buffer 122 | sub._buffer.length = height; 123 | foreach (i; 0 .. height) 124 | sub._buffer[i] = _buffer[i + y][x .. x + width]; 125 | } 126 | 127 | public: 128 | /// width 129 | @property uint width(){ 130 | return cast(uint)(_buffer.length ? _buffer[0].length : 0); 131 | } 132 | /// height 133 | @property uint height() const { 134 | return cast(uint)_buffer.length; 135 | } 136 | /// x coordinate of viewport (offset X) 137 | @property uint x() const { 138 | return _offX; 139 | } 140 | /// y coordinate of viewport (offset Y) 141 | @property uint y() const { 142 | return _offY; 143 | } 144 | 145 | /// Returns: true if x and y are at a position where writing can happen 146 | bool isWritable(uint x, uint y){ 147 | return x >= _offX && y >= _offY && 148 | x < width + _offX && y < height + _offY; 149 | } 150 | 151 | /// move to a position. if x > width, moved to x=0 of next row 152 | /// 153 | /// Returns: true if done, false if outside writing area 154 | bool moveTo(uint x, uint y){ 155 | if (!isWritable(x, y)) 156 | return false; 157 | _seekX = x - _offX; 158 | _seekY = y - _offY; 159 | _incSeekX(0); // increment by 0, to fix overflow. TODO is this necessary? 160 | return true; 161 | } 162 | 163 | /// Writes a character at current position and move ahead 164 | /// 165 | /// Returns: false if outside writing area 166 | bool write(dchar c, Color fg = Color.Default, Color bg = Color.Default){ 167 | if (_seekX < width && _seekY < height) 168 | _buffer[_seekY][_seekX] = Cell(c, fg, bg); 169 | _incSeekX; 170 | return true; 171 | } 172 | 173 | /// Writes a string. 174 | /// 175 | /// Returns: number of characters written 176 | uint write(dstring s, Color fg = Color.Default, Color bg = Color.Default){ 177 | foreach (i, c; s){ 178 | if (!write(c, fg, bg)) 179 | return cast(uint)i; 180 | } 181 | return cast(uint)s.length; 182 | } 183 | 184 | /// Fills line, starting from current coordinates, 185 | /// with maximum `max` number of chars, if `max>0` 186 | /// 187 | /// Returns: number of characters written, or 0 if outside bounds 188 | uint fillLine(dchar c = ' ', Color fg = Color.Default, 189 | Color bg = Color.Default, uint max = 0){ 190 | if (_seekX >= width || _seekY >= height) 191 | return 0; 192 | // find how many cells to fill 193 | uint r = width - _seekX; 194 | if (max) 195 | r = min(max, r); 196 | // fill them 197 | _buffer[_seekY][_seekX .. _seekX + r] = Cell(c, fg, bg); 198 | // increment seek 199 | _incSeekX(r); 200 | return r; 201 | } 202 | } 203 | 204 | /// Base class for all widgets, including layouts and QTerminal 205 | /// 206 | /// Use this as parent-class for new widgets 207 | abstract class QWidget{ 208 | private: 209 | /// position of this widget, relative to parent 210 | uint _posX, _posY; 211 | /// width of widget 212 | uint _width; 213 | /// height of widget 214 | uint _height; 215 | /// if this widget is requesting update 216 | bool _requestingUpdate = true; 217 | /// the parent widget 218 | QParent _parent; 219 | /// cursor X/Y 220 | int _cursorX = -1, _cursorY = -1; 221 | 222 | /// custom adopt event 223 | AdoptEventFunction _customAdoptEvent; 224 | /// custom mouse event 225 | MouseEventFuction _customMouseEvent; 226 | /// custom keyboard event 227 | KeyboardEventFunction _customKeyboardEvent; 228 | /// custom resize event 229 | ResizeEventFunction _customResizeEvent; 230 | /// custom rescroll event 231 | ScrollEventFunction _customScrollEvent; 232 | /// custom onActivate event, 233 | ActivateEventFunction _customActivateEvent; 234 | /// custom onTimer event 235 | TimerEventFunction _customTimerEvent; 236 | /// custom upedateEvent 237 | UpdateEventFunction _customUpdateEvent; 238 | 239 | protected: 240 | /// minimum width 241 | uint _minWidth; 242 | /// maximum width 243 | uint _maxWidth; 244 | /// minimum height 245 | uint _minHeight; 246 | /// maximum height 247 | uint _maxHeight; 248 | /// viewport 249 | Viewport view; 250 | 251 | /// activate the passed widget if this is the correct widget 252 | /// 253 | /// Returns: if it was activated or not 254 | bool widgetActivate(QWidget target){ 255 | return this == target && _parent && _parent.widgetIsActive(this); 256 | } 257 | 258 | /// Sets cursor position. Only takes affect during update 259 | final void cursorPos(int x, int y){ 260 | if (x < 0 || y < 0){ 261 | _cursorX = _cursorY = -1; 262 | return; 263 | } 264 | _cursorX = _posX + x - view.x; 265 | _cursorY = _posY + y - view.y; 266 | } 267 | 268 | /// request parent to adjust horizontal scroll so cell at column x is visible 269 | /// Returns: true if done, false if not 270 | final bool scrollToX(int x){ 271 | if (!_parent || x < 0 || x > _width) 272 | return false; 273 | return _parent.requestScrollToX(this, _posX + x); 274 | } 275 | 276 | /// request parent to adjust vertical scroll so cell at row y is visible 277 | /// Returns: true if done, false if not 278 | final bool scrollToY(int y){ 279 | if (!_parent || y < 0 || y > _height) 280 | return false; 281 | return _parent.requestScrollToY(this, _posY + y); 282 | } 283 | 284 | /// trigger update event 285 | /// Returns: true if parent acknowledged, false if not 286 | final bool update(){ 287 | if (!_parent) 288 | return false; 289 | if (_requestingUpdate) 290 | return true; 291 | return _requestingUpdate = _parent.requestUpdate(this); 292 | } 293 | 294 | /// Requests parent widget to resize it **Do not call this in a resizeEvent** 295 | /// Returns: true if parent acknowledged, false if not 296 | final bool resize(){ 297 | if (!_parent) 298 | return false; 299 | return _parent.requestResize(this); 300 | } 301 | 302 | /// Called when this widget is adopted by a parent, or disowned. 303 | /// The `adopted` flag is true when adopted, false when disowned 304 | bool adoptEvent(bool adopted){ 305 | return false; 306 | } 307 | 308 | /// Called when mouse is clicked with cursor on this widget. 309 | bool mouseEvent(MouseEvent mouse){ 310 | return false; 311 | } 312 | 313 | /// Called when key is pressed and this widget is active. 314 | /// `cycle` indicates if widget cycling should happen 315 | bool keyboardEvent(KeyboardEvent key, bool cycle){ 316 | return false; 317 | } 318 | 319 | /// Called when widget size is changed, 320 | bool resizeEvent(){ 321 | return false; 322 | } 323 | 324 | /// Called when the widget is rescrolled, ~but size not changed.~ 325 | bool scrollEvent(){ 326 | return false; 327 | } 328 | 329 | /// called right after this widget is activated, or de-activated 330 | bool activateEvent(bool isActive){ 331 | return false; 332 | } 333 | 334 | /// called `msecs` milliseconds after the previous timerEvent was called 335 | bool timerEvent(uint msecs){ 336 | return false; 337 | } 338 | 339 | /// called when this widget should re-draw itself 340 | bool updateEvent(){ 341 | return false; 342 | } 343 | 344 | public: 345 | /// custom initialize event 346 | final @property AdoptEventFunction onAdoptEvent(AdoptEventFunction func){ 347 | return _customAdoptEvent = func; 348 | } 349 | 350 | /// custom mouse event 351 | final @property MouseEventFuction onMouseEvent(MouseEventFuction func){ 352 | return _customMouseEvent = func; 353 | } 354 | 355 | /// custom keyboard event 356 | final @property KeyboardEventFunction onKeyboardEvent( 357 | KeyboardEventFunction func){ 358 | return _customKeyboardEvent = func; 359 | } 360 | 361 | /// custom resize event 362 | final @property ResizeEventFunction onResizeEvent(ResizeEventFunction func){ 363 | return _customResizeEvent = func; 364 | } 365 | 366 | /// custom scroll event 367 | final @property ScrollEventFunction onScrollEvent(ScrollEventFunction func){ 368 | return _customScrollEvent = func; 369 | } 370 | 371 | /// custom activate event 372 | final @property ActivateEventFunction onActivateEvent( 373 | ActivateEventFunction func){ 374 | return _customActivateEvent = func; 375 | } 376 | 377 | /// custom timer event 378 | final @property TimerEventFunction onTimerEvent(TimerEventFunction func){ 379 | return _customTimerEvent = func; 380 | } 381 | 382 | /// custom update event 383 | final @property UpdateEventFunction onUpdateEvent(UpdateEventFunction func){ 384 | return _customUpdateEvent = func; 385 | } 386 | 387 | /// width of widget 388 | final @property uint width() const { 389 | return _width; 390 | } 391 | 392 | /// height of widget 393 | final @property uint height() const { 394 | return _height; 395 | } 396 | 397 | /// if this widget's height has constraints 398 | final @property bool heightConstrained(){ 399 | return (minHeight || maxHeight) && 400 | (!(minHeight && maxHeight) || maxHeight >= minHeight); 401 | } 402 | 403 | /// if this widget's width has constraints 404 | final @property bool widthConstrained(){ 405 | return (minWidth || maxWidth) && 406 | (!(minWidth && maxWidth) || maxWidth >= minWidth); 407 | } 408 | 409 | /// if this widget's size is constrained 410 | final @property bool sizeConstrained(){ 411 | return heightConstrained || widthConstrained; 412 | } 413 | 414 | /// if this widget wants focus. 415 | /// This should return true if the widget wants keyboard input 416 | @property bool wantsFocus() const { 417 | return false; 418 | } 419 | 420 | /// Sets min/max width constraints. 421 | /// Returns: true if done, false if invalid and not done 422 | bool widthConstraint(uint min = 0, uint max = 0){ 423 | if (min && max && max < min) 424 | return false; 425 | _minWidth = min; 426 | _maxWidth = max; 427 | resize; 428 | return true; 429 | } 430 | 431 | /// Sets min/max height constraints 432 | /// Returns: true if done, false if invalid and not done 433 | bool heightConstraint(uint min = 0, uint max = 0){ 434 | if (min && max && max < min) 435 | return false; 436 | _minHeight = min; 437 | _maxHeight = max; 438 | resize; 439 | return true; 440 | } 441 | 442 | /// Sets min/max width/height constraints. 443 | /// Returns: true if done, false if invalid and not done 444 | final bool sizeConstraint(uint minWidth = 0, uint maxWidth = 0, 445 | uint minHeight = 0, uint maxHeight = 0){ 446 | return widthConstraint(minWidth, maxWidth) | 447 | heightConstraint(minHeight, maxHeight); 448 | } 449 | 450 | /// minimum width 451 | @property uint minWidth(){ 452 | return _minWidth; 453 | } 454 | 455 | /// minimum height 456 | @property uint minHeight(){ 457 | return _minHeight; 458 | } 459 | 460 | /// maximum width 461 | @property uint maxWidth(){ 462 | return _maxWidth; 463 | } 464 | 465 | /// maximum height 466 | @property uint maxHeight(){ 467 | return _maxHeight; 468 | } 469 | } 470 | 471 | /// Base class for parent widgets 472 | abstract class QParent : QWidget{ 473 | protected: 474 | /// Called when this widget was made to disown a child 475 | void disownEvent(QWidget widget){} 476 | 477 | /// when widget is requesting to adjust horizontal scroll so the column at 478 | /// x is visible. 479 | /// Returns: true if done, false if not 480 | bool requestScrollToX(QWidget widget, int x){ 481 | if (!widgetIsActive(null) && !widgetIsActive(widget)) 482 | return false; 483 | return scrollToX(x); 484 | } 485 | 486 | /// when widget is requesting to adjust vertical scroll so the row at 487 | /// y is visible. 488 | /// Returns: true if done, false if not 489 | bool requestScrollToY(QWidget widget, int y){ 490 | if (!widgetIsActive(null) && !widgetIsActive(widget)) 491 | return false; 492 | return scrollToY(y); 493 | } 494 | 495 | /// when widget is requesting update event 496 | /// Returns: true if request accepted, false if not 497 | bool requestUpdate(QWidget widget){ 498 | return update; 499 | } 500 | 501 | /// when widget is requesting to be resized 502 | /// Returns: true if accepted, false if not 503 | bool requestResize(QWidget widget){ 504 | return resize; 505 | } 506 | 507 | /// positions child widget on X axis 508 | final void widgetPositionX(QWidget child, uint x){ 509 | if (!child || child._parent != this) 510 | return; 511 | child._posX = x; 512 | } 513 | 514 | /// positions child widget on y axis 515 | final void widgetPositionY(QWidget child, uint y){ 516 | if (!child || child._parent != this) 517 | return; 518 | child._posY = y; 519 | } 520 | 521 | /// positions child widget on X and Y coordinates 522 | final void widgetPosition(QWidget child, uint x, uint y){ 523 | if (!child || child._parent != this) 524 | return; 525 | child._posX = x; 526 | child._posY = y; 527 | } 528 | 529 | /// Assigns widget a viewport based on its position and size 530 | /// if width is 0, `widget.width` is used 531 | /// if height is 0, `widget.height` is used 532 | final void widgetViewportAssign(QWidget child, 533 | uint width = 0, uint height = 0, 534 | uint scrollX = 0, uint scrollY = 0){ 535 | if (!child || child._parent != this) 536 | return; 537 | if (!width) 538 | width = child.width; 539 | if (!height) 540 | height = child.height; 541 | view._getSlice(child.view, child._posX, child._posY, 542 | width, height, scrollX, scrollY); 543 | } 544 | 545 | /// sets child widget's width 546 | /// Returns: true if width applied as it is, false if constrained 547 | final bool widgetSizeWidth(QWidget child, uint width){ 548 | if (!child || child._parent != this) 549 | return false; 550 | child._width = width; 551 | if (!child.widthConstrained) 552 | return true; 553 | if (child.minWidth && child._width < child.minWidth) 554 | child._width = child.minWidth; 555 | if (child.maxWidth && child._width > child.maxWidth) 556 | child._width = child.maxWidth; 557 | return width == child._width; 558 | } 559 | 560 | /// sets child widget's height 561 | /// Returns: true if height applied as it is, false if constrained 562 | final bool widgetSizeHeight(QWidget child, uint height){ 563 | if (!child || child._parent != this) 564 | return false; 565 | child._height = height; 566 | if (!child.heightConstrained) 567 | return true; 568 | if (child.minHeight && child._height < child.minHeight) 569 | child._height = child.minHeight; 570 | if (child.maxHeight && child._height > child.maxHeight) 571 | child._height = child.maxHeight; 572 | return height == child._height; 573 | } 574 | 575 | /// sets child widget's width and height 576 | /// Returns: true if sizes applied as it is, false if constrained 577 | final bool widgetSize(QWidget child, uint width, uint height){ 578 | return widgetSizeWidth(child, width) & widgetSizeHeight(child, height); 579 | } 580 | 581 | /// Adopt another widget (i.e become its parent) 582 | /// If the widget already has a parent, the current parent will disown it 583 | /// i.e: current parent will receive a `disownEvent` 584 | final void adopt(QWidget widget){ 585 | if (!widget) 586 | return; 587 | if (widget._parent) 588 | widget._parent.disown(widget); 589 | widget._parent = this; 590 | widget.view._reset; 591 | adoptEventCall(widget, true); 592 | } 593 | 594 | /// Disown a widget. Doing so will trigger it's own disownEvent 595 | /// Returns: true if done, false if not (probably due to it not being child) 596 | final bool disown(QWidget widget){ 597 | if (!widget || widget._parent != this) 598 | return false; 599 | disownEvent(widget); 600 | widget.view._reset; 601 | widget._parent = null; 602 | adoptEventCall(widget, false); 603 | return true; 604 | } 605 | 606 | /// Calls adoptEvent on child 607 | final bool adoptEventCall(QWidget child, bool adopted){ 608 | if (!child || child._parent != this) 609 | return false; 610 | if (child._customAdoptEvent) 611 | child._customAdoptEvent(child, adopted); 612 | return child.adoptEvent(adopted); 613 | } 614 | 615 | /// Calls mouseEvent on child 616 | final bool mouseEventCall(QWidget child, MouseEvent mouse){ 617 | if (!child || child._parent != this) 618 | return false; 619 | mouse.x = (mouse.x - cast(int)child._posX) + child.view.x; 620 | mouse.y = (mouse.y - cast(int)child._posY) + child.view.y; 621 | if (child._customMouseEvent) 622 | child._customMouseEvent(child, mouse); 623 | return child.mouseEvent(mouse); 624 | } 625 | 626 | /// Calls keyboardEvent on child 627 | final bool keyboardEventCall(QWidget child, KeyboardEvent key, bool cycle){ 628 | if (!child || child._parent != this) 629 | return false; 630 | if (child._customKeyboardEvent) 631 | child._customKeyboardEvent(child, key, cycle); 632 | return child.keyboardEvent(key, cycle); 633 | } 634 | 635 | /// Calls resizeEvent on child 636 | final bool resizeEventCall(QWidget child){ 637 | if (!child || child._parent != this) 638 | return false; 639 | if (child._customResizeEvent) 640 | child._customResizeEvent(child); 641 | return child.resizeEvent; 642 | } 643 | 644 | /// Calls scrollEvent on child 645 | final bool scrollEventCall(QWidget child){ 646 | if (!child || child._parent != this) 647 | return false; 648 | if (child._customScrollEvent) 649 | child._customScrollEvent(child); 650 | return child.scrollEvent; 651 | } 652 | 653 | /// Calls activateEvent on child 654 | final bool activateEventCall(QWidget child, bool isActive){ 655 | if (!child || child._parent != this) 656 | return false; 657 | if (child._customActivateEvent) 658 | child._customActivateEvent(child, isActive); 659 | bool ret = child.activateEvent(isActive); 660 | if (widgetIsActive(child)){ 661 | cursorPos(child._cursorX, child._cursorY); 662 | update; // HACK: to fix cursor position in case no widget actually want 663 | // to update 664 | } 665 | return ret; 666 | } 667 | 668 | /// Calls timerEvent on child 669 | final bool timerEventCall(QWidget child, uint msecs){ 670 | if (!child || child._parent != this) 671 | return false; 672 | if (child._customTimerEvent) 673 | child._customTimerEvent(child, msecs); 674 | return child.timerEvent(msecs); 675 | } 676 | 677 | /// Calls updateEvent on child 678 | final bool updateEventCall(QWidget child){ 679 | if (!child || child._parent != this) 680 | return false; 681 | bool ret = false; 682 | if (child._requestingUpdate){ 683 | child._requestingUpdate = false; 684 | if (child._customUpdateEvent) 685 | child._customUpdateEvent(child); 686 | ret = child.updateEvent; 687 | } 688 | if (widgetIsActive(child)) 689 | cursorPos(child._cursorX, child._cursorY); 690 | return ret; 691 | } 692 | 693 | public: 694 | /// Returns: true if the widget is the current active widget 695 | /// if no widget is active, then it should return true for `null` input 696 | abstract bool widgetIsActive(QWidget); 697 | } 698 | 699 | /// Layout type 700 | enum QLayoutType{ 701 | Horizontal, 702 | Vertical, 703 | } 704 | 705 | /// Positions widgets in a Vertical or Horizontal layout 706 | /// Attempts to best mimic size properties of widgets inside it. 707 | class QLayout(QLayoutType type) : QParent{ 708 | private: 709 | /// Sets sizes for a widget 710 | /// Returns: true if "natural" size was used, false if size was constrained 711 | bool _widgetSizeByRatio(QWidget widget, uint sizeTotal, uint count){ 712 | assert (count != 0); 713 | static if (type == Type.Horizontal){ 714 | return widgetSizeWidth(widget, sizeTotal / count); 715 | }else static if (type == Type.Vertical){ 716 | return widgetSizeHeight(widget, sizeTotal / count); 717 | } 718 | } 719 | 720 | /// Resets sizes caches 721 | void _sizeCacheReset(){ 722 | // uint.max indicates yet to be calculated 723 | _maxWidth = _maxHeight = _minWidth = _minHeight = uint.max; 724 | } 725 | 726 | protected: 727 | /// widgets 728 | QWidget[] widgets; 729 | /// active widget index. `>=widgets.length` when no widgets 730 | uint activeWidgetIndex; 731 | 732 | /// recalculates all widgets' sizes 733 | void widgetsSizeRecalculate(){ 734 | QWidget[] queue = widgets.dup; 735 | uint sizeTotal = height; 736 | static if (type == Type.Horizontal) 737 | sizeTotal = width; 738 | uint count = cast(uint)queue.length, size = sizeTotal; 739 | for (int i = 0; i < queue.length; i ++){ 740 | QWidget widget = queue[i]; 741 | bool natural = _widgetSizeByRatio(widget, size, count); 742 | uint sizeUsed; 743 | static if (type == Type.Horizontal){ 744 | widgetSizeHeight(widget, height); 745 | sizeUsed = widget.width; 746 | }else static if (type == Type.Vertical){ 747 | widgetSizeWidth(widget, width); 748 | sizeUsed = widget.height; 749 | } 750 | if (!natural){ 751 | // start over again, exclude this widget 752 | if (queue.length == 1) 753 | break; 754 | queue[i] = queue[$ - 1]; 755 | queue.length --; 756 | sizeTotal -= sizeUsed; 757 | count = cast(uint)queue.length; 758 | size = sizeTotal; 759 | i = -1; 760 | continue; 761 | } 762 | count --; 763 | size -= sizeUsed; 764 | } 765 | } 766 | 767 | /// Positions widgets in the widgets array 768 | void widgetsReposition(){ 769 | uint pos = 0; 770 | foreach (widget; widgets){ 771 | static if (type == Type.Horizontal){ 772 | widgetPosition(widget, pos, 0); 773 | pos += widget.width; 774 | }else static if (type == Type.Vertical){ 775 | widgetPosition(widget, 0, pos); 776 | pos += widget.height; 777 | } 778 | widgetViewportAssign(widget); 779 | } 780 | } 781 | 782 | /// Gets widget at x, y coorinates 783 | /// Returns: widget index in widgets, or >= widgets.length if none 784 | uint widgetAt(uint x, uint y){ 785 | uint ret = uint.max; 786 | foreach (i, widget; widgets[1 .. $]){ 787 | static if (type == Type.Horizontal){ 788 | if (widget._posX > x) 789 | ret = cast(uint)i; 790 | }else static if (type == Type.Vertical){ 791 | if (widget._posY > y) 792 | ret = cast(uint)i; 793 | } 794 | } 795 | if (ret < widgets.length){ 796 | static if (type == Type.Horizontal){ 797 | if (y < widgets[ret].height) 798 | return ret; 799 | }else static if (type == Type.Vertical){ 800 | if (x < widgets[ret].width) 801 | return ret; 802 | } 803 | } 804 | return uint.max; 805 | } 806 | 807 | /// Gets next candidate for focus 808 | /// Returns: index of widget, or >= widgets.length if none 809 | uint activeWidgetNext(){ 810 | uint index = activeWidgetIndex + 1; 811 | if (index >= widgets.length) 812 | index = 0; 813 | for (; index < widgets.length; index ++){ 814 | if (widgets[index].wantsFocus) 815 | return index; 816 | } 817 | return uint.max; 818 | } 819 | 820 | /// Activates a widget, by index. taking care of calling activate events 821 | void widgetActivate(uint index){ 822 | if (activeWidgetIndex == index) 823 | return; 824 | if (activeWidgetIndex < widgets.length) 825 | activateEventCall(widgets[activeWidgetIndex], false); 826 | if (index < widgets.length && widgets[index].wantsFocus){ 827 | activeWidgetIndex = index; 828 | activateEventCall(widgets[activeWidgetIndex], true); 829 | return; 830 | } 831 | activeWidgetIndex = uint.max; 832 | } 833 | 834 | override bool widgetActivate(QWidget target){ 835 | foreach (i, widget; widgets){ 836 | if (widget.wantsFocus && widget.widgetActivate(target)){ 837 | widgetActivate(cast(uint)i); 838 | return true; 839 | } 840 | } 841 | return false; 842 | } 843 | 844 | override void disownEvent(QWidget widget){ 845 | int index = cast(int)widgets.countUntil(widget); 846 | if (index < 0) 847 | return; 848 | widgets[index .. $ - 1] = widgets[index + 1 .. $]; 849 | widgets.length --; 850 | _sizeCacheReset; 851 | resize; 852 | update; 853 | } 854 | 855 | override bool adoptEvent(bool adopted){ 856 | if (!adopted) 857 | return true; 858 | _sizeCacheReset; 859 | resize; 860 | update; 861 | return true; 862 | } 863 | 864 | override bool mouseEvent(MouseEvent mouse){ 865 | uint index = widgetAt(mouse.x, mouse.y); 866 | if (index >= widgets.length) 867 | return false; 868 | widgetActivate(index); 869 | return mouseEventCall(widgets[index], mouse); 870 | } 871 | 872 | override bool keyboardEvent(KeyboardEvent key, bool cycle){ 873 | if (!widgets.length) 874 | return false; 875 | if (activeWidgetIndex < widgets.length && 876 | keyboardEventCall(widgets[activeWidgetIndex], key, cycle)) 877 | return true; 878 | if (!cycle) 879 | return false; 880 | widgetActivate(activeWidgetNext); 881 | return activeWidgetIndex < widgets.length; 882 | } 883 | 884 | override bool resizeEvent(){ 885 | _sizeCacheReset; 886 | // reposition stuff, and assign them views 887 | widgetsSizeRecalculate; 888 | widgetsReposition; 889 | foreach (widget; widgets) 890 | resizeEventCall(widget); 891 | return true; 892 | } 893 | 894 | override bool scrollEvent(){ 895 | foreach (widget; widgets) 896 | widgetViewportAssign(widget); 897 | foreach (widget; widgets) 898 | scrollEventCall(widget); 899 | return true; 900 | } 901 | 902 | override bool activateEvent(bool isActive){ 903 | if (!widgets.length) 904 | return false; 905 | if (!isActive){ 906 | if (activeWidgetIndex < widgets.length) 907 | activateEventCall(widgets[activeWidgetIndex], false); 908 | return true; 909 | } 910 | if (activeWidgetIndex >= widgets.length) 911 | activeWidgetIndex = activeWidgetNext; 912 | widgetActivate(activeWidgetIndex); 913 | return true; 914 | } 915 | 916 | override bool timerEvent(uint msecs){ 917 | foreach (widget; widgets) 918 | timerEventCall(widget, msecs); 919 | return true; 920 | } 921 | 922 | override bool updateEvent(){ 923 | foreach(widget; widgets) 924 | updateEventCall(widget); 925 | return true; 926 | } 927 | 928 | public: 929 | /// Layout types 930 | alias Type = QLayoutType; 931 | /// constructor 932 | this(){ 933 | _sizeCacheReset; 934 | } 935 | 936 | override bool widgetIsActive(QWidget widget){ 937 | return (widget is null && activeWidgetIndex >= widgets.length) || 938 | (activeWidgetIndex < widgets.length && 939 | widgets[activeWidgetIndex] == widget); 940 | } 941 | 942 | /// Adds a widget at end 943 | /// If the widget already has a parent, they will disown it 944 | void widgetAdd(QWidget widget){ 945 | if (!widget) 946 | return; 947 | adopt(widget); 948 | widgets ~= widget; 949 | resize; 950 | } 951 | 952 | /// Removes a widget 953 | /// Returns: true if done, false if failed (probably due to it not existing) 954 | bool widgetRemove(QWidget widget){ 955 | if (widgets.countUntil(widget) < 0 || !disown(widget)) 956 | return false; 957 | resize; 958 | return true; 959 | } 960 | 961 | override @property bool wantsFocus() const { 962 | foreach (widget; widgets){ 963 | if (widget.wantsFocus) 964 | return true; 965 | } 966 | return false; 967 | } 968 | 969 | override bool widthConstraint(uint = 0, uint = 0){ 970 | return false; 971 | } 972 | 973 | override bool heightConstraint(uint = 0, uint = 0){ 974 | return false; 975 | } 976 | 977 | override @property uint minWidth(){ 978 | if (_minWidth != uint.max) 979 | return _minWidth; 980 | _minWidth = 0; 981 | static if (type == Type.Horizontal){ 982 | foreach (widget; widgets) 983 | _minWidth += widget.widthConstrained * widget.minWidth; 984 | }else static if (type == Type.Vertical){ 985 | foreach (widget; widgets){ 986 | if (widget.widthConstrained) 987 | _minWidth = max(widget.minWidth, _minWidth); 988 | } 989 | } 990 | return _minWidth; 991 | } 992 | 993 | override @property uint minHeight(){ 994 | if (_minHeight != uint.max) 995 | return _minHeight; 996 | _minHeight = 0; 997 | static if (type == Type.Horizontal){ 998 | foreach (widget; widgets){ 999 | if (widget.heightConstrained) 1000 | _minHeight = max(widget.minHeight, _minHeight); 1001 | } 1002 | }else static if (type == Type.Vertical){ 1003 | foreach (widget; widgets) 1004 | _minHeight += widget.heightConstrained * widget.minHeight; 1005 | } 1006 | return _minHeight; 1007 | } 1008 | 1009 | override @property uint maxWidth(){ 1010 | if (_maxWidth != uint.max) 1011 | return _maxWidth; 1012 | _maxWidth = 0; 1013 | static if (type == Type.Horizontal){ 1014 | foreach (widget; widgets){ 1015 | if (widget.widthConstrained || widget.maxWidth == 0) 1016 | return _maxWidth = 0; 1017 | _maxWidth += widget.maxWidth; 1018 | } 1019 | }else static if (type == Type.Vertical){ 1020 | foreach (widget; widgets){ 1021 | if (widget.widthConstrained) 1022 | _maxWidth = max(widget.maxWidth, _maxWidth); 1023 | } 1024 | } 1025 | return _maxWidth; 1026 | } 1027 | 1028 | override @property uint maxHeight(){ 1029 | if (_maxHeight != uint.max) 1030 | return _maxHeight; 1031 | _maxHeight = 0; 1032 | static if (type == Type.Horizontal){ 1033 | foreach (widget; widgets){ 1034 | if (widget.heightConstrained) 1035 | _maxHeight = max(widget.maxHeight, _maxHeight); 1036 | } 1037 | }else static if (type == Type.Vertical){ 1038 | foreach (widget; widgets){ 1039 | if (!widget.heightConstrained || widget.maxHeight == 0) 1040 | return _maxHeight = 0; 1041 | _maxHeight += widget.maxHeight; 1042 | } 1043 | } 1044 | return _maxHeight; 1045 | } 1046 | } 1047 | 1048 | alias QHorizontalLayout = QLayout!(QLayoutType.Horizontal); 1049 | alias QVerticalLayout = QLayout!(QLayoutType.Vertical); 1050 | 1051 | /// A container for widgets 1052 | /// It will create virtual space, which can be scrolled, to fit larger widgets 1053 | /// in smaller spaces. 1054 | /// its maxHeight and maxWidth are equal to it's child's minimum sizes 1055 | class QContainer : QParent{ 1056 | private: 1057 | /// scroll offsets 1058 | uint _scrollX, _scrollY; 1059 | /// the widget being contained 1060 | QWidget _widget; 1061 | /// milliseconds since scrollbars became visible 1062 | uint _scrollbarMsecs; 1063 | /// milliseconds for which scrollbars become visible 1064 | uint _scrollbarVisibleForMsecs = 1000; 1065 | /// scrollbar colors 1066 | Color _scrollbarFg = Color.Default, _scrollbarBg = Color.Default; 1067 | 1068 | protected: 1069 | /// whether to scroll on Page Up/Down keys 1070 | bool _pgScroll = true; 1071 | /// whether to scroll on Mouse Wheel 1072 | bool _msScroll = true; 1073 | 1074 | /// Returns: [cells before bar, cells in bar] for drawing scrollbar 1075 | static uint[2] scrollbarSize(uint totalSize, uint visible, uint scrolled){ 1076 | if (totalSize == 0 || visible == 0) 1077 | return [0, 0]; 1078 | return [scrolled * visible / totalSize, 1079 | visible * visible / totalSize]; 1080 | } 1081 | 1082 | /// Returns: true if scrollbar is visible at the moment 1083 | @property bool scrollbarVisible(){ 1084 | return _widget && _scrollbarMsecs < _scrollbarVisibleForMsecs && 1085 | (_widget.height > height || _widget.width > width); 1086 | } 1087 | 1088 | /// draws vertical scrollbar 1089 | void scrollbarVertDraw(){ 1090 | uint[2] barSize = scrollbarSize(_widget.height, height, _scrollY); 1091 | barSize[1] += barSize[0]; 1092 | foreach (i; 0 .. height){ 1093 | dchar ch = ' '; 1094 | if (i >= barSize[0] && i < barSize[1]) 1095 | ch = '█'; 1096 | if (!view.moveTo(width - 1, i)) 1097 | break; 1098 | view.write(ch, _scrollbarFg, _scrollbarBg); 1099 | } 1100 | } 1101 | 1102 | /// draws horizontal scrollbar 1103 | void scrollbarHorzDraw(){ 1104 | uint[2] barSize = scrollbarSize(_widget.width, width, _scrollX); 1105 | barSize[1] += barSize[0]; 1106 | foreach (i; 0 .. width){ 1107 | dchar ch = ' '; 1108 | if (i >= barSize[0] && i < barSize[1]) 1109 | ch = '■'; 1110 | if (!view.moveTo(i, height - 1)) 1111 | break; 1112 | view.write(ch, _scrollbarFg, _scrollbarBg); 1113 | } 1114 | } 1115 | 1116 | override bool widgetActivate(QWidget target){ 1117 | if (!_widget) 1118 | return false; 1119 | return _widget.widgetActivate(target); 1120 | } 1121 | 1122 | override bool requestResize(QWidget widget){ 1123 | if (!_widget || widget != _widget) 1124 | return false; 1125 | resizeEvent; 1126 | return true; 1127 | } 1128 | 1129 | override bool requestUpdate(QWidget widget){ 1130 | if (!_widget || widget != _widget) 1131 | return false; 1132 | return update; 1133 | } 1134 | 1135 | override bool requestScrollToX(QWidget widget, int x){ 1136 | if (!_widget || widget != _widget || _widget.width <= view.width || x < 0) 1137 | return false; 1138 | if (x <= scrollX || x > scrollX + width) 1139 | scrollX = min(x, scrollXMax); 1140 | super.scrollToX(x); 1141 | return true; 1142 | } 1143 | 1144 | override bool requestScrollToY(QWidget widget, int y){ 1145 | if (!_widget || widget != _widget || _widget.height <= view.height || y < 0) 1146 | return false; 1147 | if (y < scrollY || y > scrollY + height) 1148 | scrollY = min(y, scrollYMax); 1149 | super.scrollToY(y); 1150 | return true; 1151 | } 1152 | 1153 | override void disownEvent(QWidget widget){ 1154 | if (widget != _widget) 1155 | return; 1156 | _widget = null; 1157 | _scrollX = _scrollY = 0; 1158 | } 1159 | 1160 | override bool mouseEvent(MouseEvent mouse){ 1161 | if (mouseEventCall(_widget, mouse)) 1162 | return true; 1163 | if (!_msScroll) 1164 | return false; 1165 | if (mouse.button == MouseEvent.Button.ScrollUp && _scrollY > 0){ 1166 | scrollY = scrollY - 1; 1167 | return true; 1168 | } 1169 | if (mouse.button == MouseEvent.Button.ScrollDown && _scrollY < scrollYMax){ 1170 | scrollY = scrollY + 1; 1171 | return true; 1172 | } 1173 | return false; 1174 | } 1175 | 1176 | override bool keyboardEvent(KeyboardEvent key, bool cycle){ 1177 | if (keyboardEventCall(_widget, key, cycle)) 1178 | return true; 1179 | if (!_pgScroll || cycle) 1180 | return false; 1181 | if (key.key == Key.PageUp && _scrollY > 0){ 1182 | scrollY = scrollY - min(scrollY, height); 1183 | return true; 1184 | } 1185 | if (key.key == Key.PageDown && _scrollY < scrollYMax){ 1186 | scrollY = scrollY + height; 1187 | return true; 1188 | } 1189 | return false; 1190 | } 1191 | 1192 | override bool resizeEvent(){ 1193 | // try to fix scrolling 1194 | scrollX = scrollX; 1195 | scrollY = scrollY; 1196 | widgetSize(_widget, width, height); 1197 | widgetViewportAssign(_widget, _widget.width, _widget.height, 1198 | _scrollX, _scrollY); // re-assign viewport 1199 | return resizeEventCall(_widget); 1200 | } 1201 | 1202 | override bool scrollEvent(){ 1203 | widgetViewportAssign(_widget, _widget.width, _widget.height, 1204 | _scrollX, _scrollY); // re-assign viewport 1205 | return scrollEventCall(_widget); 1206 | } 1207 | 1208 | override bool activateEvent(bool isActive){ 1209 | return activateEventCall(_widget, isActive); 1210 | } 1211 | 1212 | override bool timerEvent(uint msecs){ 1213 | if (scrollbarVisible){ 1214 | _scrollbarMsecs += msecs; 1215 | if (!scrollbarVisible) 1216 | scrollEventCall(_widget); // HACK: to get it to draw over scrollbar 1217 | } 1218 | return timerEventCall(_widget, msecs); 1219 | } 1220 | 1221 | override bool updateEvent(){ 1222 | bool ret = updateEventCall(_widget); 1223 | if (!scrollbarVisible) 1224 | return ret; 1225 | scrollbarVertDraw; 1226 | scrollbarHorzDraw; 1227 | if (view.moveTo(width - 1, height - 1)) 1228 | view.write(' ', _scrollbarFg, _scrollbarBg); 1229 | return true; 1230 | } 1231 | 1232 | public: 1233 | /// constructor 1234 | this(){} 1235 | 1236 | /// widget to contain 1237 | @property QWidget widget(){ 1238 | return _widget; 1239 | } 1240 | /// ditto 1241 | @property QWidget widget(QWidget newVal){ 1242 | if (_widget) 1243 | disown(_widget); 1244 | if (newVal){ 1245 | _widget = newVal; 1246 | adopt(_widget); 1247 | widgetPosition(_widget, 0, 0); 1248 | } 1249 | _scrollX = _scrollY = 0; 1250 | resize; 1251 | return _widget; 1252 | } 1253 | 1254 | override bool widgetIsActive(QWidget widget){ 1255 | return _widget == widget; 1256 | } 1257 | 1258 | override @property bool wantsFocus() const { 1259 | return _widget && 1260 | (_widget.wantsFocus || _widget.height > height || _widget.width > width); 1261 | } 1262 | 1263 | /// Returns: number of milliseconds the scrollbar is visible for, after 1264 | /// scrolling 1265 | @property uint scrollbarVisibleForMsecs() const { 1266 | return _scrollbarVisibleForMsecs; 1267 | } 1268 | /// ditto 1269 | @property uint scrollbarVisibleForMsecs(uint newVal){ 1270 | return _scrollbarVisibleForMsecs = newVal; 1271 | } 1272 | 1273 | /// upper bound for scrollX (inclusive) 1274 | @property scrollXMax(){ 1275 | if (!_widget || width >= _widget.width) 1276 | return 0; 1277 | return _widget.width - width; 1278 | } 1279 | /// scrollX 1280 | @property uint scrollX(){ 1281 | return _scrollX; 1282 | } 1283 | /// ditto 1284 | @property uint scrollX(uint newVal){ 1285 | newVal = min(newVal, scrollXMax); 1286 | if (newVal == _scrollX) 1287 | return _scrollX; 1288 | _scrollX = newVal; 1289 | _scrollbarMsecs = 0; 1290 | scrollEvent; 1291 | update; 1292 | return _scrollX; 1293 | } 1294 | 1295 | /// upper bound for scrollY (inclusive) 1296 | @property uint scrollYMax(){ 1297 | if (!_widget || height >= _widget.height) 1298 | return 0; 1299 | return _widget.height - height; 1300 | } 1301 | /// scrollX 1302 | @property uint scrollY(){ 1303 | return _scrollY; 1304 | } 1305 | /// ditto 1306 | @property uint scrollY(uint newVal){ 1307 | newVal = min(newVal, scrollYMax); 1308 | if (newVal == _scrollY) 1309 | return _scrollY; 1310 | _scrollY = newVal; 1311 | _scrollbarMsecs = 0; 1312 | scrollEvent; 1313 | update; 1314 | return _scrollY; 1315 | } 1316 | 1317 | override bool widthConstraint(uint min = 0, uint max = 0){ 1318 | return super.widthConstraint(min, 0); // dont allow max to be non-zero 1319 | } 1320 | 1321 | override bool heightConstraint(uint min = 0, uint max = 0){ 1322 | return super.heightConstraint(min, 0); // dont allow max to be non-zero 1323 | } 1324 | 1325 | override @property uint maxWidth(){ 1326 | if (_widget) 1327 | return _widget.maxWidth; 1328 | return 0; 1329 | } 1330 | 1331 | override @property uint maxHeight(){ 1332 | if (_widget) 1333 | return _widget.maxHeight; 1334 | return 0; 1335 | } 1336 | } 1337 | 1338 | /// Terminal 1339 | class QTerminal : QContainer{ 1340 | private: 1341 | /// To actually access the terminal 1342 | TermWrapper _termWrap; 1343 | bool _isRunning; 1344 | /// the key used for cycling active widget 1345 | dchar _activeWidgetCycleKey = WIDGET_CYCLE_KEY; 1346 | 1347 | /// Reads InputEvent and calls appropriate functions 1348 | void _readEvent(Event event){ 1349 | if (event.type == Event.Type.HangupInterrupt){ 1350 | if (stopOnInterrupt){ 1351 | _isRunning = false; 1352 | }else{ // otherwise read it as a Ctrl+C 1353 | KeyboardEvent keyEvent; 1354 | keyEvent.key = 'c'; 1355 | keyEvent.mod = KeyboardEvent.Modifier.Control; 1356 | keyboardEventCall(this, keyEvent, false); 1357 | } 1358 | }else if (event.type == Event.Type.Keyboard){ 1359 | keyboardEvent(event.keyboard, false); 1360 | }else if (event.type == Event.Type.Mouse){ 1361 | mouseEvent(event.mouse); 1362 | }else if (event.type == Event.Type.Resize){ 1363 | resizeEvent; 1364 | } 1365 | } 1366 | 1367 | /// writes view to _termWrap 1368 | void _flushBuffer(){ 1369 | if (!view.height || !view.width) 1370 | return; 1371 | Viewport.Cell prev = view._buffer[0][0]; 1372 | _termWrap.color(prev.fg, prev.bg); 1373 | foreach (y, row; view._buffer){ 1374 | foreach (x, cell; row){ 1375 | if (!prev.colorsSame(cell)){ 1376 | _termWrap.color(cell.fg, cell.bg); 1377 | prev = cell; 1378 | } 1379 | _termWrap.put(cast(uint)x, cast(uint)y, cell.c); 1380 | } 1381 | } 1382 | } 1383 | 1384 | protected: 1385 | /// positions cursor 1386 | void cursorPosition(){ 1387 | if (_cursorX < 0 || _cursorY < 0){ 1388 | _termWrap.cursorVisible = false; 1389 | }else{ 1390 | _termWrap.moveCursor(_cursorX, _cursorY); 1391 | _termWrap.cursorVisible = true; 1392 | } 1393 | } 1394 | 1395 | /// Resets cursor position 1396 | void cursorReset(){ 1397 | _cursorX = _cursorY = -1; 1398 | } 1399 | 1400 | override bool requestUpdate(QWidget widget){ 1401 | if (!_widget || widget != _widget) 1402 | return false; 1403 | _requestingUpdate = true; 1404 | return true; 1405 | } 1406 | 1407 | override bool mouseEvent(MouseEvent mouse){ 1408 | if (_customMouseEvent) 1409 | _customMouseEvent(this, mouse); 1410 | return super.mouseEvent(mouse); 1411 | } 1412 | 1413 | override bool keyboardEvent(KeyboardEvent key, bool cycle){ 1414 | cycle = key == KeyboardEvent(_activeWidgetCycleKey); 1415 | if (_customKeyboardEvent) 1416 | _customKeyboardEvent(this, key, cycle); 1417 | if (keyboardEventCall(_widget, key, cycle)) 1418 | return true; 1419 | if (!_pgScroll) 1420 | return false; 1421 | if (key == KeyboardEvent(Key.PageUp) && _scrollY > 0){ 1422 | scrollY = scrollY - min(scrollY, height); 1423 | return true; 1424 | } 1425 | if (key == KeyboardEvent(Key.PageDown) && _scrollY < scrollYMax){ 1426 | scrollY = scrollY + height; 1427 | return true; 1428 | } 1429 | return false; 1430 | } 1431 | 1432 | override bool resizeEvent(){ 1433 | _height = _termWrap.height; 1434 | _width = _termWrap.width; 1435 | view._resize(_width, _height); 1436 | 1437 | if (_customResizeEvent) 1438 | _customResizeEvent(this); 1439 | super.resizeEvent; 1440 | return true; 1441 | } 1442 | 1443 | override bool timerEvent(uint msecs){ 1444 | if (_customTimerEvent) 1445 | _customTimerEvent(this, msecs); 1446 | return super.timerEvent(msecs); 1447 | } 1448 | 1449 | override bool updateEvent(){ 1450 | if (!_requestingUpdate) 1451 | return false; 1452 | cursorReset; 1453 | _requestingUpdate = false; 1454 | super.updateEvent; 1455 | // flush view._buffer to _termWrap 1456 | _flushBuffer; 1457 | cursorPosition; 1458 | _termWrap.flush; 1459 | return true; 1460 | } 1461 | 1462 | public: 1463 | /// whether to stop UI loop on Interrupt (Ctrl+C) 1464 | bool stopOnInterrupt = true; 1465 | /// time to wait between timer events (milliseconds) 1466 | ushort timerMsecs = 500; 1467 | /// minimum time to wait before updating, between events 1468 | ushort updateMsecs = 50; // 20 updates per second 1469 | 1470 | /// constructor 1471 | this(){ 1472 | // HACK: "fix" for issue #18 (resizing on alacritty borked) 1473 | if (environment.get("TERM", "") == "alacritty") 1474 | environment["TERM"] = "xterm"; 1475 | 1476 | _termWrap = new TermWrapper; 1477 | } 1478 | ~this(){ 1479 | .destroy(_termWrap); 1480 | } 1481 | 1482 | /// stops UI loop. **not instant** 1483 | void stop(){ 1484 | _isRunning = false; 1485 | } 1486 | 1487 | /// runs the UI loop 1488 | void run(){ 1489 | resizeEvent; 1490 | _isRunning = true; 1491 | StopWatch sw = StopWatch(AutoStart.yes); 1492 | int timerUpdate, timerTimer; 1493 | while (_isRunning){ 1494 | Event event; 1495 | if (_termWrap.getEvent(cast(int)min(timerTimer, timerUpdate), event)) 1496 | _readEvent(event); 1497 | int passed = cast(int)sw.peek.total!"msecs"; 1498 | sw.reset; 1499 | timerUpdate -= passed; 1500 | timerTimer -= passed; 1501 | if (timerUpdate <= 0){ 1502 | updateEvent; 1503 | timerUpdate = updateMsecs; 1504 | } 1505 | if (timerTimer <= 0){ 1506 | timerEvent(timerMsecs); 1507 | timerTimer = timerMsecs; 1508 | } 1509 | } 1510 | } 1511 | } 1512 | --------------------------------------------------------------------------------