Skip to content

Commit

Permalink
karm-text: Allow text to be colored.
Browse files Browse the repository at this point in the history
  • Loading branch information
sleepy-monax committed Dec 16, 2024
1 parent c721aee commit 1eda808
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 59 deletions.
2 changes: 2 additions & 0 deletions src/apps/hideo-zoo/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "page-print-dialog.h"
#include "page-radio.h"
#include "page-resizable.h"
#include "page-rich-text.h"
#include "page-rows.h"
#include "page-select.h"
#include "page-side-nav.h"
Expand All @@ -45,6 +46,7 @@ static Array PAGES = {
&PAGE_PRINT_DIALOG,
&PAGE_RADIO,
&PAGE_RESIZABLE,
&PAGE_RICHTEXT,
&PAGE_ROWS,
&PAGE_SELECT,
&PAGE_SIDE_PANEL,
Expand Down
45 changes: 45 additions & 0 deletions src/apps/hideo-zoo/page-rich-text.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#pragma once

#include <karm-kira/titlebar.h>
#include <karm-ui/layout.h>
#include <mdi/text-box.h>

#include "model.h"

namespace Hideo::Zoo {

static inline Page PAGE_RICHTEXT{
Mdi::TEXT_BOX,
"Rich Text"s,
"An area that displays text with various styles and formatting options.",
[] {
auto prose = makeStrong<Text::Prose>(Ui::TextStyles::bodyMedium());

prose->append("This is a simple text with no formatting.\n"s);

prose->append("We can change color: "s);

prose->pushSpan();
prose->spanColor(Gfx::RED);
prose->append("red"s);
prose->popSpan();
prose->append(", "s);

prose->pushSpan();
prose->spanColor(Gfx::GREEN);
prose->append(" green"s);
prose->popSpan();
prose->append(", "s);

prose->pushSpan();
prose->spanColor(Gfx::BLUE);
prose->append("blue"s);
prose->popSpan();

prose->append(".\n"s);

return Ui::text(prose) | Ui::center();
},
};

} // namespace Hideo::Zoo
38 changes: 25 additions & 13 deletions src/libs/karm-text/prose.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@ Prose::Prose(ProseStyle style, Str str) : _style(style) {

void Prose::_beginBlock() {
_blocks.pushBack({
.prose = this,
.runeRange = {_runes.len(), 0},
.cellRange = {_cells.len(), 0},
});
}

void Prose::append(Rune rune) {
if (any(_blocks) and last(_blocks).newline(*this))
if (any(_blocks) and last(_blocks).newline())
_beginBlock();

if (any(_blocks) and last(_blocks).spaces(*this))
if (any(_blocks) and last(_blocks).spaces())
_beginBlock();

auto glyph = _style.font.glyph(rune == '\n' ? ' ' : rune);

_cells.pushBack({
.prose = this,
.span = _currentSpan,
.runeRange = {_runes.len(), 1},
.glyph = glyph,
});
Expand Down Expand Up @@ -61,7 +64,7 @@ void Prose::_measureBlocks() {
auto adv = 0.0f;
bool first = true;
Glyph prev = Glyph::TOFU;
for (auto &cell : block.cells(*this)) {
for (auto &cell : block.cells()) {
if (not first)
adv += _style.font.kern(prev, cell.glyph);
else
Expand All @@ -79,19 +82,20 @@ void Prose::_measureBlocks() {
void Prose::_wrapLines(f64 width) {
_lines.clear();

Line line{{}, {}};
Line line{this, {}, {}};
bool first = true;
f64 adv = 0;
for (usize i = 0; i < _blocks.len(); i++) {
auto &block = _blocks[i];
if (adv + block.width > width and _style.wordwrap and _style.multiline and not first) {
_lines.pushBack(line);
line = {block.runeRange, {i, 1}};
line = {this, block.runeRange, {i, 1}};
adv = block.width;

if (block.newline(*this)) {
if (block.newline()) {
_lines.pushBack(line);
line = {
this,
{block.runeRange.end(), 0},
{i + 1, 0},
};
Expand All @@ -101,9 +105,10 @@ void Prose::_wrapLines(f64 width) {
line.blockRange.size++;
line.runeRange.end(block.runeRange.end());

if (block.newline(*this) and _style.multiline) {
if (block.newline() and _style.multiline) {
_lines.pushBack(line);
line = {
this,
{block.runeRange.end(), 0},
{i + 1, 0},
};
Expand Down Expand Up @@ -136,7 +141,7 @@ f64 Prose::_layoutHorizontaly(f64 width) {
continue;

f64 pos = 0;
for (auto &block : line.blocks(*this)) {
for (auto &block : line.blocks()) {
block.pos = pos;
pos += block.width;
}
Expand All @@ -151,12 +156,12 @@ f64 Prose::_layoutHorizontaly(f64 width) {
break;

case TextAlign::CENTER:
for (auto &block : line.blocks(*this))
for (auto &block : line.blocks())
block.pos += free / 2;
break;

case TextAlign::RIGHT:
for (auto &block : line.blocks(*this))
for (auto &block : line.blocks())
block.pos += free;
break;
}
Expand Down Expand Up @@ -192,9 +197,16 @@ void Prose::paint(Gfx::Canvas &g) {
g.fillStyle(*_style.color);

for (auto const &line : _lines) {
for (auto &block : line.blocks(*this)) {
for (auto &cell : block.cells(*this)) {
g.fill(_style.font, cell.glyph, {block.pos + cell.pos, line.baseline});
for (auto &block : line.blocks()) {
for (auto &cell : block.cells()) {
if (cell.span and cell.span->color) {
g.push();
g.fillStyle(*cell.span->color);
g.fill(_style.font, cell.glyph, {block.pos + cell.pos, line.baseline});
g.pop();
} else {
g.fill(_style.font, cell.glyph, {block.pos + cell.pos, line.baseline});
}
}
}
}
Expand Down
98 changes: 66 additions & 32 deletions src/libs/karm-text/prose.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,80 +57,92 @@ struct ProseStyle {
}
};

struct Prose {
struct Prose : public Meta::Static {
struct Span {
MutCursor<Span> parent = nullptr;
Opt<Gfx::Color> color;
};

struct Cell {
MutCursor<Prose> prose;
MutCursor<Span> span;

urange runeRange;
Glyph glyph;
f64 pos = 0; //< Position of the glyph within the block
f64 adv = 0; //< Advance of the glyph

MutSlice<Rune> runes(Prose &t) {
return mutSub(t._runes, runeRange);
MutSlice<Rune> runes() {
return mutSub(prose->_runes, runeRange);
}

Slice<Rune> runes(Prose const &t) const {
return sub(t._runes, runeRange);
Slice<Rune> runes() const {
return sub(prose->_runes, runeRange);
}

bool newline(Prose const &t) const {
auto r = runes(t);
bool newline() const {
auto r = runes();
if (not r)
return false;
return last(r) == '\n';
}

bool space(Prose const &t) const {
auto r = runes(t);
bool space() const {
auto r = runes();
if (not r)
return false;
return last(r) == '\n' or isAsciiSpace(last(r));
}
};

struct Block {
MutCursor<Prose> prose;

urange runeRange;
urange cellRange;

f64 pos = 0; // Position of the block within the line
f64 width = 0;

MutSlice<Cell> cells(Prose &t) {
return mutSub(t._cells, cellRange);
MutSlice<Cell> cells() {
return mutSub(prose->_cells, cellRange);
}

Slice<Cell> cells(Prose const &t) const {
return sub(t._cells, cellRange);
Slice<Cell> cells() const {
return sub(prose->_cells, cellRange);
}

bool empty() const {
return cellRange.empty();
}

bool spaces(Prose const &t) const {
bool spaces() const {
if (empty())
return false;
return last(cells(t)).space(t);
return last(cells()).space();
}

bool newline(Prose const &t) const {
bool newline() const {
if (empty())
return false;
return last(cells(t)).newline(t);
return last(cells()).newline();
}
};

struct Line {
MutCursor<Prose> prose;

urange runeRange;
urange blockRange;
f64 baseline = 0; // Baseline of the line within the text
f64 width = 0;

Slice<Block> blocks(Prose const &t) const {
return sub(t._blocks, blockRange);
Slice<Block> blocks() const {
return sub(prose->_blocks, blockRange);
}

MutSlice<Block> blocks(Prose &t) {
return mutSub(t._blocks, blockRange);
MutSlice<Block> blocks() {
return mutSub(prose->_blocks, blockRange);
}
};

Expand Down Expand Up @@ -170,7 +182,29 @@ struct Prose {

void append(Slice<Rune> runes);

// MARK: Layout -------------------------------------------------------------
// MARK: Span --------------------------------------------------------------

Vec<Box<Span>> _spans;
MutCursor<Span> _currentSpan = nullptr;

void pushSpan() {
_spans.pushBack(makeBox<Span>(_currentSpan));
_currentSpan = &*last(_spans);
}

void spanColor(Gfx::Color color) {
if (not _currentSpan)
return;

_currentSpan->color = color;
}

void popSpan() {
if (_currentSpan)
_currentSpan = _currentSpan->parent;
}

// MARK: Layout ------------------------------------------------------------

void _measureBlocks();

Expand Down Expand Up @@ -218,7 +252,7 @@ struct Prose {
auto &line = _lines[li];

auto maybeBi = searchLowerBound(
line.blocks(*this), [&](Block const &b) {
line.blocks(), [&](Block const &b) {
return b.runeRange.start <=> runeIndex;
}
);
Expand All @@ -228,10 +262,10 @@ struct Prose {

auto bi = *maybeBi;

auto &block = line.blocks(*this)[bi];
auto &block = line.blocks()[bi];

auto maybeCi = searchLowerBound(
block.cells(*this), [&](Cell const &c) {
block.cells(), [&](Cell const &c) {
return c.runeRange.start <=> runeIndex;
}
);
Expand All @@ -241,7 +275,7 @@ struct Prose {

auto ci = *maybeCi;

auto cell = block.cells(*this)[ci];
auto cell = block.cells()[ci];

if (cell.runeRange.end() == runeIndex) {
// Handle the case where the rune is the last of the text
Expand All @@ -259,21 +293,21 @@ struct Prose {

auto &line = _lines[li];

if (isEmpty(line.blocks(*this)))
if (isEmpty(line.blocks()))
return {0, line.baseline};

auto &block = line.blocks(*this)[bi];
auto &block = line.blocks()[bi];

if (isEmpty(block.cells(*this)))
if (isEmpty(block.cells()))
return {block.pos, line.baseline};

if (ci >= block.cells(*this).len()) {
if (ci >= block.cells().len()) {
// Handle the case where the rune is the last of the text
auto &cell = last(block.cells(*this));
auto &cell = last(block.cells());
return {block.pos + cell.pos + cell.adv, line.baseline};
}

auto &cell = block.cells(*this)[ci];
auto &cell = block.cells()[ci];

return {block.pos + cell.pos, line.baseline};
}
Expand Down
Loading

0 comments on commit 1eda808

Please sign in to comment.