Skip to content

Commit 2f1a3da

Browse files
committed
karm-ui: Fixed multiline selection in text input and added mouse selection.
1 parent 2b80fbc commit 2f1a3da

File tree

4 files changed

+175
-23
lines changed

4 files changed

+175
-23
lines changed

src/karm-gfx/prose.cpp

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -440,16 +440,13 @@ export struct Prose : Meta::Pinned {
440440
for (auto& line : _lines) {
441441
Au lineTop = currHeight;
442442

443-
Au maxAscent = 0_au;
444-
Au maxDescend = 0_au;
443+
Au maxAscent = fontAscent;
444+
Au maxDescend = fontDescend;
445445
for (auto const& block : line.blocks()) {
446446
if (block.strut()) {
447447
Au baseline{block.strut()->baseline};
448448
maxAscent = max(maxAscent, baseline);
449449
maxDescend = max(maxDescend, block.strut()->size.y - baseline);
450-
} else {
451-
maxAscent = max(maxAscent, fontAscent);
452-
maxDescend = max(maxDescend, fontDescend);
453450
}
454451
}
455452

@@ -594,6 +591,57 @@ export struct Prose : Meta::Pinned {
594591

595592
return {block.pos + cell.pos, cell.Yposition(line.baseline)};
596593
}
594+
595+
// MARK: Hit Testing -------------------------------------------------------
596+
597+
usize hitTest(Vec2Au point) const {
598+
if (isEmpty(_lines))
599+
return 0;
600+
601+
Au fontAscent = Au{_lineHeight};
602+
603+
// Find the line containing the y coordinate
604+
usize li = _lines.len() - 1;
605+
for (usize i = 0; i + 1 < _lines.len(); i++) {
606+
Au nextLineTop = _lines[i + 1].baseline - fontAscent;
607+
if (point.y < nextLineTop) {
608+
li = i;
609+
break;
610+
}
611+
}
612+
613+
auto& line = _lines[li];
614+
615+
if (isEmpty(line.blocks()))
616+
return line.runeRange.start;
617+
618+
// Find the block closest to the x coordinate
619+
usize bi = 0;
620+
for (usize i = 0; i < line.blocks().len(); i++) {
621+
auto& block = line.blocks()[i];
622+
if (point.x < block.pos + block.width) {
623+
bi = i;
624+
break;
625+
}
626+
bi = i;
627+
}
628+
629+
auto& block = line.blocks()[bi];
630+
631+
if (isEmpty(block.cells()))
632+
return block.runeRange.start;
633+
634+
// Find the cell closest to the x coordinate
635+
for (auto& cell : block.cells()) {
636+
Au cellMid = block.pos + cell.pos + cell.adv / 2_au;
637+
if (point.x < cellMid)
638+
return cell.runeRange.start;
639+
}
640+
641+
// Past the last cell — return end of block
642+
auto& lastCell = last(block.cells());
643+
return lastCell.runeRange.end();
644+
}
597645
};
598646

599647
} // namespace Karm::Gfx

src/karm-ui/input.cpp

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -380,31 +380,43 @@ static void _paintCaret(Gfx::Canvas& g, Gfx::Prose& p, usize runeIndex, Gfx::Col
380380
g.lineTo(ce);
381381
g.strokeStyle({
382382
.fill = color,
383-
.width = 2.0,
383+
.width = 1.0,
384384
.align = Gfx::CENTER_ALIGN,
385385
});
386386
g.stroke();
387387
}
388388

389389
static void _paintSelection(Gfx::Canvas& g, Gfx::Prose& p, usize start, usize end, Gfx::Color color) {
390-
if (start == end)
390+
if (start == end or isEmpty(p._lines))
391391
return;
392392

393-
if (not p._style.multiline) {
394-
auto ps = p.queryPosition(start);
395-
auto pe = p.queryPosition(end);
396-
auto m = p._style.font.metrics();
393+
if (start > end)
394+
std::swap(start, end);
397395

398-
auto rect =
396+
auto m = p._style.font.metrics();
397+
auto [startLi, startBi, startCi] = p.lbcAt(start);
398+
auto [endLi, endBi, endCi] = p.lbcAt(end);
399+
400+
g.fillStyle(color);
401+
402+
for (usize li = startLi; li <= endLi; li++) {
403+
auto& line = p._lines[li];
404+
405+
auto lineStart = (li == startLi)
406+
? p.queryPosition(start)
407+
: Vec2Au{0_au, line.baseline};
408+
409+
auto lineEnd = (li == endLi)
410+
? p.queryPosition(end)
411+
: Vec2Au{line.width, line.baseline};
412+
413+
g.fill(
399414
RectAu::fromTwoPoint(
400-
ps + Vec2Au{0_au, Au{m.descend}},
401-
pe - Vec2Au{0_au, Au{m.ascend}}
415+
lineStart + Vec2Au{0_au, Au{m.descend}},
416+
lineEnd - Vec2Au{0_au, Au{m.ascend}}
402417
)
403-
.cast<f64>();
404-
405-
g.fillStyle(color);
406-
g.fill(rect);
407-
return;
418+
.cast<f64>()
419+
);
408420
}
409421
}
410422

@@ -414,6 +426,7 @@ struct Input : View<Input> {
414426
FocusListener _focus;
415427
Rc<TextModel> _model;
416428
Send<TextAction> _onChange;
429+
bool _mouseDown = false;
417430

418431
Opt<Rc<Gfx::Prose>> _text;
419432

@@ -458,6 +471,31 @@ struct Input : View<Input> {
458471
}
459472

460473
void event(App::Event& e) override {
474+
_focus.event(*this, e);
475+
476+
if (auto me = e.is<App::MouseEvent>()) {
477+
if (me->type == App::MouseEvent::PRESS and
478+
me->button == App::MouseButton::LEFT and
479+
bound().contains(me->pos)) {
480+
_ensureText().layout(Au{bound().width});
481+
auto local = me->pos - bound().xy;
482+
auto pos = _ensureText().hitTest({Au{local.x}, Au{local.y}});
483+
_mouseDown = true;
484+
_onChange(*this, TextAction::moveTo(pos));
485+
e.accept();
486+
} else if (me->type == App::MouseEvent::MOVE and _mouseDown) {
487+
_ensureText().layout(Au{bound().width});
488+
auto local = me->pos - bound().xy;
489+
auto pos = _ensureText().hitTest({Au{local.x}, Au{local.y}});
490+
_onChange(*this, TextAction::selectTo(pos));
491+
e.accept();
492+
} else if (me->type == App::MouseEvent::RELEASE and
493+
me->button == App::MouseButton::LEFT) {
494+
_mouseDown = false;
495+
}
496+
return;
497+
}
498+
461499
auto a = TextAction::fromEvent(e);
462500
if (a) {
463501
e.accept();
@@ -472,6 +510,8 @@ struct Input : View<Input> {
472510

473511
Math::Vec2i size(Math::Vec2i s, Hint) override {
474512
auto size = _ensureText().layout(Au{s.width});
513+
// NOTE: Ensure the input is always at least 1 pixel wide to show the caret.
514+
size.x = max(size.x, Au{1});
475515
return size.ceil().cast<isize>();
476516
}
477517
};
@@ -492,6 +532,7 @@ struct SimpleInput : View<SimpleInput> {
492532
FocusListener _focus;
493533
Opt<TextModel> _model;
494534
Opt<Rc<Gfx::Prose>> _prose;
535+
bool _mouseDown = false;
495536

496537
SimpleInput(Gfx::ProseStyle style, String text, Send<String> onChange)
497538
: _style(style),
@@ -547,6 +588,32 @@ struct SimpleInput : View<SimpleInput> {
547588

548589
void event(App::Event& e) override {
549590
_focus.event(*this, e);
591+
592+
if (auto me = e.is<App::MouseEvent>()) {
593+
if (me->type == App::MouseEvent::PRESS and
594+
me->button == App::MouseButton::LEFT and
595+
bound().contains(me->pos)) {
596+
_ensureText().layout(Au{bound().width});
597+
auto local = me->pos - bound().xy;
598+
auto pos = _ensureText().hitTest({Au{local.x}, Au{local.y}});
599+
_mouseDown = true;
600+
_ensureModel().setCursor(pos);
601+
shouldRepaint(*this);
602+
e.accept();
603+
} else if (me->type == App::MouseEvent::MOVE and _mouseDown) {
604+
_ensureText().layout(Au{bound().width});
605+
auto local = me->pos - bound().xy;
606+
auto pos = _ensureText().hitTest({Au{local.x}, Au{local.y}});
607+
_ensureModel().setSelectionEnd(pos);
608+
shouldRepaint(*this);
609+
e.accept();
610+
} else if (me->type == App::MouseEvent::RELEASE and
611+
me->button == App::MouseButton::LEFT) {
612+
_mouseDown = false;
613+
}
614+
return;
615+
}
616+
550617
auto a = TextAction::fromEvent(e);
551618
if (a and a->op == TextAction::NEWLINE and not _style.multiline)
552619
a = NONE;
@@ -566,6 +633,8 @@ struct SimpleInput : View<SimpleInput> {
566633

567634
Math::Vec2i size(Math::Vec2i s, Hint) override {
568635
auto size = _ensureText().layout(Au{s.width});
636+
// NOTE: Ensure the input is always at least 1 pixel wide to show the caret.
637+
size.x = max(size.x, Au{1});
569638
return size.ceil().cast<isize>();
570639
}
571640
};

src/karm-ui/reducer.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,24 @@ struct Model {
2323
static constexpr auto reduce = R;
2424

2525
template <typename X, typename... Args>
26-
static Func<void(Node&)> bind(Args... args) {
26+
static Send<> bind(Args... args) {
2727
return bindBubble<Action>(X{std::forward<Args>(args)...});
2828
}
2929

3030
template <typename X>
31-
static Func<void(Node&)> bind(X value) {
31+
static Send<> bind(X value) {
3232
return bindBubble<Action>(value);
3333
}
3434

3535
template <typename X, typename... Args>
36-
static Opt<Func<void(Node&)>> bindIf(bool cond, Args... args) {
36+
static Opt<Send<>> bindIf(bool cond, Args... args) {
3737
if (not cond)
3838
return NONE;
3939
return bindBubble<Action>(X{std::forward<Args>(args)...});
4040
}
4141

4242
template <typename X>
43-
static Opt<Func<void(Node&)>> bindIf(bool cond, X value) {
43+
static Opt<Send<>> bindIf(bool cond, X value) {
4444
if (not cond)
4545
return NONE;
4646
return bindBubble<Action>(value);

src/karm-ui/text.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export struct TextAction {
4646
CUT,
4747
PASTE,
4848

49+
MOVE_TO,
50+
SELECT_TO,
51+
4952
UNDO,
5053
REDO,
5154

@@ -54,9 +57,22 @@ export struct TextAction {
5457

5558
_Op op;
5659
Rune rune = {};
60+
usize pos = {};
5761

5862
TextAction(_Op op, Rune rune = 0) : op(op), rune(rune) {}
5963

64+
static TextAction moveTo(usize pos) {
65+
TextAction a{MOVE_TO};
66+
a.pos = pos;
67+
return a;
68+
}
69+
70+
static TextAction selectTo(usize pos) {
71+
TextAction a{SELECT_TO};
72+
a.pos = pos;
73+
return a;
74+
}
75+
6076
static Opt<TextAction> fromEvent(App::Event& e) {
6177
if (
6278
auto ke = e.is<App::KeyboardEvent>();
@@ -379,6 +395,17 @@ export struct TextModel {
379395
return _buf.len();
380396
}
381397

398+
// MARK: Cursor API -------------------------------------------------------
399+
400+
void setCursor(usize pos) {
401+
_cur.head = min(pos, _buf.len());
402+
_cur.tail = _cur.head;
403+
}
404+
405+
void setSelectionEnd(usize pos) {
406+
_cur.head = min(pos, _buf.len());
407+
}
408+
382409
// MARK: Commands
383410

384411
void insert(Rune rune) {
@@ -667,6 +694,14 @@ export struct TextModel {
667694
selectAll();
668695
break;
669696

697+
case TextAction::MOVE_TO:
698+
_moveTo(a.pos);
699+
break;
700+
701+
case TextAction::SELECT_TO:
702+
_selectTo(a.pos);
703+
break;
704+
670705
case TextAction::UNDO:
671706
undo();
672707
break;

0 commit comments

Comments
 (0)