diff --git a/demos/screen_write_strings.d b/demos/screen_write_strings.d new file mode 100644 index 0000000..06d81ce --- /dev/null +++ b/demos/screen_write_strings.d @@ -0,0 +1,114 @@ +/+ dub.sdl: + name "screen_write_strings" + description "Demo showcasing Screen string/style write helpers" + dependency "dcell" path=".." + targetType "executable" + +/ + +// SPDX-License-Identifier: BSL-1.0 + +/** + * Demo: Screen string and style writing helpers. + * + * This tiny demo showcases the convenience helpers added to `Screen`: + * - Single-cell string assignment via index operators: `screen[x, y] = "A"` and `screen[Coord(x,y)] = "Z"`. + * - Style assignment via index operators: `screen[x, y] = someStyle;` (preserves existing text). + * - Multi-cell string write with truncation: `screen.write(x, y, s);` + * - Multi-cell string write with wrapping: `screen.writeWrap(x, y, s);` + * - Styled variants that apply a uniform style across written cells. + * + * Press ESC (or F1) to exit. Resizing the terminal will redraw the content. + */ +module screen_write_strings; + +import std.concurrency : thisTid, receive; +import dcell; // public imports provide Screen, Style, Color, Key, Event, etc. + +private void draw(Screen s) +{ + auto sz = s.size(); + + // Set a pleasant default background for readability. + Style def; def.bg = Color.silver; def.fg = Color.black; + s.setStyle(def); + s.clear(); + + // Title centered using opDollar ($) and Coord overloads later below. + Style title; title.fg = Color.white; title.bg = Color.darkBlue; title.attr = Attr.bold; + string titleText = "Screen.write* demo"; + // Use truncate write for the title (it will naturally truncate if terminal is very small). + int titleX = (sz.x - cast(int) titleText.length) / 2; + if (titleX < 0) titleX = 0; + s.write(cast(size_t) titleX, 0, titleText, title); + + // 1) Single-cell string assignment via index operators. + s[2, 2] = "A"; // string write (preserves style) + s[3, 2] = "角"; // non-ASCII example + s[4, 2] = "\n"; // control normalized to space + s[Coord(5, 2)] = "Z"; // Coord overload + + s.write(1, 4, "Дејан Лекић"); // Serbian cyrillic + s.write(1, 5, "Dejan Lekić"); // Serbian latin + + // 2) Style assignment via index operators, preserving text. + // Seed a cell's text then apply style only. + s[8, 2] = "S"; + Style emph; emph.fg = Color.yellow; emph.bg = Color.maroon; emph.attr = Attr.bold | Attr.underline; + s[8, 2] = emph; // apply style, text stays "S" + + // 3) Multi-cell string write (truncate at end of row). + string truncMsg = "This will truncate at row end"; + size_t tx = cast(size_t) (sz.x > 24 ? sz.x - 24 : 0); + s.write(tx, 4, truncMsg); // preserves existing styles + + // 4) Multi-cell string write with wrapping. + string wrapMsg = "Wrapping across rows using writeWrap(...)"; + size_t wx = (sz.x > 6) ? cast(size_t) (sz.x - 6) : 0; // start near the right edge + s.writeWrap(wx, 6, wrapMsg); + + // 5) Styled variants: apply a uniform style while writing. + Style banner; banner.fg = Color.black; banner.bg = Color.papayaWhip; banner.attr = Attr.reverse; + s.write(2, 8, "Styled truncate", banner); + s.writeWrap(2, 10, "Styled wrapping continues to next line if needed", banner); + + // 6) Coord overloads for write helpers and use of $ for center marker. + s.write(Coord(2, 12), "Coord overload works →" ); + s[$/2, $/2] = "+"; // center marker + + s.show(); +} + +private void handleEvent(Screen s, Event ev) +{ + import core.stdc.stdlib : exit; + switch (ev.type) + { + case EventType.key: + if (ev.key.key == Key.esc || ev.key.key == Key.f1) + { + s.stop(); + exit(0); + } + break; + case EventType.resize: + s.resize(); + draw(s); + s.sync(); + break; + default: + break; + } +} + +void main() +{ + auto screen = newScreen(); + assert(screen !is null); + + screen.start(thisTid()); + draw(screen); + for (;;) + { + receive((Event ev) { handleEvent(screen, ev); }); + } +} diff --git a/source/dcell/screen.d b/source/dcell/screen.d index 67a2caa..8aa9051 100644 --- a/source/dcell/screen.d +++ b/source/dcell/screen.d @@ -57,6 +57,216 @@ interface Screen this[pos.x, pos.y] = c; } + /** + * Set content for the cell, preserving existing styling. + * + * Params: + * s = text (character) to display. Note that only a single character + * (including combining marks) is written. If empty or a control + * character, a single space is used instead. + * x = X coordinate (column) + * y = Y coordinate (row) + */ + final void opIndexAssign(string s, size_t x, size_t y) + { + if (s == "" || s[0] < ' ') + { + s = " "; + } + this[x, y].text = s; // preserve existing style + } + + /** Convenience variant for Coord. */ + final void opIndexAssign(string s, Coord pos) + { + this[pos.x, pos.y] = s; // delegate to (x, y) overload + } + + /** + * Set style for the cell, preserving existing text/content. + * + * Params: + * v = style to apply to the cell + * x = X coordinate (column) + * y = Y coordinate (row) + */ + final void opIndexAssign(Style v, size_t x, size_t y) + { + this[x, y].style = v; // preserve existing text + } + + /** Convenience variant for Coord. */ + final void opIndexAssign(Style v, Coord pos) + { + this[pos.x, pos.y] = v; // delegate to (x, y) overload + } + + /** + * Write a multi-character string horizontally starting at the given + * location, truncating at the end of the row. Existing cell styles are + * preserved; only text content is changed. + * + * Params: + * x = X coordinate (column) to start writing at + * y = Y coordinate (row) to start writing at + * s = string to write; each Unicode scalar value maps to a single cell + * (combining marks are not clustered; behavior mirrors CellBuffer) + */ + final void write(size_t x, size_t y, string s) + { + auto sz = size(); + if (y >= cast(size_t) sz.y) return; + if (x >= cast(size_t) sz.x) return; + if (s.length == 0) return; + + import std.utf : decode; + size_t i = 0; + while (i < s.length && x < cast(size_t) sz.x) + { + auto start = i; + decode(s, i); // advances i to next code point start + auto unit = s[start .. i]; + if (unit.length == 0 || unit[0] < ' ') + { + unit = " "; + } + this[x, y] = unit; // preserves style + ++x; + } + } + + /** Convenience overload using Coord. */ + final void write(Coord pos, string s) + { + write(cast(size_t) pos.x, cast(size_t) pos.y, s); + } + + /** + * Write a multi-character string starting at the given location, wrapping + * to the next row when the end of the current row is reached. Writing + * stops at the bottom of the screen. Existing cell styles are preserved. + * + * Params: + * x = X coordinate (column) to start writing at + * y = Y coordinate (row) to start writing at + * s = string to write; each Unicode scalar value maps to a single cell + */ + final void writeWrap(size_t x, size_t y, string s) + { + auto sz = size(); + if (y >= cast(size_t) sz.y) return; + if (x >= cast(size_t) sz.x && (y + 1) >= cast(size_t) sz.y) return; + if (s.length == 0) return; + + import std.utf : decode; + size_t i = 0; + while (i < s.length && y < cast(size_t) sz.y) + { + if (x >= cast(size_t) sz.x) + { + x = 0; + ++y; + if (y >= cast(size_t) sz.y) break; + } + auto start = i; + decode(s, i); + auto unit = s[start .. i]; + if (unit.length == 0 || unit[0] < ' ') + { + unit = " "; + } + this[x, y] = unit; + ++x; + } + } + + /** Convenience overload using Coord for wrapping write. */ + final void writeWrap(Coord pos, string s) + { + writeWrap(cast(size_t) pos.x, cast(size_t) pos.y, s); + } + + /** + * Write a string with a uniform style applied to each written cell, + * truncating at the end of the row. + * + * Params: + * x = X coordinate (column) + * y = Y coordinate (row) + * s = string to write + * st = style to apply to each written cell + */ + final void write(size_t x, size_t y, string s, Style st) + { + auto sz = size(); + if (y >= cast(size_t) sz.y) return; + if (x >= cast(size_t) sz.x) return; + if (s.length == 0) return; + + import std.utf : decode; + size_t i = 0; + while (i < s.length && x < cast(size_t) sz.x) + { + auto start = i; + decode(s, i); + auto unit = s[start .. i]; + if (unit.length == 0 || unit[0] < ' ') + { + unit = " "; + } + this[x, y] = unit; + this[x, y] = st; + ++x; + } + } + + /** Convenience overload using Coord for styled write. */ + final void write(Coord pos, string s, Style st) + { + write(cast(size_t) pos.x, cast(size_t) pos.y, s, st); + } + + /** + * Write a string with a uniform style applied, wrapping across rows as + * needed until either the entire string is written or the bottom of the + * screen is reached. + */ + final void writeWrap(size_t x, size_t y, string s, Style st) + { + auto sz = size(); + if (y >= cast(size_t) sz.y) return; + if (x >= cast(size_t) sz.x && (y + 1) >= cast(size_t) sz.y) return; + if (s.length == 0) return; + + import std.utf : decode; + size_t i = 0; + while (i < s.length && y < cast(size_t) sz.y) + { + if (x >= cast(size_t) sz.x) + { + x = 0; + ++y; + if (y >= cast(size_t) sz.y) break; + } + auto start = i; + decode(s, i); + auto unit = s[start .. i]; + if (unit.length == 0 || unit[0] < ' ') + { + unit = " "; + } + this[x, y] = unit; + this[x, y] = st; + ++x; + } + } + + /** Convenience overload using Coord for styled, wrapping write. */ + final void writeWrap(Coord pos, string s, Style st) + { + writeWrap(cast(size_t) pos.x, cast(size_t) pos.y, s, st); + } + /** Support $ operation in indices. */ size_t opDollar(size_t dim)() { static if (dim == 0) { @@ -235,3 +445,230 @@ interface Screen */ void stop(); } + +version(unittest) +private class FakeScreen : Screen +{ + private CellBuffer buffer; + private Style defStyle; + + this(Coord sz) + { + buffer = new CellBuffer(sz); + } + + // Clear screen to spaces using current default style. + void clear() + { + buffer.fill(" ", defStyle); + } + + // Indexing into our backing buffer. + ref Cell opIndex(size_t x, size_t y) + { + return buffer[x, y]; + } + + // Assign a full Cell into our backing buffer. + void opIndexAssign(Cell c, size_t x, size_t y) + { + buffer[x, y] = c; + } + + // Note: string convenience assignment is provided by Screen's final + // overloads; we don't re-declare them here to avoid overriding finals. + + // Cursor-related helpers (no-ops for tests). + void showCursor(Cursor cur) {} + void showCursor(Coord pos, Cursor cur = Cursor.current) {} + + // Basic capabilities not used in tests. + bool hasKey(Key) { return false; } + + // Current size of the buffer. + Coord size() + { + return buffer.size(); + } + + // Event APIs return none for tests. + Event receiveEvent(Duration) { return Event(EventType.none); } + Event receiveEvent() { return Event(EventType.none); } + + // Paste & mouse control (no-ops for tests). + void enablePaste(bool) {} + bool hasMouse() { return false; } + void enableMouse(MouseEnable) {} + + // Color reporting (not used). + int colors() { return 0; } + + // Rendering flush/sync (no-ops). + void show() {} + void sync() {} + + // Bell (no-op). + void beep() {} + + // Sizing and default style. + void setSize(Coord sz) { buffer.resize(sz); } + void setStyle(Style st) { defStyle = st; } + + // Fill helpers delegate to buffer. + void fill(string s, Style style) { buffer.fill(s, style); } + void fill(string s) { buffer.fill(s); } + + // Resize notification (not needed for tests). + void resize() {} + + // Lifecycle (no-ops for tests). + void start() {} + void start(Tid) {} + void stop() {} +} + +unittest +{ + // Test helper: construct a small fake screen for exercising Screen conveniences. + Screen screen = new FakeScreen(Coord(4, 3)); + + // Test 1: Write simple glyph at X,Y and verify text and style preservation. + auto beforeStyle = screen[2, 1].style; // capture existing style + screen[2, 1] = "A"; // use string convenience (x, y) + assert(screen[2, 1].text == "A"); + assert(screen[2, 1].style == beforeStyle); // style preserved + + // Test 2: Write via Coord overload and verify text. + screen[Coord(0, 0)] = "Z"; // use Coord convenience + assert(screen[0, 0].text == "Z"); + + // Test 3: Empty string is normalized to a single space. + screen[1, 1] = ""; + assert(screen[1, 1].text == " "); + + // Test 4: Control character is normalized to a single space. + screen[1, 2] = "\n"; + assert(screen[1, 2].text == " "); + + // Test 5: Style is preserved when writing a string. + Style styled; + styled.fg = Color.red; + styled.bg = Color.blue; + styled.attr = Attr.bold; + // seed a non-default style using Cell assignment + screen[3, 0] = Cell("X", styled); + // now write string and ensure style unchanged while text updated + screen[3, 0] = "="; + assert(screen[3, 0].text == "="); + assert(screen[3, 0].style == styled); +} + +unittest +{ + // Test style assignment via (x, y): style updates while text is preserved. + Screen screen = new FakeScreen(Coord(4, 3)); + screen[1, 1] = "T"; // seed some text + auto beforeText = screen[1, 1].text; + + Style s; + s.fg = Color.green; + s.bg = Color.black; + s.attr = Attr.underline; + + screen[1, 1] = s; // use Style convenience (x, y) + assert(screen[1, 1].style == s); // style applied + assert(screen[1, 1].text == beforeText); // text preserved + + // Test style assignment via Coord overload: same guarantees hold. + Style s2; + s2.fg = Color.yellow; + s2.bg = Color.blue; + s2.attr = Attr.reverse; + + screen[Coord(2, 0)] = "X"; // seed text + screen[Coord(2, 0)] = s2; // apply style via Coord + assert(screen[2, 0].style == s2); // style applied + assert(screen[2, 0].text == "X"); // text preserved +} + +unittest +{ + // Write basic (truncate): ensure writing stops at end of row. + Screen screen = new FakeScreen(Coord(4, 2)); + screen.clear(); + screen.write(1, 0, "ABC"); + assert(screen[1, 0].text == "A"); + assert(screen[2, 0].text == "B"); + assert(screen[3, 0].text == "C"); + // Writing at last column truncates remainder + screen.write(3, 1, "XYZ"); + assert(screen[3, 1].text == "X"); +} + +unittest +{ + // Write via Coord (truncate) + Screen screen = new FakeScreen(Coord(4, 2)); + screen.clear(); + screen.write(Coord(0, 1), "Hi"); + assert(screen[0, 1].text == "H"); + assert(screen[1, 1].text == "i"); +} + +unittest +{ + // Wrap across rows: string continues on next line. + Screen screen = new FakeScreen(Coord(3, 2)); + screen.clear(); + screen.writeWrap(2, 0, "WXYZ"); + assert(screen[2, 0].text == "W"); + assert(screen[0, 1].text == "X"); + assert(screen[1, 1].text == "Y"); + assert(screen[2, 1].text == "Z"); +} + +unittest +{ + // Control and empty normalization: control → space; empty → no-ops + Screen screen = new FakeScreen(Coord(4, 1)); + screen.clear(); + screen.write(0, 0, "\n\t"); + assert(screen[0, 0].text == " "); + assert(screen[1, 0].text == " "); + screen.writeWrap(Coord(2, 0), ""); // no writes + assert(screen[2, 0].text == " "); +} + +unittest +{ + // Style application (truncate): both text and style applied to written cells. + Screen screen = new FakeScreen(Coord(4, 1)); + screen.clear(); + Style st; st.fg = Color.green; st.bg = Color.black; st.attr = Attr.bold; + screen.write(1, 0, "AB", st); + assert(screen[1, 0].text == "A" && screen[1, 0].style == st); + assert(screen[2, 0].text == "B" && screen[2, 0].style == st); + // Unaffected cell keeps default style (not asserting exact default value, just that text stayed space) + assert(screen[0, 0].text == " "); +} + +unittest +{ + // Style application with wrap: style applied across wrapped cell. + Screen screen = new FakeScreen(Coord(3, 2)); + screen.clear(); + Style st; st.fg = Color.yellow; st.bg = Color.blue; st.attr = Attr.reverse; + screen.writeWrap(2, 0, "ab", st); + assert(screen[2, 0].text == "a" && screen[2, 0].style == st); + assert(screen[0, 1].text == "b" && screen[0, 1].style == st); +} + +unittest +{ + // Non-ASCII smoke test: ensure Unicode chars are placed as units. + Screen screen = new FakeScreen(Coord(4, 1)); + screen.clear(); + screen.write(0, 0, "角🙂"); + assert(screen[0, 0].text == "角"); + assert(screen[1, 0].text == "🙂"); +}