Minor appearance tweaks

This commit is contained in:
diamondburned 2020-07-10 23:48:44 -07:00
parent e35837ee2b
commit f5093a690b
11 changed files with 188 additions and 87 deletions

View File

@ -26,34 +26,21 @@ type ImageContainerSizer interface {
// AsyncImage loads an image. This method uses the cache.
func AsyncImage(img ImageContainer, url string, procs ...imgutil.Processor) {
if url == "" {
return
}
ctx, cancel := context.WithCancel(context.Background())
connectDestroyer(img, cancel)
gif := strings.Contains(url, ".gif")
l, err := gdk.PixbufLoaderNew()
if err != nil {
log.Error(errors.Wrap(err, "Failed to make pixbuf loader"))
return
}
l.Connect("area-prepared", areaPreparedFn(ctx, img, gif))
go syncImage(ctx, l, url, procs, gif)
asyncImage(img, url, 0, 0, procs)
}
// AsyncImageSized resizes using GdkPixbuf. This method uses the cache.
func AsyncImageSized(img ImageContainerSizer, url string, w, h int, procs ...imgutil.Processor) {
asyncImage(img, url, w, h, procs)
}
func asyncImage(img ImageContainer, url string, w, h int, procs []imgutil.Processor) {
if url == "" {
return
}
// Add a processor to resize.
procs = append(procs, imgutil.Resize(w, h))
// // Add a processor to resize.
// procs = append(procs, imgutil.Resize(w, h))
ctx, cancel := context.WithCancel(context.Background())
connectDestroyer(img, cancel)
@ -66,13 +53,17 @@ func AsyncImageSized(img ImageContainerSizer, url string, w, h int, procs ...img
return
}
l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) {
w, h = imgutil.MaxSize(imgW, imgH, w, h)
if w != imgW || h != imgH {
l.SetSize(w, h)
execIfCtx(ctx, func() { img.SetSizeRequest(w, h) })
}
})
if w > 0 && h > 0 {
l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) {
w, h = imgutil.MaxSize(imgW, imgH, w, h)
if w != imgW || h != imgH {
l.SetSize(w, h)
execIfCtx(ctx, func() {
img.(ImageContainerSizer).SetSizeRequest(w, h)
})
}
})
}
l.Connect("area-prepared", areaPreparedFn(ctx, img, gif))

View File

@ -10,8 +10,8 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
)
@ -165,17 +165,17 @@ func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage {
}
type Avatar struct {
gtk.Image
roundimage.Image
url string
}
func NewAvatar() *Avatar {
avatar, _ := gtk.ImageNew()
avatar, _ := roundimage.NewImage(0)
avatar.SetSizeRequest(AvatarSize, AvatarSize)
avatar.SetVAlign(gtk.ALIGN_START)
// Default icon.
primitives.SetImageIcon(avatar, "user-available-symbolic", AvatarSize)
primitives.SetImageIcon(avatar.Image, "user-available-symbolic", AvatarSize)
return &Avatar{*avatar, ""}
}
@ -191,7 +191,7 @@ func (a *Avatar) SetURL(url string) {
}
a.url = url
httputil.AsyncImageSized(a, url, AvatarSize, AvatarSize, imgutil.Round(true))
httputil.AsyncImageSized(a, url, AvatarSize, AvatarSize)
}
// ManuallySetURL sets the URL without downloading the image. It assumes the

View File

@ -7,13 +7,16 @@ import (
"image/png"
"io"
"io/ioutil"
"mime"
"os"
"path/filepath"
"strings"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
"github.com/disintegration/imaging"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk"
@ -24,7 +27,10 @@ var pngEncoder = png.Encoder{
CompressionLevel: png.BestCompression,
}
const FileIconSize = 72
const (
ThumbSize = 72
IconSize = 56
)
// File represents a middle format that can be used to create a
// MessageAttachment.
@ -264,8 +270,11 @@ func (c *Container) remove(name string) {
var previewCSS = primitives.PrepareCSS(`
.attachment-preview {
background-color: alpha(@theme_fg_color, 0.2);
border-radius: 4px;
box-shadow: none;
border: none;
background-color: alpha(@theme_fg_color, 0.15);
border-radius: 5px;
}
`)
@ -278,7 +287,7 @@ var deleteAttBtnCSS = primitives.PrepareCSS(`
/* Add our own styling */
border-radius: 999px 999px;
transition: linear 100ms all;
background-color: alpha(@theme_bg_color, 0.50);
background-color: alpha(@theme_bg_color, 0.75);
}
.delete-attachment:hover {
background-color: alpha(red, 0.5);
@ -287,9 +296,9 @@ var deleteAttBtnCSS = primitives.PrepareCSS(`
func (c *Container) addPreview(name string, src image.Image) {
// Make a fallback image first.
gimg, _ := gtk.ImageNew()
primitives.SetImageIcon(gimg, "image-x-generic-symbolic", FileIconSize/3)
gimg.SetSizeRequest(FileIconSize, FileIconSize)
gimg, _ := roundimage.NewImage(4) // border-radius: 4px
primitives.SetImageIcon(gimg.Image, iconFromName(name), IconSize)
gimg.SetSizeRequest(ThumbSize, ThumbSize)
gimg.SetVAlign(gtk.ALIGN_CENTER)
gimg.SetHAlign(gtk.ALIGN_CENTER)
gimg.SetTooltipText(name)
@ -300,14 +309,14 @@ func (c *Container) addPreview(name string, src image.Image) {
// Determine if we could generate an image preview.
if src != nil {
// Get the minimum dimension.
var w, h = minsize(src.Bounds().Dx(), src.Bounds().Dy(), FileIconSize)
var w, h = minsize(src.Bounds().Dx(), src.Bounds().Dy(), ThumbSize)
var img *image.NRGBA
// Downscale the image.
img = imaging.Resize(src, w, h, imaging.Lanczos)
// Crop to a square.
img = imaging.CropCenter(img, FileIconSize, FileIconSize)
img = imaging.CropCenter(img, ThumbSize, ThumbSize)
// Copy the image to a pixbuf.
gimg.SetFromPixbuf(gts.RenderPixbuf(img))
@ -315,7 +324,7 @@ func (c *Container) addPreview(name string, src image.Image) {
// BLOAT!!! Make an overlay of an event box that, when hovered, will show
// something that allows closing the image.
del, _ := gtk.ButtonNewFromIconName("window-close", gtk.ICON_SIZE_DIALOG)
del, _ := gtk.ButtonNewFromIconName("window-close-symbolic", gtk.ICON_SIZE_DIALOG)
del.SetVAlign(gtk.ALIGN_CENTER)
del.SetHAlign(gtk.ALIGN_CENTER)
del.SetTooltipText("Remove " + name)
@ -325,7 +334,7 @@ func (c *Container) addPreview(name string, src image.Image) {
primitives.AttachCSS(del, deleteAttBtnCSS)
ovl, _ := gtk.OverlayNew()
ovl.SetSizeRequest(FileIconSize, FileIconSize)
ovl.SetSizeRequest(ThumbSize, ThumbSize)
ovl.Add(gimg)
ovl.AddOverlay(del)
ovl.Show()
@ -334,6 +343,24 @@ func (c *Container) addPreview(name string, src image.Image) {
c.Box.PackStart(ovl, false, false, 0)
}
func iconFromName(filename string) string {
switch t := mime.TypeByExtension(filepath.Ext(filename)); {
case strings.HasPrefix(t, "image"):
return "image-x-generic-symbolic"
case strings.HasPrefix(t, "audio"):
return "audio-x-generic-symbolic"
case strings.HasPrefix(t, "application"):
return "application-x-appliance-symbolic"
case strings.HasPrefix(t, "text"):
fallthrough
default:
return "text-x-generic-symbolic"
}
}
func minsize(w, h, maxsz int) (int, int) {
if w < h {
// return the scaled width as max

View File

@ -35,25 +35,13 @@ var textCSS = primitives.PrepareCSS(`
}
.message-input * {
background-color: @theme_base_color;
transition: linear 50ms background-color;
/* Legacy styling
border: 1px solid alpha(@theme_fg_color, 0.2);
border-radius: 4px;
*/
}
.message-input:focus * {
background-color: mix(
@theme_base_color,
@theme_selected_bg_color,
0.15
);
/* Legacy styling
border-color: @theme_selected_bg_color;
*/
}
`)

View File

@ -7,7 +7,6 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
)
@ -41,7 +40,7 @@ var usernameCSS = primitives.PrepareCSS(`
`)
func NewContainer() *Container {
avatar := rich.NewIcon(AvatarSize, imgutil.Round(true))
avatar := rich.NewIcon(AvatarSize)
avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize)
avatar.Show()

View File

@ -0,0 +1,105 @@
package roundimage
import (
"math"
"github.com/gotk3/gotk3/cairo"
"github.com/gotk3/gotk3/gtk"
)
const (
pi = math.Pi
circle = 2 * math.Pi
)
type Image struct {
*gtk.Image
Radius float64
}
// NewImage creates a new round image. If radius is 0, then it will be half the
// dimensions. If the radius is less than 0, then nothing is rounded.
func NewImage(radius float64) (*Image, error) {
i, err := gtk.ImageNew()
if err != nil {
return nil, err
}
image := &Image{Image: i, Radius: radius}
// Connect to the draw callback and clip the context.
i.Connect("draw", func(i *gtk.Image, cc *cairo.Context) bool {
var w = float64(i.GetAllocatedWidth())
var h = float64(i.GetAllocatedHeight())
var min = w
// Use the smallest side for radius calculation.
if h < w {
min = h
}
// Copy the variables in case we need to change them.
var r = image.Radius
// We have to do this so the arc paint doesn't leave back a black
// background instead of the usual alpha.
// cc.SetSourceRGBA(255, 255, 255, 0)
switch {
// If radius is less than 0, then don't round.
case r < 0:
break
// If radius is 0, then we have to calculate our own radius.:This only
// works if the image is a square.
case r == 0:
// Calculate the radius by dividing a side by 2.
r = (min / 2)
// Draw an arc from 0deg to 360deg.
cc.Arc(w/2, h/2, r, 0, circle)
cc.SetSourceRGBA(255, 255, 255, 0)
// Clip the image with the arc we drew.
cc.Clip()
// If radius is more than 0, then we have to calculate the radius from
// the edges.
case r > 0:
// StackOverflow is godly.
// https://stackoverflow.com/a/6959843.
// Account for the offset.
// r += o
// Radius should be largest a single side divided by 2.
if max := min / 2; r > max {
r = max
}
// Draw 4 arcs at 4 corners.
cc.Arc(0+r, 0+r, r, 2*(pi/2), 3*(pi/2)) // top left
cc.Arc(w-r, 0+r, r, 3*(pi/2), 4*(pi/2)) // top right
cc.Arc(w-r, h-r, r, 0*(pi/2), 1*(pi/2)) // bottom right
cc.Arc(0+r, h-r, r, 1*(pi/2), 2*(pi/2)) // bottom left
// Close the created path.
cc.ClosePath()
cc.SetSourceRGBA(255, 255, 255, 0)
// Clip the image with the arc we drew.
cc.Clip()
}
// Paint the changes.
cc.Paint()
return false
})
return image, nil
}
func (i *Image) SetRadius(r float64) {
i.Radius = r
}

View File

@ -7,6 +7,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
@ -14,9 +15,10 @@ import (
type IconerFn = func(context.Context, cchat.IconContainer) (func(), error)
// Icon represents a rounded image container.
type Icon struct {
*gtk.Revealer
Image *gtk.Image
Image *roundimage.Image
procs []imgutil.Processor
size int
@ -35,7 +37,7 @@ func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
sizepx = DefaultIconSize
}
img, _ := gtk.ImageNew()
img, _ := roundimage.NewImage(0)
img.Show()
img.SetSizeRequest(sizepx, sizepx)
@ -92,7 +94,7 @@ func (i *Icon) SetPlaceholderIcon(iconName string, iconSzPx int) {
i.SetSize(iconSzPx)
if iconName != "" {
primitives.SetImageIcon(i.Image, iconName, iconSzPx)
primitives.SetImageIcon(i.Image.Image, iconName, iconSzPx)
}
}
@ -128,11 +130,7 @@ func (i *Icon) SetIconUnsafe(url string) {
}
func (i *Icon) updateAsync() {
if i.size > 0 {
httputil.AsyncImageSized(i.Image, i.url, i.size, i.size, i.procs...)
} else {
httputil.AsyncImage(i.Image, i.url, i.procs...)
}
httputil.AsyncImageSized(i.Image, i.url, i.size, i.size, i.procs...)
}
type ToggleButtonImage struct {

View File

@ -17,7 +17,7 @@ type ToggleButtonImage struct {
clicked func(bool)
err error
icon bool // whether or not the button has an icon
icon string // whether or not the button has an icon
iconSz int
}
@ -62,8 +62,8 @@ func (b *ToggleButtonImage) SetNormal() {
b.SetLabelUnsafe(b.GetLabel())
b.menu.SetItems(b.extraMenu)
if b.icon {
b.Image.SetPlaceholderIcon("user-available-symbolic", b.Image.Size())
if b.icon != "" {
b.Image.SetPlaceholderIcon(b.icon, b.Image.Size())
}
}
@ -73,7 +73,7 @@ func (b *ToggleButtonImage) SetLoading() {
// Reset the menu.
b.menu.SetItems(b.extraMenu)
if b.icon {
if b.icon != "" {
b.Image.SetPlaceholderIcon("content-loading-symbolic", b.Image.Size())
}
}
@ -87,13 +87,13 @@ func (b *ToggleButtonImage) SetFailed(err error, retry func()) {
b.menu.AddItems(b.extraMenu...)
// If we have an icon set, then we can use the failed icon.
if b.icon {
if b.icon != "" {
b.Image.SetPlaceholderIcon("computer-fail-symbolic", b.Image.Size())
}
}
func (b *ToggleButtonImage) SetPlaceholderIcon(iconName string, iconSzPx int) {
b.icon = true
b.icon = iconName
b.Image.SetPlaceholderIcon(iconName, iconSzPx)
}
@ -102,12 +102,6 @@ func (b *ToggleButtonImage) SetIcon(url string) {
}
func (b *ToggleButtonImage) SetIconUnsafe(url string) {
b.icon = true
b.icon = ""
b.Image.SetIconUnsafe(url)
}
// type Row struct {
// gtk.Box
// Button *ToggleButtonImage
// Children *gtk.Box
// }

View File

@ -6,7 +6,6 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/service/config"
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
)
@ -21,7 +20,6 @@ type header struct {
func newHeader(svc cchat.Service) *header {
b := rich.NewToggleButtonImage(svc.Name())
b.Image.AddProcessors(imgutil.Round(true))
b.Image.SetPlaceholderIcon("folder-remote-symbolic", IconSize)
b.SetRelief(gtk.RELIEF_NONE)
b.SetMode(true)

View File

@ -10,13 +10,12 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/service/loading"
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
const ChildrenMargin = 24
const IconSize = 20
const IconSize = 32
type Controller interface {
RowSelected(*ServerRow, cchat.ServerMessage)
@ -36,8 +35,6 @@ type Row struct {
func NewRow(parent breadcrumb.Breadcrumber, name text.Rich) *Row {
button := button.NewToggleButtonImage(name)
button.Box.SetHAlign(gtk.ALIGN_START)
button.Image.AddProcessors(imgutil.Round(true))
button.Image.SetSize(IconSize)
button.SetRelief(gtk.RELIEF_NONE)
button.Show()
@ -64,6 +61,7 @@ func (r *Row) SetLabelUnsafe(name text.Rich) {
func (r *Row) SetIconer(v interface{}) {
if iconer, ok := v.(cchat.Icon); ok {
r.Button.Image.SetSize(IconSize)
r.Button.Image.AsyncSetIconer(iconer, "Error getting server icon URL")
}
}
@ -134,7 +132,9 @@ func (r *Row) childrenFailed(err error) {
func (r *Row) childrenDone() {
r.loaded = true
r.SetDone()
// I don't think this is supposed to be called here...
// r.SetDone()
}
// SetSelected is used for highlighting the current message server.

View File

@ -17,6 +17,7 @@ import (
)
const IconSize = 32
const IconName = "face-plain-symbolic"
// Controller extends server.RowController to add session.
type Controller interface {
@ -67,7 +68,7 @@ func NewLoading(parent breadcrumb.Breadcrumber, id, name string, ctrl Controller
func newRow(parent breadcrumb.Breadcrumber, name text.Rich, ctrl Controller) *Row {
srow := server.NewRow(parent, name)
srow.Button.SetPlaceholderIcon("user-invisible-symbolic", IconSize)
srow.Button.SetPlaceholderIcon(IconName, IconSize)
srow.Show()
// Bind the row to .session in CSS.
@ -142,7 +143,7 @@ func (r *Row) DisconnectSession() {
r.Reset()
// Set the offline icon to the button.
r.Button.Image.SetPlaceholderIcon("user-invisible-symbolic", IconSize)
r.Button.Image.SetPlaceholderIcon(IconName, IconSize)
// Also unselect the button.
r.Button.SetActive(false)