Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 0 additions & 31 deletions src/ui/components/fullscreen_overlay.zig
Original file line number Diff line number Diff line change
Expand Up @@ -263,37 +263,6 @@ pub const FullscreenOverlay = struct {
}
}

pub fn renderScrollbar(self: *const FullscreenOverlay, renderer: *c.SDL_Renderer, host: *const types.UiHost, rect: geom.Rect, title_h: c_int, content_height: f32, viewport_height: f32) void {
if (content_height <= viewport_height) return;

const scrollbar_width = dpi.scale(6, host.ui_scale);
const scrollbar_margin = dpi.scale(4, host.ui_scale);
const track_height = rect.h - title_h - scrollbar_margin * 2;
const thumb_ratio = viewport_height / content_height;
const thumb_height: c_int = @max(dpi.scale(20, host.ui_scale), @as(c_int, @intFromFloat(@as(f32, @floatFromInt(track_height)) * thumb_ratio)));
const scroll_ratio = if (self.max_scroll > 0) self.scroll_offset / self.max_scroll else 0;
const thumb_y: c_int = @intFromFloat(@as(f32, @floatFromInt(track_height - thumb_height)) * scroll_ratio);

_ = c.SDL_SetRenderDrawBlendMode(renderer, c.SDL_BLENDMODE_BLEND);
const bar_alpha = self.render_alpha;
_ = c.SDL_SetRenderDrawColor(renderer, 128, 128, 128, @intFromFloat(30.0 * bar_alpha));
_ = c.SDL_RenderFillRect(renderer, &c.SDL_FRect{
.x = @floatFromInt(rect.x + rect.w - scrollbar_width - scrollbar_margin),
.y = @floatFromInt(rect.y + title_h + scrollbar_margin),
.w = @floatFromInt(scrollbar_width),
.h = @floatFromInt(track_height),
});

const accent_col = host.theme.accent;
_ = c.SDL_SetRenderDrawColor(renderer, accent_col.r, accent_col.g, accent_col.b, @intFromFloat(120.0 * bar_alpha));
_ = c.SDL_RenderFillRect(renderer, &c.SDL_FRect{
.x = @floatFromInt(rect.x + rect.w - scrollbar_width - scrollbar_margin),
.y = @floatFromInt(rect.y + title_h + scrollbar_margin + thumb_y),
.w = @floatFromInt(scrollbar_width),
.h = @floatFromInt(thumb_height),
});
}

/// Render a title texture centered vertically in the title area.
pub fn renderTitle(self: *const FullscreenOverlay, renderer: *c.SDL_Renderer, rect: geom.Rect, title_tex: *c.SDL_Texture, title_w: c_int, title_h: c_int, host: *const types.UiHost) void {
const scaled_title_h = dpi.scale(title_height, host.ui_scale);
Expand Down
100 changes: 92 additions & 8 deletions src/ui/components/story_overlay.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const font_cache_mod = @import("../../font_cache.zig");
const open_url = @import("../../os/open.zig");
const markdown_parser = @import("markdown_parser.zig");
const markdown_renderer = @import("markdown_renderer.zig");
const scrollbar = @import("scrollbar.zig");

const log = std.log.scoped(.story_overlay);

Expand Down Expand Up @@ -50,6 +51,7 @@ const AnchorPosition = struct {
pub const StoryOverlayComponent = struct {
allocator: std.mem.Allocator,
overlay: FullscreenOverlay = .{},
scrollbar_state: scrollbar.State = .{},

raw_content: ?[]u8 = null,
blocks: std.ArrayList(markdown_parser.DisplayBlock) = .{},
Expand Down Expand Up @@ -126,6 +128,7 @@ pub const StoryOverlayComponent = struct {

pub fn hide(self: *StoryOverlayComponent, now_ms: i64) void {
self.overlay.hide(now_ms);
self.scrollbar_state.hideNow();
self.hovered_anchor = null;
self.search_active = false;
self.selected_match = null;
Expand Down Expand Up @@ -344,7 +347,10 @@ pub const StoryOverlayComponent = struct {
return true;
}

if (self.overlay.handleScrollKey(key, host)) return true;
if (self.overlay.handleScrollKey(key, host)) {
self.scrollbar_state.noteActivity(host.now_ms);
return true;
}

return true;
},
Expand All @@ -360,6 +366,7 @@ pub const StoryOverlayComponent = struct {
},
c.SDL_EVENT_MOUSE_WHEEL => {
self.overlay.handleMouseWheel(event.wheel.y);
self.scrollbar_state.noteActivity(host.now_ms);
return true;
},
c.SDL_EVENT_MOUSE_BUTTON_DOWN => {
Expand All @@ -379,6 +386,33 @@ pub const StoryOverlayComponent = struct {
}

if (event.button.button == c.SDL_BUTTON_LEFT) {
const title_h = dpi.scale(FullscreenOverlay.title_height, host.ui_scale);
const content_rect = geom.Rect{
.x = overlay_rect.x,
.y = overlay_rect.y + title_h,
.w = overlay_rect.w,
.h = overlay_rect.h - title_h,
};
const scroll_metrics = scrollbar.Metrics.init(
self.totalContentHeight(host),
self.overlay.scroll_offset,
@floatFromInt(@max(0, content_rect.h)),
);
if (scrollbar.computeLayout(content_rect, host.ui_scale, scroll_metrics)) |layout| {
switch (scrollbar.hitTest(layout, mouse_x, mouse_y)) {
.thumb => {
self.scrollbar_state.beginDrag(layout, mouse_y, host.now_ms);
return true;
},
.track => {
self.overlay.scroll_offset = scrollbar.offsetForTrackClick(layout, scroll_metrics, mouse_y);
self.scrollbar_state.noteActivity(host.now_ms);
return true;
},
.none => {},
}
}

if (self.linkHitIndexAt(mouse_x, mouse_y)) |hit_idx| {
const href = self.link_hits.items[hit_idx].href;
open_url.openUrl(self.allocator, href) catch |err| {
Expand All @@ -390,19 +424,53 @@ pub const StoryOverlayComponent = struct {

return true;
},
c.SDL_EVENT_MOUSE_BUTTON_UP => {
if (event.button.button == c.SDL_BUTTON_LEFT and self.scrollbar_state.dragging) {
self.scrollbar_state.endDrag(host.now_ms);
}
return true;
},
c.SDL_EVENT_MOUSE_MOTION => {
const mouse_x: c_int = @intFromFloat(event.motion.x);
const mouse_y: c_int = @intFromFloat(event.motion.y);
self.overlay.updateCloseHover(mouse_x, mouse_y, host);

const overlay_rect = FullscreenOverlay.overlayRect(host);
const title_h = dpi.scale(FullscreenOverlay.title_height, host.ui_scale);
const content_rect = geom.Rect{
.x = overlay_rect.x,
.y = overlay_rect.y + title_h,
.w = overlay_rect.w,
.h = overlay_rect.h - title_h,
};
const scroll_metrics = scrollbar.Metrics.init(
self.totalContentHeight(host),
self.overlay.scroll_offset,
@floatFromInt(@max(0, content_rect.h)),
);
const scroll_layout = scrollbar.computeLayout(content_rect, host.ui_scale, scroll_metrics);

if (self.scrollbar_state.dragging) {
if (scroll_layout) |layout| {
self.overlay.scroll_offset = scrollbar.offsetForDrag(&self.scrollbar_state, layout, scroll_metrics, mouse_y);
self.scrollbar_state.noteActivity(host.now_ms);
} else {
self.scrollbar_state.endDrag(host.now_ms);
}
}

const scroll_hit = if (scroll_layout) |layout| scrollbar.hitTest(layout, mouse_x, mouse_y) else .none;
const was_scrollbar = self.scrollbar_state.hovered or self.scrollbar_state.dragging;
self.scrollbar_state.setHovered(self.scrollbar_state.dragging or scroll_hit != .none, host.now_ms);

const prev_hovered_anchor = self.hovered_anchor;
self.updateAnchorHover(mouse_x, mouse_y, host);

const prev_link = self.hovered_link;
self.hovered_link = self.linkHitIndexAt(mouse_x, mouse_y);

const want_pointer = self.hovered_anchor != null or self.hovered_link != null;
const was_pointer = prev_hovered_anchor != null or prev_link != null;
const want_pointer = self.hovered_anchor != null or self.hovered_link != null or self.scrollbar_state.dragging or scroll_hit != .none;
const was_pointer = prev_hovered_anchor != null or prev_link != null or was_scrollbar;
if (want_pointer != was_pointer) {
const cursor = if (want_pointer) self.pointer_cursor else self.arrow_cursor;
if (cursor) |cur| _ = c.SDL_SetCursor(cur);
Expand All @@ -417,6 +485,7 @@ pub const StoryOverlayComponent = struct {
fn updateFn(self_ptr: *anyopaque, host: *const types.UiHost, _: *types.UiActionQueue) void {
const self: *StoryOverlayComponent = @ptrCast(@alignCast(self_ptr));
_ = self.overlay.updateAnimation(host.now_ms);
self.scrollbar_state.update(host.now_ms);
if (!self.overlay.visible) return;

const new_wrap = self.computeWrapCols(host);
Expand All @@ -431,9 +500,12 @@ pub const StoryOverlayComponent = struct {
return self.overlay.hitTest(host, x, y);
}

fn wantsFrameFn(self_ptr: *anyopaque, _: *const types.UiHost) bool {
fn wantsFrameFn(self_ptr: *anyopaque, host: *const types.UiHost) bool {
const self: *StoryOverlayComponent = @ptrCast(@alignCast(self_ptr));
return self.overlay.wantsFrame() or self.hovered_anchor != null or self.hovered_link != null;
return self.overlay.wantsFrame() or
self.scrollbar_state.wantsFrame(host.now_ms) or
self.hovered_anchor != null or
self.hovered_link != null;
}

// --- Anchor hover ---
Expand Down Expand Up @@ -466,8 +538,7 @@ pub const StoryOverlayComponent = struct {
_ = self;
const rect = FullscreenOverlay.overlayRect(host);
const scaled_padding = dpi.scale(FullscreenOverlay.text_padding, host.ui_scale);
const scrollbar_w = dpi.scale(10, host.ui_scale);
const text_area_w = rect.w - scaled_padding * 2 - scrollbar_w;
const text_area_w = rect.w - scaled_padding * 2 - scrollbar.reservedWidth(host.ui_scale);
if (text_area_w <= 0) return 80;

const estimated_char_w: c_int = dpi.scale(8, host.ui_scale);
Expand Down Expand Up @@ -572,7 +643,19 @@ pub const StoryOverlayComponent = struct {

_ = c.SDL_SetRenderClipRect(renderer, null);

self.overlay.renderScrollbar(renderer, host, overlay_rect, title_h, content_height, viewport_height);
const scroll_metrics = scrollbar.Metrics.init(content_height, self.overlay.scroll_offset, viewport_height);
const content_rect = geom.Rect{
.x = overlay_rect.x,
.y = overlay_rect.y + title_h,
.w = overlay_rect.w,
.h = overlay_rect.h - title_h,
};
if (scrollbar.computeLayout(content_rect, host.ui_scale, scroll_metrics)) |layout| {
scrollbar.render(renderer, layout, host.theme.accent, &self.scrollbar_state);
self.scrollbar_state.markDrawn();
} else {
self.scrollbar_state.hideNow();
}
self.overlay.first_frame.markDrawn();
}

Expand Down Expand Up @@ -1236,6 +1319,7 @@ pub const StoryOverlayComponent = struct {

fn destroy(self: *StoryOverlayComponent, renderer: *c.SDL_Renderer) void {
_ = renderer;
self.scrollbar_state.deinit();
self.clearContent();
self.blocks.deinit(self.allocator);
self.lines.deinit(self.allocator);
Expand Down