package mujica import ( "fmt" "log/slog" "time" "github.com/egoist/mygo/ui" "mujicaui/icons" "mujicaui/internal" "mujicaui/theme" ) // StripFrame is one thumbnail of a VideoThumbnailStrip. type StripFrame struct { // Image is a bitmap or SVG; nil leaves it empty. Image ui.ImageSource At time.Duration } // VideoThumbnailStripOptions configure a VideoThumbnailStrip. type VideoThumbnailStripOptions struct { // FrameWidth is each cell's width; zero means 96. FrameWidth float32 Disabled bool } const stripGap = 4 type stripKey struct{} // VideoThumbnailStrip shows the frames fitting around *current. func VideoThumbnailStrip(c *ui.Context, current *int, frames []StripFrame, opts VideoThumbnailStripOptions) ChoiceResult { if len(frames) == 0 || *current < 0 || *current >= len(frames) { panic("mujica: VideoThumbnailStrip current out of range") } if opts.FrameWidth < 0 { panic("mujica: VideoThumbnailStrip FrameWidth must not be negative") } fw := opts.FrameWidth if fw == 0 { fw = 96 } fh := fw * 9 / 16 k := Tokens(c) old := *current row := ui.Row(c).Gap(Density(c).Unit()).AlignItems(ui.Center).Disabled(opts.Disabled) row.Children(func() { prev := iconAction(c, icons.Must("chevron-left"), msg(c, "video_thumbnail_strip.previous")).Disabled(*current == 0) view := ui.Row(c).Grow(1).MinWidth(fw).Height(fh + 4).Gap(stripGap).Clip().Focusable(). Role(ui.RoleGroup).Label(msg(c, "video_thumbnail_strip.label")). Description(fmt.Sprintf(msg(c, "video_thumbnail_strip.position"), *current+1, len(frames), mediaClock(frames[*current].At))) internal.Ring(view, k.Focus, theme.ControlRadius) next := iconAction(c, icons.Must("chevron-right"), msg(c, "video_thumbnail_strip.next")).Disabled(*current == len(frames)-1) live := !row.IsDisabled() switch { case live && (prev.Clicked() || view.Shortcut(0, ui.KeyLeft)): *current = max(0, *current-1) case live && (next.Clicked() || view.Shortcut(0, ui.KeyRight)): *current = min(len(frames)-1, *current+1) case live && view.Shortcut(0, ui.KeyHome): *current = 0 case live && view.Shortcut(0, ui.KeyEnd): *current = len(frames) - 1 } // Only frames that fit are built, around *current. w := view.Bounds().W if w <= 0 { // The count needs the width: lay out, then rebuild. c.AnimationFrame() } n := max(1, int((w+stripGap)/(fw+stripGap))) start := ui.Local(view, stripKey{}, func() int { return 0 }) *start = max(0, min(*start, *current, len(frames)-n)) if *current >= *start+n { *start = *current - n + 1 } view.Children(func() { for i := *start; i < min(len(frames), *start+n); i++ { if stripCell(c, k, frames[i], i, i == *current, fw, fh) && live { *current = i } } }) }) changed := *current != old if changed { slog.Debug("mujica: video_strip jump", "index", *current) } return ChoiceResult{Element: row, changed: changed} } // stripCell draws one frame and reports a click on it. func stripCell(c *ui.Context, k theme.Tokens, f StripFrame, i int, on bool, fw, fh float32) bool { cell := ui.Box(c).Key(i).Size(fw, fh).Shrink(0).Radius(theme.ControlRadius).Clip().Background(videoBlack). Cursor(ui.CursorPointer).AlignSelf(ui.Center) edge, width := k.Border, theme.BorderWidth if on { edge, width = k.AccentText, 2 } cell.Border(width, edge) cell.Children(func() { if f.Image != nil { ui.Image(c, f.Image).Fit(ui.Cover).Size(fw, fh) } ui.Text(c, mediaClock(f.At)).Absolute().Left(4).Bottom(3).FontSize(FontSize(c, theme.CaptionSize)). TextColor(ui.Hex("#F2EBDD")).Background(ui.RGBA(0, 0, 0, 0.6)).FontFeatures("tnum") if on { ui.Box(c).Absolute().Top(3).Right(3).Size(10, 10).Draw(func(p *ui.Painter, r ui.Rect) { p.FillPath(internal.Diamond(internal.Snap(p, r.X+5), internal.Snap(p, r.Y+5), 10), k.AccentText) }) } }) return cell.Clicked() }