diff --git a/src/gopheros/device/video/console/device.go b/src/gopheros/device/video/console/device.go index 8956396..c4891f1 100644 --- a/src/gopheros/device/video/console/device.go +++ b/src/gopheros/device/video/console/device.go @@ -1,6 +1,9 @@ package console -import "image/color" +import ( + "gopheros/device/video/console/font" + "image/color" +) // ScrollDir defines a scroll direction. type ScrollDir uint8 @@ -43,3 +46,11 @@ type Device interface { // supported colors should be a no-op. SetPaletteColor(uint8, color.RGBA) } + +// FontSetter is an interface implemented by console devices that +// support loadable bitmap fonts. +// +// SetFont selects a bitmap font to be used by the console. +type FontSetter interface { + SetFont(*font.Font) +} diff --git a/src/gopheros/device/video/console/font/font.go b/src/gopheros/device/video/console/font/font.go new file mode 100644 index 0000000..9a7c94c --- /dev/null +++ b/src/gopheros/device/video/console/font/font.go @@ -0,0 +1,97 @@ +package font + +var ( + // The list of available fonts. + availableFonts []*Font +) + +// Font describes a bitmap font that can be used by a console device. +type Font struct { + // The name of the font + Name string + + // The width of each glyph in pixels. + GlyphWidth uint32 + + // The height of each glyph in pixels. + GlyphHeight uint32 + + // The recommended console resolution for this font. + RecommendedWidth uint32 + RecommendedHeight uint32 + + // Font priority (lower is better). When auto-detecting a font to use, the font with + // the lowest priority will be preferred + Priority uint32 + + // The number of bytes describing a row in a glyph. + BytesPerRow uint32 + + // The font bitmap. Each character consists of BytesPerRow * Height + // bytes where each bit indicates whether a pixel should be set to the + // foreground or the background color. + Data []byte +} + +// FindByName looks up a font instance by name. If the font is not found then +// the function returns nil. +func FindByName(name string) *Font { + for _, f := range availableFonts { + if f.Name == name { + return f + } + } + + return nil +} + +// BestFit returns the best font from the available font list given the +// specified console dimensions. If multiple fonts match the dimension criteria +// then their priority attribute is used to select one. +// +// The algorithm for selecting the best font is the following: +// For each font: +// - calculate the sum of abs differences between the font recommended dimension +// and the console dimensions. +// - if the font score is lower than the current best font's score then the +// font becomes the new best font. +// - if the font score is equal to the current best font's score then the +// font with the lowest priority becomes the new best font. +func BestFit(consoleWidth, consoleHeight uint32) *Font { + var ( + best *Font + bestDelta uint32 + absDeltaW, absDeltaH, absDelta uint32 + ) + + for _, f := range availableFonts { + if f.RecommendedWidth > consoleWidth { + absDeltaW = f.RecommendedWidth - consoleWidth + } else { + absDeltaW = consoleWidth - f.RecommendedWidth + } + + if f.RecommendedHeight > consoleHeight { + absDeltaH = f.RecommendedHeight - consoleHeight + } else { + absDeltaH = consoleHeight - f.RecommendedHeight + } + + absDelta = absDeltaW + absDeltaH + + if best == nil { + best = f + bestDelta = absDelta + continue + } + + if best.Priority < f.Priority || absDelta > bestDelta { + continue + } + + best = f + bestDelta = absDelta + } + + return best +} diff --git a/src/gopheros/device/video/console/font/font_test.go b/src/gopheros/device/video/console/font/font_test.go new file mode 100644 index 0000000..c2c8fd3 --- /dev/null +++ b/src/gopheros/device/video/console/font/font_test.go @@ -0,0 +1,59 @@ +package font + +import "testing" + +func TestFindByName(t *testing.T) { + defer func(origList []*Font) { + availableFonts = origList + }(availableFonts) + + availableFonts = []*Font{ + &Font{Name: "foo"}, + &Font{Name: "bar"}, + } + + exp := availableFonts[1] + if got := FindByName("bar"); got != exp { + t.Fatalf("expected to get font: %v; got %v", exp, got) + } + + if got := FindByName("not-existing-font"); got != nil { + t.Fatalf("expected to get nil for a font that does not exist; got %v", got) + } +} + +func TestBestFit(t *testing.T) { + defer func(origList []*Font) { + availableFonts = origList + }(availableFonts) + + availableFonts = []*Font{ + &Font{Name: "retina1", RecommendedWidth: 2560, RecommendedHeight: 1600, Priority: 2}, + &Font{Name: "retina2", RecommendedWidth: 2560, RecommendedHeight: 1600, Priority: 1}, + &Font{Name: "default", RecommendedWidth: 800, RecommendedHeight: 600, Priority: 0}, + &Font{Name: "standard", RecommendedWidth: 1024, RecommendedHeight: 768, Priority: 0}, + } + + specs := []struct { + consW, consH uint32 + expName string + }{ + {320, 200, "default"}, + {800, 600, "default"}, + {1024, 768, "standard"}, + {3000, 3000, "retina2"}, + {2500, 600, "retina2"}, + } + + for specIndex, spec := range specs { + got := BestFit(spec.consW, spec.consH) + if got == nil { + t.Errorf("[spec %d] unable to find a font", specIndex) + continue + } + + if got.Name != spec.expName { + t.Errorf("[spec %d] expected to get font %q; got %q", specIndex, spec.expName, got.Name) + } + } +}