Upgrade MyGo to v0.5.5
MyGo v0.5.5 returns ui.Element as a checked value handle instead of a
pointer. Migrated with `mygo migrate-ui`, then fixed what the codemod
leaves to review:
- Element nil checks become .Valid(); absent elements are ui.Element{}.
- Input handlers and paint callbacks run outside the build pass, where
elements expire and Context methods are inert: read IsDisabled into a
local during the build, keep a ui.Handle for Focus, use c.Services()
for clipboard access, create ui.Local state during the build.
- Add core.AutoFocus: v0.5.5 records an overlay's opener after its
content builds, so same-frame autofocus lost focus restore on close.
It focuses one frame later; cascader rows defer their focus likewise.
- core.OverlayLayer identifies build passes by the root element, not
c.Now(), which repeats across passes and under the Tester clock.
- Context menus survive the opening press counted as PressedOutside;
split button tooltip no longer reorders its open menu; disabled
menubar rows pass presses to the panel; Form defers blur-started
async checks so the growing message no longer moves Submit away
from the release; Combobox drops its disabled copy now that MyGo
blocks typing under a disabled wrapper (canary flipped to a guard).
- The Tester clock is virtual: tests use Advance/Now instead of
time.Sleep/time.Now and read element state inside the view. The web
mirror advances the clock to wall time before each frame and input.
- AGENTS.md and docs describe the value Element API and the new pin.
Snapshot goldens are darwin-only and were not re-verified here.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bc716d27be
commit
bcce34cda1
603 files changed
+1366
-1277
No files matched your search
@@ -38,7 +38,7 @@ func ArtifactCard(c *ui.Context, opts ArtifactCardOptions) core.ChoiceResult {
|
||||
k := core.Tokens(c)
|
||||
d := core.Density(c)
|
||||
kind := artifactKinds[opts.Kind]
|
||||
var b *ui.Element
|
||||
var b ui.Element
|
||||
ui.Row(c).Shrink(0).Disabled(opts.Disabled).Children(func() {
|
||||
b = ui.ButtonBase(c).MinWidth(260).MaxWidth(420).MinHeight(56).Padding(d.Unit()*2, d.Unit()*3).Gap(d.Unit()*3).
|
||||
Justify(ui.Start).Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).
|
||||
|
||||
@@ -28,7 +28,7 @@ type Attachment struct {
|
||||
|
||||
// AttachmentChipResult carries the chip and requested action.
|
||||
type AttachmentChipResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
removed, retried bool
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ type AudioMessageOptions struct {
|
||||
|
||||
// AudioMessageResult is an AudioMessage's element and the user's requests this frame.
|
||||
type AudioMessageResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
toggled bool
|
||||
seek time.Duration
|
||||
seeked bool
|
||||
|
||||
@@ -26,7 +26,7 @@ type CapabilityCardsOptions struct {
|
||||
|
||||
// CapabilityCardsView is built CapabilityCards and the example clicked this frame.
|
||||
type CapabilityCardsView struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Example is the example prompt chosen, empty for none.
|
||||
Example string
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func CapabilityCards(c *ui.Context, cards []CapabilityCard, opts CapabilityCards
|
||||
if card.Title == "" {
|
||||
panic("mujica: CapabilityCard needs a Title")
|
||||
}
|
||||
box := ui.Column(c).Key(i).Gap(d.Unit()*2).Padding(d.Unit()*4).Background(k.Surface).
|
||||
box := ui.Column(c.Key(i)).Gap(d.Unit()*2).Padding(d.Unit()*4).Background(k.Surface).
|
||||
Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius).Role(ui.RoleGroup).Label(card.Title)
|
||||
box.Children(func() {
|
||||
ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() {
|
||||
|
||||
@@ -35,10 +35,10 @@ func TestCapabilityCardsExample(t *testing.T) {
|
||||
|
||||
func TestCapabilityCardsColumnsAndDisabled(t *testing.T) {
|
||||
got := ""
|
||||
var g *ui.Element
|
||||
built := false
|
||||
tt := chatTester(900, 600, func(c *ui.Context) {
|
||||
v := CapabilityCards(c, chatCards(), CapabilityCardsOptions{Columns: 1, Disabled: true})
|
||||
g = v.Element
|
||||
built = v.Element.Valid()
|
||||
if v.Example != "" {
|
||||
got = v.Example
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func TestCapabilityCardsColumnsAndDisabled(t *testing.T) {
|
||||
}
|
||||
a, _ := tt.Find("Write")
|
||||
b, _ := tt.Find("Plan")
|
||||
if b.Y <= a.Y || g == nil {
|
||||
if b.Y <= a.Y || !built {
|
||||
t.Fatalf("one column should stack cards: %v %v", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
+14
-9
@@ -28,15 +28,19 @@ type CitationState struct {
|
||||
Active int
|
||||
hover int
|
||||
owner uint64
|
||||
seen time.Time
|
||||
locate uint64
|
||||
// owned marks the hover owner built in this pass; painted, a paint since the last build.
|
||||
owned, painted bool
|
||||
locate uint64
|
||||
}
|
||||
|
||||
// lit reports whether source n is chosen or hovered anywhere.
|
||||
func (s *CitationState) lit(n int) bool { return n > 0 && (s.Active == n || s.hover == n) }
|
||||
|
||||
// track refreshes pointer ownership for the current frame.
|
||||
func (s *CitationState) track(c *ui.Context, e *ui.Element, n int) {
|
||||
func (s *CitationState) track(c *ui.Context, e ui.Element, n int) {
|
||||
if s.painted { // the first build after a paint starts a pass
|
||||
s.painted, s.owned = false, false
|
||||
}
|
||||
switch over := e.Hovered() && !e.IsDisabled(); {
|
||||
case over && s.hover != n:
|
||||
s.hover, s.owner = n, e.ID()
|
||||
@@ -46,12 +50,14 @@ func (s *CitationState) track(c *ui.Context, e *ui.Element, n int) {
|
||||
c.Invalidate()
|
||||
}
|
||||
if s.owner == e.ID() {
|
||||
s.seen = c.Now()
|
||||
s.owned = true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CitationState) expire(c *ui.Context, frame time.Time) {
|
||||
if s.hover != 0 && s.seen != frame {
|
||||
// expire drops a hover whose owner this pass did not build; it runs while painting.
|
||||
func (s *CitationState) expire(c *ui.Context) {
|
||||
s.painted = true
|
||||
if s.hover != 0 && !s.owned {
|
||||
s.hover, s.owner = 0, 0
|
||||
c.Invalidate()
|
||||
}
|
||||
@@ -73,9 +79,8 @@ func CitationBadge(c *ui.Context, n int, src Source, st *CitationState) core.Cho
|
||||
} else {
|
||||
b.Background(k.SurfaceHover).Border(theme.BorderWidth, k.Border).TextColor(k.TextMuted)
|
||||
}
|
||||
frame := c.Now()
|
||||
b.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
st.expire(c, frame)
|
||||
st.expire(c)
|
||||
if st.lit(n) {
|
||||
p.Fill(ui.Rect{X: r.X + 4, Y: r.Y + r.H - 4, W: r.W - 8, H: 2}, k.AccentText, 0)
|
||||
}
|
||||
@@ -96,7 +101,7 @@ const citationPreviewDelay = 500 * time.Millisecond
|
||||
|
||||
// citationPreview displays a source after resting hover or focus.
|
||||
// Pass-through preserves badge clicks; Escape dismisses the preview.
|
||||
func citationPreview(c *ui.Context, b *ui.Element, n int, src Source) {
|
||||
func citationPreview(c *ui.Context, b ui.Element, n int, src Source) {
|
||||
st := ui.Local(b, "mujica.citation", func() overlay.TooltipState { return overlay.TooltipState{} })
|
||||
size := ui.Local(b, "mujica.citation.size", func() ui.Rect { return ui.Rect{} })
|
||||
if b.IsDisabled() || !b.Hovered() && !b.FocusVisible() {
|
||||
|
||||
@@ -50,8 +50,7 @@ func TestCitationBadgeActivatesAndPreviews(t *testing.T) {
|
||||
if a.st.hover != 3 {
|
||||
t.Fatalf("hover %d, want 3", a.st.hover)
|
||||
}
|
||||
time.Sleep(600 * time.Millisecond)
|
||||
tt.Frame()
|
||||
tt.Advance(600 * time.Millisecond)
|
||||
if n := len(tt.Texts()); n == 0 || !tt.HasText("example.net") {
|
||||
t.Fatalf("no preview card: %q", tt.Texts())
|
||||
}
|
||||
@@ -114,8 +113,7 @@ func TestCitationBadgeHitAreaAndDisabledPreview(t *testing.T) {
|
||||
t.Errorf("hit area %v, want at least 28x28", b)
|
||||
}
|
||||
tt.Key(0, ui.KeyTab)
|
||||
time.Sleep(550 * time.Millisecond)
|
||||
tt.Frame()
|
||||
tt.Advance(550 * time.Millisecond)
|
||||
off = true
|
||||
tt.Frame()
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
@@ -133,8 +131,7 @@ func TestCitationBadgePreviewFitsNarrowWindow(t *testing.T) {
|
||||
ui.Row(c).Padding(8).Children(func() { CitationBadge(c, 1, src, &st) })
|
||||
}, 180, 120)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
time.Sleep(550 * time.Millisecond)
|
||||
tt.Frame()
|
||||
tt.Advance(550 * time.Millisecond)
|
||||
preview, ok := tt.Find("Source preview")
|
||||
if !ok || preview.X < 0 || preview.X+preview.W > 180 || preview.Y+preview.H > 120 {
|
||||
t.Fatalf("preview content outside window: %v found %v", preview, ok)
|
||||
@@ -147,12 +144,12 @@ func TestCitationBadgeRemovedOwnerClearsHover(t *testing.T) {
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
core.Use(c, core.Settings{})
|
||||
ui.Column(c).Children(func() {
|
||||
ui.Row(c).Key("badge").Height(36).Children(func() {
|
||||
ui.Row(c.Key("badge")).Height(36).Children(func() {
|
||||
if show {
|
||||
CitationBadge(c, 1, sourcesSample[0], &st)
|
||||
}
|
||||
})
|
||||
ui.Row(c).Key("card").Width(300).Children(func() { SourceCard(c, 1, sourcesSample[0], &st) })
|
||||
ui.Row(c.Key("card")).Width(300).Children(func() { SourceCard(c, 1, sourcesSample[0], &st) })
|
||||
})
|
||||
}, 400, 300)
|
||||
b, _ := tt.Find("Source 1: The Palace of Versailles")
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
// CopyResult is a block's element and whether its copy action ran this frame.
|
||||
type CopyResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
copied bool
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func CodeBlock(c *ui.Context, code string, opts CodeBlockOptions) CopyResult {
|
||||
if st.collapsed {
|
||||
return
|
||||
}
|
||||
var body *ui.Element
|
||||
var body ui.Element
|
||||
if st.wrap {
|
||||
body = ui.Column(c).Padding(u*2, u*3).MinWidth(0)
|
||||
} else {
|
||||
|
||||
@@ -20,7 +20,7 @@ type ContextChipsOptions struct {
|
||||
|
||||
// RemoveResult carries the element and removed item ID.
|
||||
type RemoveResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
removed string
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func ContextChips(c *ui.Context, items []ContextItem, opts ContextChipsOptions)
|
||||
shown = min(shown, limit)
|
||||
}
|
||||
for _, it := range items[:shown] {
|
||||
ui.Row(c).Key(it.ID).Gap(u).AlignItems(ui.Center).Padding(0, 0, 0, u*2).MinHeight(core.Density(c).ControlHeight()).
|
||||
ui.Row(c.Key(it.ID)).Gap(u).AlignItems(ui.Center).Padding(0, 0, 0, u*2).MinHeight(core.Density(c).ControlHeight()).
|
||||
Radius(theme.ControlRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).Role(ui.RoleListItem).
|
||||
Children(func() {
|
||||
ui.Icon(c, icons.Must(it.Kind.icon())).Size(14, 14).Shrink(0).TextColor(k.AccentText)
|
||||
|
||||
@@ -53,7 +53,7 @@ type ContextMentionOptions struct {
|
||||
|
||||
// ContextMentionResult carries the popup, query and chosen item.
|
||||
type ContextMentionResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
query string
|
||||
queryChanged bool
|
||||
chosen ContextItem
|
||||
@@ -70,7 +70,7 @@ func (r ContextMentionResult) Chosen() (ContextItem, bool) { return r.chosen, r.
|
||||
// ContextMentionMenu groups results for the active mention query.
|
||||
// The caller filters results synchronously or asynchronously.
|
||||
func ContextMentionMenu(c *ui.Context, composer input.InputResult, value *string, items []ContextItem, opts ContextMentionOptions) ContextMentionResult {
|
||||
if composer.Element == nil || composer.Input == nil || value == nil {
|
||||
if !composer.Element.Valid() || !composer.Input.Valid() || value == nil {
|
||||
panic("mujica: ContextMentionMenu needs composer and value")
|
||||
}
|
||||
data := ui.Local(composer.Element, "mention-data", func() mentionMenuData { return mentionMenuData{} })
|
||||
|
||||
@@ -26,7 +26,7 @@ type ContextWindowMeterOptions struct {
|
||||
|
||||
// ContextWindowMeter displays segmented usage and threshold warnings.
|
||||
// Palette colors and shaped legend markers identify segments.
|
||||
func ContextWindowMeter(c *ui.Context, segments []ContextSegment, limit int, opts ContextWindowMeterOptions) *ui.Element {
|
||||
func ContextWindowMeter(c *ui.Context, segments []ContextSegment, limit int, opts ContextWindowMeterOptions) ui.Element {
|
||||
if limit <= 0 || opts.Warn < 0 || opts.Warn > 1 {
|
||||
panic("mujica: ContextWindowMeter needs limit > 0 and 0 <= Warn <= 1")
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ type ConversationExportOptions struct {
|
||||
|
||||
// ConversationExportView is a built ConversationExport and the export asked for.
|
||||
type ConversationExportView struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Exported reports Content is ready for the caller to write.
|
||||
Exported bool
|
||||
Content string
|
||||
@@ -172,13 +172,13 @@ func ConversationExport(c *ui.Context, s *ConversationExportState, messages []Se
|
||||
v := ConversationExportView{Element: col, Format: s.Format}
|
||||
content := conversationExportContent(s, messages, opts)
|
||||
col.Children(func() {
|
||||
input.FormField(c, core.Msg(c, "conversation_export.format"), input.FormFieldOptions{}, func() *ui.Element {
|
||||
input.FormField(c, core.Msg(c, "conversation_export.format"), input.FormFieldOptions{}, func() ui.Element {
|
||||
return input.RadioGroup(c, &s.Format, []input.RadioItem[ExportFormat]{
|
||||
{Value: ExportMarkdown, Label: "Markdown"}, {Value: ExportJSON, Label: "JSON"},
|
||||
{Value: ExportText, Label: core.Msg(c, "conversation_export.text")},
|
||||
}, input.RadioGroupOptions{Horizontal: true, Label: core.Msg(c, "conversation_export.format"), Disabled: opts.Disabled}).Element
|
||||
})
|
||||
input.FormField(c, core.Msg(c, "conversation_export.range"), input.FormFieldOptions{}, func() *ui.Element {
|
||||
input.FormField(c, core.Msg(c, "conversation_export.range"), input.FormFieldOptions{}, func() ui.Element {
|
||||
return ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Children(func() {
|
||||
input.RadioGroup(c, &s.Range, []input.RadioItem[ExportRange]{
|
||||
{Value: ExportAll, Label: fmt.Sprintf(core.Msg(c, "conversation_export.all"), len(messages))},
|
||||
|
||||
@@ -29,7 +29,7 @@ type ConversationDrag struct{ ID string }
|
||||
|
||||
// ConversationItemView is a built ConversationItem and its requests.
|
||||
type ConversationItemView struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Opened, Deleted, Exported and PinToggled are requests for the caller.
|
||||
Opened, Deleted, Exported, PinToggled bool
|
||||
// Renamed reports a confirmed new title, already in conv.Title.
|
||||
@@ -54,7 +54,7 @@ func ConversationItem(c *ui.Context, conv *ChatConversation, opts ConversationIt
|
||||
if today.IsZero() {
|
||||
today = core.DateOf(c.Now())
|
||||
}
|
||||
row := ui.Row(c).Key(conv.ID).FillWidth().MinHeight(d.ControlHeight()).Gap(d.Unit()).AlignItems(ui.Center).
|
||||
row := ui.Row(c.Key(conv.ID)).FillWidth().MinHeight(d.ControlHeight()).Gap(d.Unit()).AlignItems(ui.Center).
|
||||
Radius(theme.ControlRadius).Disabled(opts.Disabled)
|
||||
v := ConversationItemView{Element: row}
|
||||
st := ui.Local(row, conversationItemKey{}, func() conversationItemState { return conversationItemState{} })
|
||||
@@ -145,7 +145,7 @@ func conversationItemEditor(c *ui.Context, k theme.Tokens, conv *ChatConversatio
|
||||
}
|
||||
|
||||
// conversationItemMenu is a borderless ellipsis button opening a menu.
|
||||
func conversationItemMenu(c *ui.Context, label string, build func(m *overlay.PopupMenu)) *ui.Element {
|
||||
func conversationItemMenu(c *ui.Context, label string, build func(m *overlay.PopupMenu)) ui.Element {
|
||||
b := core.IconAction(c, icons.Must("ellipsis"), label).Role(ui.RoleMenuButton).Shrink(0)
|
||||
open := ui.Local(b, conversationItemKey{}, func() bool { return false })
|
||||
if b.IsDisabled() {
|
||||
|
||||
@@ -54,7 +54,7 @@ type conversationListRow struct {
|
||||
|
||||
// ConversationListView is a built ConversationList and this frame's events.
|
||||
type ConversationListView struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
changed bool
|
||||
submitted bool
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func ConversationList(c *ui.Context, s *ConversationListState, items []ChatConve
|
||||
s.rows.Header = func(i int) bool { return rows[i].item < 0 }
|
||||
s.rows.Label = func(i int) string { return conversationListTitle(c, items, rows[i]) }
|
||||
s.rows.Selected = cursor
|
||||
var list *ui.Element
|
||||
var list ui.Element
|
||||
wrap := ui.Column(c).MinHeight(0).Disabled(opts.Disabled)
|
||||
wrap.Children(func() {
|
||||
restore := data.DataSelectTheme(c, 0)
|
||||
|
||||
@@ -34,7 +34,7 @@ type ConversationSearchOptions struct {
|
||||
|
||||
// ConversationSearchView is a built ConversationSearch and this frame's events.
|
||||
type ConversationSearchView struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// QueryChanged asks the caller to search for the new query.
|
||||
QueryChanged bool
|
||||
// Chosen is the hit opened this frame, or empty.
|
||||
|
||||
@@ -32,7 +32,7 @@ func EstimateCost(inputTokens, outputTokens int, inputPrice, outputPrice float64
|
||||
}
|
||||
|
||||
// CostEstimator shows token counts and cost at caller prices.
|
||||
func CostEstimator(c *ui.Context, opts CostEstimatorOptions) *ui.Element {
|
||||
func CostEstimator(c *ui.Context, opts CostEstimatorOptions) ui.Element {
|
||||
if opts.InputTokens < 0 || opts.OutputTokens < 0 {
|
||||
panic("mujica: CostEstimator token counts must not be negative")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type DateSeparatorOptions struct {
|
||||
}
|
||||
|
||||
// DateSeparator marks where a day's messages begin, localized.
|
||||
func DateSeparator(c *ui.Context, day core.Date, opts DateSeparatorOptions) *ui.Element {
|
||||
func DateSeparator(c *ui.Context, day core.Date, opts DateSeparatorOptions) ui.Element {
|
||||
if !day.Valid() {
|
||||
panic("mujica: DateSeparator needs a valid date")
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ type DragDropOverlayOptions struct {
|
||||
|
||||
// DropResult carries the overlay and dropped files.
|
||||
type DropResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
accepted []string
|
||||
rejected []string
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ type ErrorMessageOptions struct {
|
||||
|
||||
// ErrorMessageResult is the error's element and a retry the user asked for.
|
||||
type ErrorMessageResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
retried bool
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ type FeedbackFormOptions struct {
|
||||
|
||||
// FeedbackFormView is a built FeedbackForm and this frame's outcome.
|
||||
type FeedbackFormView struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Submitted, Cancelled bool
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ type FileMessageOptions struct {
|
||||
|
||||
// FileMessageResult is a FileMessage's element and this frame's actions.
|
||||
type FileMessageResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
opened, downloaded bool
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ type GridImage struct {
|
||||
|
||||
// ImageGridResult is an ImageGrid's element and a retry the user asked for.
|
||||
type ImageGridResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
retry int
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func ImageGrid(c *ui.Context, images []GridImage) ImageGridResult {
|
||||
for i, im := range images {
|
||||
loaded := im.Source != nil && !im.Loading && im.Err == nil
|
||||
// Only a loaded picture is a button; a failed one keeps its retry.
|
||||
var b *ui.Element
|
||||
var b ui.Element
|
||||
if loaded {
|
||||
b = ui.ButtonBase(c).Label(im.Alt)
|
||||
internal.Ring(b, k.Focus, theme.ControlRadius)
|
||||
|
||||
@@ -18,7 +18,7 @@ type LinkPreview struct {
|
||||
}
|
||||
|
||||
// LinkPreviewCard shows a link's site, title, description and thumbnail; Clicked opens it.
|
||||
func LinkPreviewCard(c *ui.Context, p LinkPreview) *ui.Element {
|
||||
func LinkPreviewCard(c *ui.Context, p LinkPreview) ui.Element {
|
||||
if p.URL == "" || p.Title == "" {
|
||||
panic("mujica: LinkPreviewCard needs URL and Title")
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
// MarkdownResult is a MarkdownView's element and the link the user opened this frame.
|
||||
type MarkdownResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
link string
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func MarkdownView(c *ui.Context, src string) MarkdownResult {
|
||||
r.Element.Children(func() {
|
||||
for i := range (*doc).Blocks {
|
||||
b := &(*doc).Blocks[i]
|
||||
ui.Column(c).Key(i).MinWidth(0).Children(func() {
|
||||
ui.Column(c.Key(i)).MinWidth(0).Children(func() {
|
||||
if url := markdownBlock(c, b); url != "" {
|
||||
r.link = url
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func markdownBlock(c *ui.Context, b *internal.MdBlock) string {
|
||||
}
|
||||
|
||||
// markdownSpans fills paragraph t with styled spans; links are focusable and return their URL when used.
|
||||
func markdownSpans(c *ui.Context, t *ui.Element, spans []internal.MdSpan) string {
|
||||
func markdownSpans(c *ui.Context, t ui.Element, spans []internal.MdSpan) string {
|
||||
k := core.Tokens(c)
|
||||
var link string
|
||||
t.MinWidth(0).Children(func() {
|
||||
|
||||
@@ -39,7 +39,7 @@ type MessageActionsOptions struct {
|
||||
|
||||
// MessageActionsResult is the toolbar's element and the action chosen this frame.
|
||||
type MessageActionsResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
action MessageAction
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ type MessageBubbleOptions struct {
|
||||
}
|
||||
|
||||
// MessageBubble frames one chat message by role: users right, others left, system centered.
|
||||
func MessageBubble(c *ui.Context, role MessageRole, opts MessageBubbleOptions, content func()) *ui.Element {
|
||||
func MessageBubble(c *ui.Context, role MessageRole, opts MessageBubbleOptions, content func()) ui.Element {
|
||||
if opts.MaxWidth < 0 {
|
||||
panic("mujica: MessageBubble MaxWidth must not be negative")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type MessageEditorOptions struct {
|
||||
|
||||
// MessageEditorResult is the editor's element and its outcome this frame.
|
||||
type MessageEditorResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
saved, canceled bool
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ type MessageHeaderOptions struct {
|
||||
}
|
||||
|
||||
// MessageHeader shows a message's model, time and status as icon and text.
|
||||
func MessageHeader(c *ui.Context, opts MessageHeaderOptions) *ui.Element {
|
||||
func MessageHeader(c *ui.Context, opts MessageHeaderOptions) ui.Element {
|
||||
k := core.Tokens(c)
|
||||
u := core.Density(c).Unit()
|
||||
small := core.FontSize(c, theme.CaptionSize)
|
||||
|
||||
@@ -35,7 +35,7 @@ type MessageListOptions struct {
|
||||
|
||||
// MessageListResult is a MessageList's element and its unread count.
|
||||
type MessageListResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
unread int
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ func ModeSelector(c *ui.Context, mode *ChatMode, opts ModeSelectorOptions) core.
|
||||
k, u := core.Tokens(c), core.Density(c).Unit()
|
||||
old, pick := *mode, int(*mode)
|
||||
wrapper := ui.Row(c).Shrink(0).Disabled(opts.Disabled)
|
||||
var track *ui.Element
|
||||
var track ui.Element
|
||||
wrapper.Children(func() {
|
||||
parts := ui.SegmentedBase(c, &pick, len(chatModes))
|
||||
track = parts.Track.Padding(2).Gap(2).Radius(theme.ControlRadius+2).Background(k.Background).
|
||||
|
||||
@@ -44,7 +44,7 @@ func ModelSelector(c *ui.Context, value *string, models []ModelOption, opts Mode
|
||||
}
|
||||
}
|
||||
k, u := core.Tokens(c), core.Density(c).Unit()
|
||||
var tr *ui.Element
|
||||
var tr ui.Element
|
||||
ui.Row(c).Shrink(0).Disabled(opts.Disabled).Children(func() { tr = ui.ButtonBase(c).Role(ui.RolePopUpButton) })
|
||||
st := ui.Local(tr, "model-selector", func() modelSelectorState { return modelSelectorState{hi: -1, lastHi: -1} })
|
||||
disabled := tr.IsDisabled()
|
||||
@@ -120,7 +120,7 @@ func ModelSelector(c *ui.Context, value *string, models []ModelOption, opts Mode
|
||||
})
|
||||
if st.open {
|
||||
ww, wh := c.Size()
|
||||
ui.PopoverBase(c, tr, &st.open, func(panel *ui.Element) {
|
||||
ui.PopoverBase(c, tr, &st.open, func(panel ui.Element) {
|
||||
core.OverlayStyle(c, panel, overlay.OverlayAnchored(c, panel, tr.Bounds()))
|
||||
a := tr.Bounds()
|
||||
room := max(a.Y, wh-a.Y-a.H) - 8
|
||||
|
||||
@@ -62,7 +62,7 @@ func ParameterPanel(c *ui.Context, params []ModelParam) core.ChoiceResult {
|
||||
format := func(v float64) string {
|
||||
return strconv.FormatFloat(v, 'f', input.SliderDecimals(p.Min, p.Step, p.Default), 64)
|
||||
}
|
||||
ui.Column(c).Key(p.Label).Gap(u).Children(func() {
|
||||
ui.Column(c.Key(p.Label)).Gap(u).Children(func() {
|
||||
ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() {
|
||||
ui.Text(c, p.Label).FontWeight(600).Grow(1).SingleLine()
|
||||
ui.Text(c, format(*p.Value)).FontFeatures("tnum").TextColor(k.AccentText).SingleLine()
|
||||
|
||||
@@ -35,7 +35,7 @@ func PasteImagePreview(c *ui.Context, images []PastedImage) RemoveResult {
|
||||
if im.ID == "" || im.Source == nil {
|
||||
panic("mujica: PastedImage needs ID and Source")
|
||||
}
|
||||
ui.Box(c).Key(im.ID).Size(pasteImageSize, pasteImageSize).Role(ui.RoleListItem).Children(func() {
|
||||
ui.Box(c.Key(im.ID)).Size(pasteImageSize, pasteImageSize).Role(ui.RoleListItem).Children(func() {
|
||||
ui.Image(c, im.Source).Fit(ui.Cover).Size(pasteImageSize, pasteImageSize).Radius(theme.ControlRadius).
|
||||
Border(theme.BorderWidth, k.Border).Label(im.Name)
|
||||
x := core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "paste_image_preview.remove"), im.Name)).
|
||||
|
||||
@@ -30,7 +30,7 @@ type ProjectKnowledgePanelOptions struct {
|
||||
|
||||
// ProjectKnowledgePanelView is a built panel and its requests.
|
||||
type ProjectKnowledgePanelView struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Add asks the caller to pick files to add.
|
||||
Add bool
|
||||
// Removed is the ID of a file to remove.
|
||||
@@ -73,7 +73,7 @@ func ProjectKnowledgePanel(c *ui.Context, files []KnowledgeFile, opts ProjectKno
|
||||
}
|
||||
ui.Column(c).Gap(2).FocusGroup(ui.Vertical).Children(func() {
|
||||
for _, f := range files {
|
||||
ui.Row(c).Key(f.ID).MinHeight(d.ControlHeight()).Gap(d.Unit()*2).AlignItems(ui.Center).Padding(0, d.Unit()).Children(func() {
|
||||
ui.Row(c.Key(f.ID)).MinHeight(d.ControlHeight()).Gap(d.Unit()*2).AlignItems(ui.Center).Padding(0, d.Unit()).Children(func() {
|
||||
ui.Icon(c, icons.Must("file-text")).Size(16, 16).TextColor(k.TextMuted)
|
||||
ui.Text(c, f.Name).SingleLine().Grow(1).MinWidth(0)
|
||||
ui.Text(c, fmt.Sprintf(core.Msg(c, "project_knowledge_panel.tokens"), f.Tokens)).
|
||||
|
||||
@@ -35,7 +35,7 @@ type ProjectListState struct {
|
||||
|
||||
// ProjectListView is a built ProjectList and this frame's events.
|
||||
type ProjectListView struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Changed reports a newly chosen project, in State.Selected.
|
||||
Changed bool
|
||||
// Opened is a conversation the user opened, empty for none.
|
||||
@@ -64,7 +64,7 @@ func ProjectList(c *ui.Context, s *ProjectListState, projects []ChatProject, con
|
||||
}
|
||||
seen[p.Folder] = true
|
||||
open := !s.closed[p.Folder]
|
||||
ui.Column(c).Key("folder:" + p.Folder).Children(func() { projectListFolder(c, p.Folder, &open) })
|
||||
ui.Column(c.Key("folder:" + p.Folder)).Children(func() { projectListFolder(c, p.Folder, &open) })
|
||||
s.closed[p.Folder] = !open
|
||||
if !open {
|
||||
continue
|
||||
@@ -94,7 +94,7 @@ func ProjectList(c *ui.Context, s *ProjectListState, projects []ChatProject, con
|
||||
}
|
||||
|
||||
// projectListFolder is a folder heading on a CollapsibleBase trigger.
|
||||
func projectListFolder(c *ui.Context, title string, open *bool) *ui.Element {
|
||||
func projectListFolder(c *ui.Context, title string, open *bool) ui.Element {
|
||||
k := core.Tokens(c)
|
||||
d := core.Density(c)
|
||||
parts := ui.CollapsibleBase(c, open)
|
||||
@@ -121,8 +121,8 @@ func projectListProject(c *ui.Context, s *ProjectListState, p ChatProject, all [
|
||||
d := core.Density(c)
|
||||
convs := conversations(p.ID)
|
||||
chosen := s.Selected == p.ID
|
||||
var b *ui.Element
|
||||
line := ui.Row(c).Key("project:" + p.ID).AlignItems(ui.Center)
|
||||
var b ui.Element
|
||||
line := ui.Row(c.Key("project:" + p.ID)).AlignItems(ui.Center)
|
||||
line.Children(func() { b = ui.ButtonBase(c).Grow(1).MinWidth(0).MinHeight(d.ControlHeight()) })
|
||||
b.Padding(0, d.Unit()*2, 0, d.Unit()*2+indent).Padding(0, d.Unit()*2, 0, d.Unit()*2+indent).
|
||||
Gap(d.Unit() * 2).Justify(ui.Start).Radius(theme.ControlRadius).Role(ui.RoleTreeItem).Label(p.Name).Selected(chosen)
|
||||
@@ -185,7 +185,7 @@ func projectListProject(c *ui.Context, s *ProjectListState, p ChatProject, all [
|
||||
func projectListConversation(c *ui.Context, conv ChatConversation, home ChatProject, all []ChatProject, indent float32, v *ProjectListView) {
|
||||
k := core.Tokens(c)
|
||||
d := core.Density(c)
|
||||
ui.Row(c).Key("conv:" + conv.ID).AlignItems(ui.Center).Children(func() {
|
||||
ui.Row(c.Key("conv:" + conv.ID)).AlignItems(ui.Center).Children(func() {
|
||||
b := ui.ButtonBase(c).Grow(1).MinWidth(0).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2, 0, d.Unit()*2+indent).
|
||||
Justify(ui.Start).Radius(theme.ControlRadius).Label(conv.Title).Drag(ConversationDrag{ID: conv.ID})
|
||||
internal.Ring(b, k.Focus, theme.ControlRadius)
|
||||
|
||||
@@ -53,6 +53,7 @@ func PromptComposer(c *ui.Context, value *string, opts PromptComposerOptions) in
|
||||
}
|
||||
var r input.InputResult
|
||||
r.Element = ui.Column(c).Gap(u * 2).MinWidth(0).Disabled(opts.Disabled)
|
||||
disabled := r.Element.IsDisabled()
|
||||
hooks := ui.Local(r.Element, promptHooksKey{}, func() *promptHooks { return &promptHooks{} })
|
||||
h := *hooks
|
||||
h.list = h.list[:0]
|
||||
@@ -62,7 +63,7 @@ func PromptComposer(c *ui.Context, value *string, opts PromptComposerOptions) in
|
||||
}
|
||||
ta := input.TextAreaOptions{Label: label, Placeholder: opts.Placeholder, MaxHeight: maxH,
|
||||
Keys: func(ev ui.InputEvent) bool {
|
||||
if r.Element.IsDisabled() {
|
||||
if disabled {
|
||||
return true
|
||||
}
|
||||
if opts.Composing && ev.Kind == ui.InputKeyDown {
|
||||
@@ -101,7 +102,7 @@ func PromptComposer(c *ui.Context, value *string, opts PromptComposerOptions) in
|
||||
|
||||
// promptComposerHook registers a handler before editor input.
|
||||
func promptComposerHook(composer input.InputResult, f func(ui.InputEvent) bool) {
|
||||
if composer.Element == nil {
|
||||
if !composer.Element.Valid() {
|
||||
panic("mujica: menus need a built PromptComposer")
|
||||
}
|
||||
h := *ui.Local(composer.Element, promptHooksKey{}, func() *promptHooks { return &promptHooks{} })
|
||||
|
||||
@@ -47,7 +47,7 @@ type promptLibraryShown struct {
|
||||
|
||||
// PromptLibraryView is a built PromptLibrary and inserted text.
|
||||
type PromptLibraryView struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Inserted is the filled template, or empty.
|
||||
Inserted string
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func promptLibraryCategory(c *ui.Context, k theme.Tokens, s *PromptLibraryState,
|
||||
// selectableRow is a focusable row marked chosen with a diamond.
|
||||
func selectableRow(c *ui.Context, k theme.Tokens, key, label string, chosen bool) bool {
|
||||
d := core.Density(c)
|
||||
b := ui.ButtonBase(c).Key(key).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).Justify(ui.Start).
|
||||
b := ui.ButtonBase(c.Key(key)).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).Justify(ui.Start).
|
||||
Radius(theme.ControlRadius).Label(label).Selected(chosen)
|
||||
b.Background(ui.Transparent).TextColor(k.Text)
|
||||
internal.Ring(b, k.Focus, theme.ControlRadius)
|
||||
@@ -202,8 +202,8 @@ func promptLibraryPreview(c *ui.Context, k theme.Tokens, s *PromptLibraryState,
|
||||
Children(func() { ui.RichText(c, spans...).Selectable() })
|
||||
for _, name := range vars {
|
||||
val := s.Values[name]
|
||||
ui.Column(c).Key("var:" + name).Children(func() {
|
||||
input.FormField(c, name, input.FormFieldOptions{Required: true}, func() *ui.Element {
|
||||
ui.Column(c.Key("var:" + name)).Children(func() {
|
||||
input.FormField(c, name, input.FormFieldOptions{Required: true}, func() ui.Element {
|
||||
r := input.TextInput(c, &val, input.TextInputOptions{Label: name, Disabled: disabled})
|
||||
if r.Changed() {
|
||||
s.Values[name] = val
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// QuoteReply shows the author and a one-line excerpt of a quoted message; Clicked jumps to it.
|
||||
func QuoteReply(c *ui.Context, author, excerpt string) *ui.Element {
|
||||
func QuoteReply(c *ui.Context, author, excerpt string) ui.Element {
|
||||
if author == "" {
|
||||
panic("mujica: QuoteReply needs an author")
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ type RegenerateMenuOptions struct {
|
||||
|
||||
// RegenerateMenuView is a built menu and the chosen retry.
|
||||
type RegenerateMenuView struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Chosen bool
|
||||
Choice RegenerateChoice
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
// ScrollToBottomButton shows while away is true, with the unread count; click it to return to the end.
|
||||
// While hidden it returns an empty element whose Clicked is false.
|
||||
func ScrollToBottomButton(c *ui.Context, away bool, unread int) *ui.Element {
|
||||
func ScrollToBottomButton(c *ui.Context, away bool, unread int) ui.Element {
|
||||
if unread < 0 {
|
||||
panic("mujica: ScrollToBottomButton unread must not be negative")
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func ScrollToBottomButton(c *ui.Context, away bool, unread int) *ui.Element {
|
||||
label = fmt.Sprintf(core.Msg(c, "scroll_to_bottom_button.unread"), unread)
|
||||
}
|
||||
wrap := ui.Row(c).Absolute().Left(0).Right(0).Bottom(u * 3).Justify(ui.Center).PassThrough()
|
||||
var b *ui.Element
|
||||
var b ui.Element
|
||||
wrap.Children(func() {
|
||||
b = ui.ButtonBase(c).MinHeight(theme.MinHitSize).MinWidth(theme.MinHitSize).Padding(0, u*3).Gap(u*2).
|
||||
Radius(theme.MinHitSize/2).Label(label).Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ type SendButtonOptions struct {
|
||||
|
||||
// SendResult carries the button and requested action.
|
||||
type SendResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
send, stop bool
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func SendButton(c *ui.Context, opts SendButtonOptions) SendResult {
|
||||
tip += " (" + opts.Shortcut + ")"
|
||||
}
|
||||
var r SendResult
|
||||
var b *ui.Element
|
||||
var b ui.Element
|
||||
r.Element = ui.Row(c).Shrink(0).Disabled(opts.Disabled && !opts.Streaming)
|
||||
r.Element.Children(func() {
|
||||
b = ui.ButtonBase(c).Size(h, h).Radius(h / 2).Label(label).Tooltip(tip).Transition(core.ColorFade)
|
||||
|
||||
@@ -23,7 +23,7 @@ type SlashCommand struct {
|
||||
|
||||
// SlashCommandResult carries the popup and chosen command.
|
||||
type SlashCommandResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
chosen SlashCommand
|
||||
ok bool
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func (r SlashCommandResult) Chosen() (SlashCommand, bool) { return r.chosen, r.o
|
||||
// SlashCommandMenu filters commands after a slash trigger.
|
||||
// Arrows navigate; Enter chooses; Tab completes; Escape dismisses.
|
||||
func SlashCommandMenu(c *ui.Context, composer input.InputResult, value *string, commands []SlashCommand) SlashCommandResult {
|
||||
if composer.Element == nil || composer.Input == nil || value == nil {
|
||||
if !composer.Element.Valid() || !composer.Input.Valid() || value == nil {
|
||||
panic("mujica: SlashCommandMenu needs composer and value")
|
||||
}
|
||||
data := ui.Local(composer.Element, "slash-data", func() slashMenuData { return slashMenuData{} })
|
||||
@@ -116,7 +116,7 @@ const (
|
||||
)
|
||||
|
||||
// slashMenuShow returns the selected row and activation method.
|
||||
func slashMenuShow(c *ui.Context, composer input.InputResult, st *slashMenuState, query string, open bool, rows []slashMenuRow, o slashMenuOptions) (int, slashMenuHow, *ui.Element) {
|
||||
func slashMenuShow(c *ui.Context, composer input.InputResult, st *slashMenuState, query string, open bool, rows []slashMenuRow, o slashMenuOptions) (int, slashMenuHow, ui.Element) {
|
||||
if !open {
|
||||
st.dismissed = nil
|
||||
}
|
||||
@@ -133,7 +133,7 @@ func slashMenuShow(c *ui.Context, composer input.InputResult, st *slashMenuState
|
||||
st.open = open
|
||||
if !open {
|
||||
st.pick = slashMenuNone
|
||||
return pick, how, nil
|
||||
return pick, how, ui.Element{}
|
||||
}
|
||||
st.hi = min(max(st.hi, 0), len(rows)-1)
|
||||
if st.pick != slashMenuNone && st.hi >= 0 {
|
||||
@@ -142,9 +142,6 @@ func slashMenuShow(c *ui.Context, composer input.InputResult, st *slashMenuState
|
||||
st.pick = slashMenuNone
|
||||
n := len(rows)
|
||||
promptComposerHook(composer, func(ev ui.InputEvent) bool {
|
||||
if composer.Element.IsDisabled() {
|
||||
return true
|
||||
}
|
||||
if ev.Kind != ui.InputKeyDown || ev.Mods != 0 {
|
||||
return false
|
||||
}
|
||||
@@ -178,7 +175,7 @@ func slashMenuShow(c *ui.Context, composer input.InputResult, st *slashMenuState
|
||||
}
|
||||
|
||||
// slashMenuPanel builds visible rows and returns clicked choices.
|
||||
func slashMenuPanel(c *ui.Context, a ui.Rect, st *slashMenuState, rows []slashMenuRow, o slashMenuOptions) (int, *ui.Element) {
|
||||
func slashMenuPanel(c *ui.Context, a ui.Rect, st *slashMenuState, rows []slashMenuRow, o slashMenuOptions) (int, ui.Element) {
|
||||
k := core.Tokens(c)
|
||||
u := core.Density(c).Unit()
|
||||
rowH := core.Density(c).ControlHeight()
|
||||
@@ -199,7 +196,7 @@ func slashMenuPanel(c *ui.Context, a ui.Rect, st *slashMenuState, rows []slashMe
|
||||
w := min(max(a.W*0.6, 320), max(a.W, 200), max(0, ww-8))
|
||||
x, y := core.PlaceOverlay(c, a, w, h, u)
|
||||
clicked, pressed := -1, false
|
||||
var result *ui.Element
|
||||
var result ui.Element
|
||||
ui.Overlay(c, func() {
|
||||
panel := ui.Column(c).Absolute().Left(x).Top(y).Width(w).Height(h).Role(ui.RoleList).Label(o.label)
|
||||
core.OverlayStyle(c, panel, overlay.OverlayAnchored(c, panel, a))
|
||||
@@ -231,7 +228,7 @@ func slashMenuPanel(c *ui.Context, a ui.Rect, st *slashMenuState, rows []slashMe
|
||||
ui.Text(c, r.group).Height(headH).Padding(u*2, u*3, 0, u*3).FontSize(core.FontSize(c, theme.CaptionSize)).
|
||||
FontWeight(600).TextColor(k.TextMuted).Role(ui.RoleText)
|
||||
}
|
||||
row := ui.Row(c).Key(i).MinHeight(rowH).Padding(0, u*3, 0, u*2).Gap(u * 2).AlignItems(ui.Center).
|
||||
row := ui.Row(c.Key(i)).MinHeight(rowH).Padding(0, u*3, 0, u*2).Gap(u * 2).AlignItems(ui.Center).
|
||||
Radius(theme.ControlRadius).Role(ui.RoleListItem).Label(r.label).Cursor(ui.CursorPointer).Selected(i == st.hi).TextColor(k.Text)
|
||||
if row.Clicked() {
|
||||
clicked = i
|
||||
|
||||
@@ -130,18 +130,18 @@ func TestSlashCommandMenuNarrowAndLargeData(t *testing.T) {
|
||||
cmds[i] = SlashCommand{Name: fmt.Sprintf("cmd%d", i)}
|
||||
}
|
||||
text := "/"
|
||||
var panel *ui.Element
|
||||
shown, b := false, ui.Rect{}
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
core.Use(c, core.Settings{})
|
||||
p := PromptComposer(c, &text, PromptComposerOptions{Label: "Prompt"})
|
||||
panel = SlashCommandMenu(c, p, &text, cmds).Element
|
||||
panel := SlashCommandMenu(c, p, &text, cmds).Element
|
||||
shown, b = panel.Valid(), panel.Bounds()
|
||||
}, 180, 120)
|
||||
_ = tt.Click("Prompt")
|
||||
tt.Frame()
|
||||
if panel == nil {
|
||||
if !shown {
|
||||
t.Fatal("menu missing")
|
||||
}
|
||||
b := panel.Bounds()
|
||||
if b.X < 0 || b.Y < 0 || b.X+b.W > 180 || b.Y+b.H > 120 {
|
||||
t.Errorf("popup outside window: %v", b)
|
||||
}
|
||||
|
||||
+2
-3
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
// SourceCard displays source metadata; Clicked requests opening.
|
||||
// Selection and an edge mark identify active or hovered sources.
|
||||
func SourceCard(c *ui.Context, n int, src Source, st *CitationState) *ui.Element {
|
||||
func SourceCard(c *ui.Context, n int, src Source, st *CitationState) ui.Element {
|
||||
if n < 1 || st == nil || src.Title == "" || src.URL == "" {
|
||||
panic("mujica: SourceCard needs n >= 1, a state and a Source with Title and URL")
|
||||
}
|
||||
@@ -23,8 +23,7 @@ func SourceCard(c *ui.Context, n int, src Source, st *CitationState) *ui.Element
|
||||
Label(fmt.Sprintf(core.Msg(c, "source_card.open"), src.Title)).Tooltip(src.URL).Transition(core.ColorFade)
|
||||
internal.Ring(b, k.Focus, theme.CardRadius)
|
||||
st.track(c, b, n)
|
||||
frame := c.Now()
|
||||
b.Draw(func(_ *ui.Painter, _ ui.Rect) { st.expire(c, frame) })
|
||||
b.Draw(func(_ *ui.Painter, _ ui.Rect) { st.expire(c) })
|
||||
switch {
|
||||
case st.lit(n):
|
||||
b.Background(k.Selection).Border(theme.BorderWidth, k.Border).BorderWidth(theme.BorderWidth, theme.BorderWidth, theme.BorderWidth, 3).
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
// OpenResult carries the list and opened 1-based source number.
|
||||
type OpenResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
opened int
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func StopGeneratingButton(c *ui.Context, opts StopGeneratingButtonOptions) core.
|
||||
if runtime.GOOS == "darwin" {
|
||||
mods = ui.Cmd
|
||||
}
|
||||
var b *ui.Element
|
||||
var b ui.Element
|
||||
wrap := ui.Row(c).Shrink(0).Disabled(opts.Disabled)
|
||||
wrap.Children(func() {
|
||||
b = ui.ButtonBase(c).MinHeight(max(theme.MinHitSize, d.ControlHeight()-4)).Padding(0, d.Unit()*3).Gap(d.Unit()*2).
|
||||
|
||||
@@ -28,7 +28,7 @@ type streamingState struct {
|
||||
}
|
||||
|
||||
// StreamingText shows text that grows as it streams; reduced motion shows it at once.
|
||||
func StreamingText(c *ui.Context, text string, opts StreamingTextOptions) *ui.Element {
|
||||
func StreamingText(c *ui.Context, text string, opts StreamingTextOptions) ui.Element {
|
||||
if opts.Rate < 0 {
|
||||
panic("mujica: StreamingText Rate must not be negative")
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type SuggestionChipsOptions struct {
|
||||
|
||||
// SuggestionChipsView is built chips and the suggestion used this frame.
|
||||
type SuggestionChipsView struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Chosen is the suggestion clicked, empty for none.
|
||||
Chosen string
|
||||
// Sent asks to send Chosen now instead of filling.
|
||||
@@ -47,7 +47,7 @@ func SuggestionChips(c *ui.Context, input *string, suggestions []string, opts Su
|
||||
if s == "" {
|
||||
panic("mujica: SuggestionChips with an empty suggestion")
|
||||
}
|
||||
b := ui.ButtonBase(c).Key(i).MinHeight(max(theme.MinHitSize, d.ControlHeight()-4)).Padding(0, d.Unit()*3).
|
||||
b := ui.ButtonBase(c.Key(i)).MinHeight(max(theme.MinHitSize, d.ControlHeight()-4)).Padding(0, d.Unit()*3).
|
||||
Gap(d.Unit()*2).Radius(theme.ControlRadius).Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Label(s)
|
||||
internal.Ring(b, k.Focus, theme.ControlRadius)
|
||||
switch {
|
||||
|
||||
@@ -30,7 +30,7 @@ type SystemPromptEditorOptions struct {
|
||||
|
||||
// SystemPromptEditorView is a built editor and its requests.
|
||||
type SystemPromptEditorView struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Save asks to save *value; Restored reports the default returned.
|
||||
Save, Restored bool
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type ThinkingIndicatorOptions struct {
|
||||
}
|
||||
|
||||
// ThinkingIndicator shows rising diamonds and a phase, still when reduced.
|
||||
func ThinkingIndicator(c *ui.Context, opts ThinkingIndicatorOptions) *ui.Element {
|
||||
func ThinkingIndicator(c *ui.Context, opts ThinkingIndicatorOptions) ui.Element {
|
||||
k := core.Tokens(c)
|
||||
d := core.Density(c)
|
||||
phase := opts.Phase
|
||||
|
||||
@@ -27,8 +27,7 @@ func TestThinkingIndicatorReducedMotionIsStill(t *testing.T) {
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: reduce, TextScale: 1})
|
||||
tt.Frame()
|
||||
if at > 0 {
|
||||
time.Sleep(at)
|
||||
tt.Frame()
|
||||
tt.Advance(at)
|
||||
}
|
||||
return encode(t, tt.Image())
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
const tokenWarnShare = 0.9
|
||||
|
||||
// TokenCounter displays estimates, limits and threshold warnings.
|
||||
func TokenCounter(c *ui.Context, used, limit int) *ui.Element {
|
||||
func TokenCounter(c *ui.Context, used, limit int) ui.Element {
|
||||
if used < 0 || limit <= 0 {
|
||||
panic("mujica: TokenCounter needs used >= 0 and limit > 0")
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ type ToolItem struct {
|
||||
|
||||
// ToolToggleResult carries the trigger and requested action.
|
||||
type ToolToggleResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
changed, authorize string
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func toolToggleRow(c *ui.Context, t ToolItem, r *ToolToggleResult) {
|
||||
u := core.Density(c).Unit()
|
||||
small := core.FontSize(c, theme.CaptionSize)
|
||||
locked := t.NeedsAuth && !t.Authorized
|
||||
ui.Row(c).Key(t.ID).Gap(u * 3).AlignItems(ui.Center).Children(func() {
|
||||
ui.Row(c.Key(t.ID)).Gap(u * 3).AlignItems(ui.Center).Children(func() {
|
||||
ui.Column(c).Grow(1).MinWidth(0).Children(func() {
|
||||
ui.Row(c).Gap(u).AlignItems(ui.Center).MinHeight(core.Density(c).ControlHeight()).Children(func() {
|
||||
if locked {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
const typingPeriod = 1200
|
||||
|
||||
// TypingIndicator shows that name is typing: three fading dots, or static text under reduced motion.
|
||||
func TypingIndicator(c *ui.Context, name string) *ui.Element {
|
||||
func TypingIndicator(c *ui.Context, name string) ui.Element {
|
||||
if name == "" {
|
||||
panic("mujica: TypingIndicator needs a name")
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ type VoiceWaveformOptions struct {
|
||||
}
|
||||
|
||||
// VoiceWaveform draws recent input levels and a silence hint.
|
||||
func VoiceWaveform(c *ui.Context, levels []float32, opts VoiceWaveformOptions) *ui.Element {
|
||||
func VoiceWaveform(c *ui.Context, levels []float32, opts VoiceWaveformOptions) ui.Element {
|
||||
if opts.Silence < 0 || opts.Silence >= 1 || opts.SilentFor < 0 || opts.Width < 0 {
|
||||
panic("mujica: VoiceWaveform needs 0 <= Silence < 1 and non-negative SilentFor and Width")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user