package main import ( "fmt" "image/color" "strings" "gioui.org/layout" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" "git.julianfamily.org/keepassgo/appstate" ) func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.showLocalLifecycle, "Local Vault") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.showRemoteLifecycle, "Remote Vault") }), ) }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(labeledEditorHelp(u.theme, "Master Password", "Leave blank if this vault is protected by key file only.", &u.masterPassword, true)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(selectorEditorHelp(u.theme, "Key File", "Optional path to a KeePass-compatible key file.", &u.keyFilePath, &u.pickKeyFile, "Choose File", false)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if u.lifecycleMode == "remote" { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)), ) } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(selectorEditorHelp(u.theme, "Vault Path", "Choose the existing .kdbx file to open.", &u.vaultPath, &u.pickVaultPath, "Choose File", false)), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(u.recentVaultList), ) }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if u.lifecycleMode == "remote" { return tonedButton(gtx, u.theme, &u.openRemote, "Open Remote Vault") } return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.openVault, "Open Vault") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.createVault, "New Vault") }), ) }), ) } func (u *ui) recentVaultList(gtx layout.Context) layout.Dimensions { if len(u.recentVaults) == 0 { return layout.Dimensions{} } if len(u.recentVaultClicks) < len(u.recentVaults) { u.recentVaultClicks = make([]widget.Clickable, len(u.recentVaults)) } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "RECENTLY OPENED") lbl.Color = mutedColor return lbl.Layout(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild { children := make([]layout.FlexChild, 0, len(u.recentVaults)*2) for i, path := range u.recentVaults { index := i label := path children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.recentVaultClicks[index], label) })) if i < len(u.recentVaults)-1 { children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) } } return children }()...) }), ) } func (u *ui) attachmentList(gtx layout.Context) layout.Dimensions { items := u.selectedAttachmentItems() if len(items) == 0 { lbl := material.Label(u.theme, unit.Sp(13), "No attachments on this entry.") lbl.Color = mutedColor return lbl.Layout(gtx) } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild { children := make([]layout.FlexChild, 0, len(items)*2) for i, item := range items { index := i itemName := item.Name label := fmt.Sprintf("%s (%d B)", itemName, item.Size) children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { for u.attachmentClicks[index].Clicked(gtx) { u.attachmentName.SetText(itemName) } return tonedButton(gtx, u.theme, &u.attachmentClicks[index], label) })) if i < len(items)-1 { children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) } } return children }()...) } func (u *ui) customFieldEditorPanel(gtx layout.Context) layout.Dimensions { if len(u.customFieldKeys) == 0 { u.setCustomFieldRows(nil) } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "CUSTOM FIELDS") lbl.Color = mutedColor return lbl.Layout(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(11), "Add key/value pairs. Changes are only saved when you save the entry.") lbl.Color = mutedColor return lbl.Layout(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild { children := make([]layout.FlexChild, 0, len(u.customFieldKeys)*2) for i := range u.customFieldKeys { index := i children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { for u.removeCustomFields[index].Clicked(gtx) { u.removeCustomFieldRow(index) } return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Flexed(0.38, func(gtx layout.Context) layout.Dimensions { return labeledEditor(u.theme, "Name", &u.customFieldKeys[index], false)(gtx) }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Flexed(0.52, func(gtx layout.Context) layout.Dimensions { return labeledEditor(u.theme, "Value", &u.customFieldValues[index], false)(gtx) }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if len(u.customFieldKeys) == 1 && (strings.TrimSpace(u.customFieldKeys[index].Text()) != "" || strings.TrimSpace(u.customFieldValues[index].Text()) != "") { return layout.Dimensions{} } return tonedButton(gtx, u.theme, &u.removeCustomFields[index], "-") }), ) })) if i < len(u.customFieldKeys)-1 { children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) } } return children }()...) }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { for u.addCustomField.Clicked(gtx) { u.appendCustomFieldRow("", "") } return tonedButton(gtx, u.theme, &u.addCustomField, "+") }), ) } func (u *ui) groupControls(gtx layout.Context) layout.Dimensions { if u.state.Section != appstate.SectionEntries { return layout.Dimensions{} } deletable, deleteReason := u.currentGroupDeletionState() return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(labeledEditor(u.theme, "Group Name", &u.groupName, false)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.createGroup, "Create Group") }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if len(u.displayPath()) == 0 { return layout.Dimensions{} } return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.renameGroup, "Rename Current Group") }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if !deletable || u.deleteGroupPendingConfirmation() { return layout.Dimensions{} } return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.deleteGroup, "Delete Empty Group") }), ) }), ) }), ) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if len(u.displayPath()) == 0 { return layout.Dimensions{} } if u.deleteGroupPendingConfirmation() { lbl := material.Label(u.theme, unit.Sp(12), fmt.Sprintf("Delete %q? This group is empty, and the deletion cannot be undone.", strings.Join(u.displayPath(), " / "))) lbl.Color = mutedColor return lbl.Layout(gtx) } if deletable || strings.TrimSpace(deleteReason) == "" { return layout.Dimensions{} } lbl := material.Label(u.theme, unit.Sp(12), deleteReason) lbl.Color = mutedColor return lbl.Layout(gtx) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if !u.deleteGroupPendingConfirmation() { return layout.Dimensions{} } return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.confirmDeleteGroup, "Confirm Delete") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.cancelDeleteGroup, "Cancel") }), ) }), ) } func (u *ui) groupControlsSection(gtx layout.Context) layout.Dimensions { if u.state.Section != appstate.SectionEntries { return layout.Dimensions{} } if u.groupControlsHidden { return layout.Dimensions{} } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(u.groupControls), ) } func (u *ui) groupControlsDisclosure(gtx layout.Context) layout.Dimensions { return u.toggleGroupControls.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { icon := u.expandLessIcon if u.groupControlsHidden { icon = u.expandMoreIcon } if icon == nil { lbl := material.Label(u.theme, unit.Sp(16), ">") if !u.groupControlsHidden { lbl.Text = "v" } lbl.Color = accentColor return lbl.Layout(gtx) } return icon.Layout(gtx, accentColor) }), layout.Rigid(layout.Spacer{Width: unit.Dp(4)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "Group Tools") lbl.Color = mutedColor return lbl.Layout(gtx) }), ) }) }) } func (u *ui) entryEditorPanel(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(labeledEditorWithFocus(u.theme, "Title", &u.entryTitle, false, u.isFocused(detailFocusID(detailFieldTitle)))), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorWithFocus(u.theme, "Username", &u.entryUsername, false, u.isFocused(detailFocusID(detailFieldUsername)))), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorWithFocus(u.theme, "Password", &u.entryPassword, true, u.isFocused(detailFocusID(detailFieldPassword)))), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorWithFocus(u.theme, "URL", &u.entryURL, false, u.isFocused(detailFocusID(detailFieldURL)))), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorWithFocus(u.theme, "Path", &u.entryPath, false, u.isFocused(detailFocusID(detailFieldPath)))), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorWithFocus(u.theme, "Tags", &u.entryTags, false, u.isFocused(detailFocusID(detailFieldTags)))), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditorWithFocus(u.theme, "Password Profile", &u.passwordProfile, false, u.isFocused(detailFocusID(detailFieldPasswordProfile)))), layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), u.passwordProfileOptionsText()) lbl.Color = mutedColor return lbl.Layout(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledMultilineEditorWithFocus(u.theme, "Notes", &u.entryNotes, false, u.isFocused(detailFocusID(detailFieldNotes)), unit.Dp(120))), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(u.customFieldEditorPanel), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(labeledEditorWithFocus(u.theme, "History Index", &u.historyIndex, false, u.isFocused(detailFocusID(detailFieldHistoryIndex)))), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.generatePassword, "Generate Password") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.cancelEdit, "Cancel") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { if u.state.Section == appstate.SectionTemplates { return tonedButton(gtx, u.theme, &u.saveTemplate, "Save Template") } return tonedButton(gtx, u.theme, &u.saveEntry, "Save Entry") }), ) }), layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(11), "Generate Password only updates the form. Nothing is persisted until you save.") lbl.Color = mutedColor return lbl.Layout(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.copyUser, "Copy User") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.copyPass, "Copy Password") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.copyURL, "Copy URL") }), ) }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "ATTACHMENTS") lbl.Color = mutedColor return lbl.Layout(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(u.attachmentList), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(labeledEditor(u.theme, "Attachment Name", &u.attachmentName, false)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditor(u.theme, "Attachment Path", &u.attachmentPath, false)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(labeledEditor(u.theme, "Export Attachment Path", &u.exportAttachmentPath, false)), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.addAttachment, "Add Attachment") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.replaceAttachment, "Replace Attachment") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.removeAttachment, "Remove Attachment") }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.exportAttachment, "Export Attachment") }), ) }), ) } func labeledEditor(th *material.Theme, label string, editor *widget.Editor, sensitive bool) layout.Widget { return labeledEditorWithFocus(th, label, editor, sensitive, false) } func labeledEditorHelp(th *material.Theme, label, help string, editor *widget.Editor, sensitive bool) layout.Widget { return labeledEditorHelpFocus(th, label, help, editor, sensitive, false) } func labeledEditorHelpFocus(th *material.Theme, label, help string, editor *widget.Editor, sensitive bool, focused bool) layout.Widget { return func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(labeledEditorWithFocus(th, label, editor, sensitive, focused)), layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(th, unit.Sp(11), help) lbl.Color = mutedColor return lbl.Layout(gtx) }), ) } } func selectorEditorHelp(th *material.Theme, label, help string, editor *widget.Editor, click *widget.Clickable, buttonLabel string, sensitive bool) layout.Widget { return func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { gtx.Constraints.Min.X = gtx.Constraints.Max.X return labeledEditor(th, label, editor, sensitive)(gtx) }), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, th, click, buttonLabel) }), ) }), layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(th, unit.Sp(11), help) lbl.Color = mutedColor return lbl.Layout(gtx) }), ) } } func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(u.unlockPasswordField), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(selectorEditorHelp(u.theme, "Key File", "Optional path to a KeePass-compatible key file.", &u.keyFilePath, &u.pickKeyFile, "Choose File", false)), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock") }), ) } func (u *ui) unlockPasswordField(gtx layout.Context) layout.Dimensions { icon := u.eyeIcon desc := "Show master password" mask := rune('•') if u.showPassword { icon = u.eyeOffIcon desc = "Hide master password" mask = 0 } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(12), "MASTER PASSWORD") lbl.Color = mutedColor return lbl.Layout(gtx) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return outlinedFieldState(gtx, false, func(gtx layout.Context) layout.Dimensions { return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { restore := u.masterPassword.Mask u.masterPassword.Mask = mask defer func() { u.masterPassword.Mask = restore }() gtx.Constraints.Min.X = gtx.Constraints.Max.X ed := material.Editor(u.theme, &u.masterPassword, "Master Password") return ed.Layout(gtx) }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { btn := material.IconButton(u.theme, &u.togglePassword, icon, desc) btn.Background = color.NRGBA{R: 239, G: 236, B: 229, A: 255} btn.Color = accentColor btn.Size = unit.Dp(18) btn.Inset = layout.UniformInset(unit.Dp(8)) return btn.Layout(gtx) }), ) }) }) }), layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(11), "Used alone or together with a key file to unlock the vault.") lbl.Color = mutedColor return lbl.Layout(gtx) }), ) } func labeledEditorWithFocus( th *material.Theme, label string, editor *widget.Editor, sensitive bool, focused bool, ) layout.Widget { return func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(th, unit.Sp(12), strings.ToUpper(label)) lbl.Color = mutedColor return lbl.Layout(gtx) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return outlinedFieldState(gtx, focused, func(gtx layout.Context) layout.Dimensions { mask := editor.Mask if sensitive { editor.Mask = '•' } defer func() { editor.Mask = mask }() gtx.Constraints.Min.X = gtx.Constraints.Max.X ed := material.Editor(th, editor, label) return layout.UniformInset(unit.Dp(8)).Layout(gtx, ed.Layout) }) }), ) } } func labeledMultilineEditorWithFocus( th *material.Theme, label string, editor *widget.Editor, sensitive bool, focused bool, minHeight unit.Dp, ) layout.Widget { return func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(th, unit.Sp(12), strings.ToUpper(label)) lbl.Color = mutedColor return lbl.Layout(gtx) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return outlinedFieldState(gtx, focused, func(gtx layout.Context) layout.Dimensions { mask := editor.Mask if sensitive { editor.Mask = '•' } defer func() { editor.Mask = mask }() gtx.Constraints.Min.X = gtx.Constraints.Max.X if min := gtx.Dp(minHeight); gtx.Constraints.Min.Y < min { gtx.Constraints.Min.Y = min } ed := material.Editor(th, editor, label) return layout.UniformInset(unit.Dp(8)).Layout(gtx, ed.Layout) }) }), ) } }