commits
tags
#include "hittest.h"
#include "clay.h"
#include "utf8.h"
#include <string.h>
#include <float.h>
#define MEASURE_BUF_SIZE 2048
static float measure_width(SDL2_Font *fonts, int fontId, int fontSize, const char *text, int start, int n) {
if (n <= 0) return 0.0f;
char buf[MEASURE_BUF_SIZE];
if (n >= MEASURE_BUF_SIZE) n = MEASURE_BUF_SIZE - 1;
memcpy(buf, text + start, (size_t)n);
buf[n] = '\0';
TTF_Font *font = fonts[fontId].font;
TTF_SetFontSize(font, fontSize);
int w = 0, h = 0;
TTF_SizeUTF8(font, buf, &w, &h);
return (float)w;
}
static Clay_ElementData get_line_data(int globalLineIndex) {
return Clay_GetElementData(CLAY_IDI("Line", globalLineIndex));
}
static Clay_ElementData get_seg_data(int globalSegIndex) {
return Clay_GetElementData(CLAY_IDI("Seg", globalSegIndex));
}
/* Finds the array index (into cache->lines) of the line that should own `offset` within `blockIndex`.
When `offset` sits exactly on the boundary between two lines, prefers the later one (start of the
next visual line), matching how a caret naturally rests after a wrapped word. */
static int find_line_for_offset(LayoutCache *cache, int blockIndex, int offset) {
int best = -1;
for (int i = 0; i < cache->lineCount; i++) {
CachedLine *l = &cache->lines[i];
if (l->blockIndex != blockIndex) continue;
if (l->srcStart <= offset) best = i;
}
return best;
}
/* Finds the byte offset within `lineIdx` whose glyph is closest to screen-space `targetX`. */
static int find_offset_in_line(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int lineIdx, float targetX) {
CachedLine *line = &cache->lines[lineIdx];
if (line->segStart == line->segEnd) return line->srcStart;
const char *text = doc->blocks[line->blockIndex].text.data;
int bestSeg = -1;
Clay_BoundingBox bestBox = {0};
for (int i = line->segStart; i < line->segEnd; i++) {
Clay_ElementData d = get_seg_data(cache->segs[i].globalSegIndex);
if (!d.found) continue;
if (bestSeg < 0) { bestSeg = i; bestBox = d.boundingBox; }
if (targetX >= d.boundingBox.x && targetX < d.boundingBox.x + d.boundingBox.width) {
bestSeg = i;
bestBox = d.boundingBox;
break;
}
if (targetX >= d.boundingBox.x) { bestSeg = i; bestBox = d.boundingBox; }
}
if (bestSeg < 0) return line->srcStart;
CachedSegment *seg = &cache->segs[bestSeg];
float localTarget = targetX - bestBox.x;
if (localTarget <= 0.0f) return seg->srcStart;
int pos = seg->srcStart;
while (pos < seg->srcEnd) {
int clen = utf8_next_len(text, pos, seg->srcEnd);
float wBefore = measure_width(fonts, seg->fontId, seg->fontSize, text, seg->srcStart, pos - seg->srcStart);
float wAfter = measure_width(fonts, seg->fontId, seg->fontSize, text, seg->srcStart, pos + clen - seg->srcStart);
if (wAfter >= localTarget) {
float mid = (wBefore + wAfter) * 0.5f;
return (localTarget < mid) ? pos : pos + clen;
}
pos += clen;
}
return seg->srcEnd;
}
HitResult hittest_point(LayoutCache *cache, Document *doc, SDL2_Font *fonts, Clay_Vector2 point) {
HitResult r = {0, 0};
if (cache->lineCount == 0 || doc->count == 0) return r;
int bestLine = -1;
float bestDist = FLT_MAX;
for (int i = 0; i < cache->lineCount; i++) {
Clay_ElementData d = get_line_data(cache->lines[i].globalLineIndex);
if (!d.found) continue;
float top = d.boundingBox.y, bot = top + d.boundingBox.height;
if (point.y >= top && point.y < bot) { bestLine = i; bestDist = 0.0f; break; }
float dist = point.y < top ? (top - point.y) : (point.y - bot);
if (dist < bestDist) { bestDist = dist; bestLine = i; }
}
if (bestLine < 0) {
r.blockIndex = doc->count - 1;
r.offset = doc->blocks[r.blockIndex].text.len;
return r;
}
r.blockIndex = cache->lines[bestLine].blockIndex;
r.offset = find_offset_in_line(cache, doc, fonts, bestLine, point.x);
return r;
}
VerticalMoveResult hittest_vertical(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int blockIndex, int offset, float preferredX, int dir) {
VerticalMoveResult r = {0, 0, 0};
int curLine = find_line_for_offset(cache, blockIndex, offset);
if (curLine < 0) return r;
int targetLine = curLine + (dir < 0 ? -1 : 1);
if (targetLine < 0 || targetLine >= cache->lineCount) return r;
r.found = 1;
r.blockIndex = cache->lines[targetLine].blockIndex;
r.offset = find_offset_in_line(cache, doc, fonts, targetLine, preferredX);
return r;
}
int hittest_offset_for_block_x(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int blockIndex, float targetX) {
for (int i = 0; i < cache->lineCount; i++) {
if (cache->lines[i].blockIndex == blockIndex) {
return find_offset_in_line(cache, doc, fonts, i, targetX);
}
}
return 0;
}
/* Screen-space x of byte `offset` within visual line `line` (whose Clay element data,
`lineData`, the caller already has). Shared by hittest_caret and hittest_line_range_box. */
static float x_at_offset_in_line(LayoutCache *cache, Document *doc, SDL2_Font *fonts, CachedLine *line, Clay_ElementData lineData, int offset) {
if (line->segStart == line->segEnd) return lineData.boundingBox.x;
int bestSeg = -1;
for (int i = line->segStart; i < line->segEnd; i++) {
if (cache->segs[i].srcStart <= offset) bestSeg = i;
}
if (bestSeg < 0) bestSeg = line->segStart;
CachedSegment *seg = &cache->segs[bestSeg];
Clay_ElementData sd = get_seg_data(seg->globalSegIndex);
if (!sd.found) return lineData.boundingBox.x;
int within = offset - seg->srcStart;
if (within < 0) within = 0;
if (within > seg->srcEnd - seg->srcStart) within = seg->srcEnd - seg->srcStart;
const char *text = doc->blocks[line->blockIndex].text.data;
float w = measure_width(fonts, seg->fontId, seg->fontSize, text, seg->srcStart, within);
return sd.boundingBox.x + w;
}
int hittest_caret(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int blockIndex, int offset, float *outX, float *outY, float *outHeight) {
int lineIdx = find_line_for_offset(cache, blockIndex, offset);
if (lineIdx < 0) return 0;
CachedLine *line = &cache->lines[lineIdx];
Clay_ElementData ld = get_line_data(line->globalLineIndex);
if (!ld.found) return 0;
*outY = ld.boundingBox.y;
*outHeight = ld.boundingBox.height;
*outX = x_at_offset_in_line(cache, doc, fonts, line, ld, offset);
return 1;
}
int hittest_line_range_box(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int lineIdx,
int rangeStart, int rangeEnd, float *outX, float *outY, float *outW, float *outHeight) {
if (lineIdx < 0 || lineIdx >= cache->lineCount) return 0;
CachedLine *line = &cache->lines[lineIdx];
int lo = rangeStart < line->srcStart ? line->srcStart : rangeStart;
int hi = rangeEnd > line->srcEnd ? line->srcEnd : rangeEnd;
if (hi < lo) return 0;
Clay_ElementData ld = get_line_data(line->globalLineIndex);
if (!ld.found) return 0;
float x0 = x_at_offset_in_line(cache, doc, fonts, line, ld, lo);
float x1 = x_at_offset_in_line(cache, doc, fonts, line, ld, hi);
float w = x1 - x0;
if (w < 4.0f) w = 4.0f; /* visible sliver for a fully-selected blank line */
*outX = x0;
*outY = ld.boundingBox.y;
*outW = w;
*outHeight = ld.boundingBox.height;
return 1;
}