font rendering
 
-       should combine all that's needed to render a text into some struct
-       also, with background nw being a thing, a padding might be nice
-       or could separate bg from fg rendering
+       with background now being a thing, a padding might be nice
+       that or maybe separate bg from fg rendering
 
        it may also be feasible to get rid of SDL_ttf and use freetype
        directly to eliminate the unneccessary surface creation
 
                        break;
                case SDL_WINDOWEVENT_RESIZED:
                        viewport.Resize(event.data1, event.data2);
-                       interface.Resize(viewport);
                        break;
                default:
                        break;
 
--- /dev/null
+#ifndef BLANK_GRAPHICS_TEXT_HPP_
+#define BLANK_GRAPHICS_TEXT_HPP_
+
+#include "align.hpp"
+#include "Texture.hpp"
+#include "../model/SpriteModel.hpp"
+
+#include <string>
+#include <glm/glm.hpp>
+
+
+namespace blank {
+
+class Font;
+class Viewport;
+
+class Text {
+
+public:
+       Text() noexcept;
+
+       void Set(const Font &, const char *);
+       void Set(const Font &f, const std::string &s) {
+               Set(f, s.c_str());
+       }
+
+       void Position(const glm::vec3 &p) noexcept {
+               pos = p;
+       }
+       void Position(
+               const glm::vec3 &p,
+               Gravity g
+       ) noexcept {
+               pos = p;
+               grav = g;
+               pivot = g;
+               dirty = true;
+       }
+       void Position(
+               const glm::vec3 &p,
+               Gravity g,
+               Gravity pv
+       ) noexcept {
+               pos = p;
+               grav = g;
+               pivot = pv;
+               dirty = true;
+       }
+
+       void Foreground(const glm::vec4 &col) noexcept { fg = col; }
+       void Background(const glm::vec4 &col) noexcept { bg = col; }
+
+       void Render(Viewport &) noexcept;
+
+       void Show() noexcept { visible = true; }
+       void Hide() noexcept { visible = false; }
+       void Toggle() noexcept { visible = !visible; }
+       bool Visible() const noexcept { return visible; }
+
+private:
+       void Update();
+
+private:
+       Texture tex;
+       SpriteModel sprite;
+       glm::vec4 bg;
+       glm::vec4 fg;
+       glm::vec2 size;
+       glm::vec3 pos;
+       Gravity grav;
+       Gravity pivot;
+       bool dirty;
+       bool visible;
+
+};
+
+}
+
+#endif
 
 #ifndef BLANK_GRAPHICS_VIEWPORT_HPP_
 #define BLANK_GRAPHICS_VIEWPORT_HPP_
 
+#include "align.hpp"
 #include "BlendedSprite.hpp"
 #include "BlockLighting.hpp"
 #include "Camera.hpp"
 #include "DirectionalLighting.hpp"
 
 #include <glm/glm.hpp>
-#include <SDL.h>
 
 
 namespace blank {
        void Clear() noexcept;
        void ClearDepth() noexcept;
 
+       void SetCursor(const glm::vec3 &);
+       void SetCursor(const glm::vec3 &, Gravity = Gravity::NORTH_WEST);
+       void MoveCursor(const glm::vec3 &);
+       const glm::mat4 &Cursor() const noexcept { return cursor; }
+
        BlockLighting &ChunkProgram() noexcept;
        DirectionalLighting &EntityProgram() noexcept;
        DirectionalLighting &HUDProgram() noexcept;
 
        const glm::mat4 &Perspective() const noexcept { return cam.Projection(); }
        const glm::mat4 &Ortho() const noexcept { return canv.Projection(); }
-       const glm::mat4 &CenterTransform() const noexcept { return center; }
 
 private:
-       SDL_GLContext ctx;
        Camera cam;
        Canvas canv;
 
-       glm::mat4 center;
+       glm::mat4 cursor;
 
        BlockLighting chunk_prog;
        DirectionalLighting entity_prog;
 
--- /dev/null
+#ifndef BLANK_GRAPHICS_ALIGN_HPP_
+#define BLANK_GRAPHICS_ALIGN_HPP_
+
+#include <glm/glm.hpp>
+
+
+namespace blank {
+
+enum class Align {
+       LEFT,
+       CENTER,
+       RIGHT,
+};
+
+enum class Gravity {
+       NORTH_WEST,
+       NORTH,
+       NORTH_EAST,
+       WEST,
+       CENTER,
+       EAST,
+       SOUTH_WEST,
+       SOUTH,
+       SOUTH_EAST,
+};
+
+inline Align get_x(Gravity g) noexcept {
+       return Align(int(g) % 3);
+}
+
+inline Align get_y(Gravity g) noexcept {
+       return Align(int(g) / 3);
+}
+
+inline Gravity get_gravity(Align x, Align y) noexcept {
+       return Gravity(int(y) * 3 + int(x));
+}
+
+inline glm::vec2 align(
+       Gravity g,
+       const glm::vec2 &size,
+       const glm::vec2 &offset = glm::vec2(0.0f, 0.0f)
+) {
+       return glm::vec2(
+               size.x * 0.5 * int(get_x(g)) + offset.x,
+               size.y * 0.5 * int(get_y(g)) + offset.y
+       );
+}
+
+}
+
+#endif
 
+#include "BlendedSprite.hpp"
 #include "Font.hpp"
 #include "Format.hpp"
+#include "Text.hpp"
 #include "Texture.hpp"
+#include "Viewport.hpp"
 
 #include <algorithm>
 #include <cstring>
 }
 
 
+Text::Text() noexcept
+: tex()
+, sprite()
+, bg(1.0f, 1.0f, 1.0f, 0.0f)
+, fg(1.0f, 1.0f, 1.0f, 1.0f)
+, size(0.0f)
+, pos(0.0f)
+, grav(Gravity::NORTH_WEST)
+, pivot(Gravity::NORTH_WEST)
+, dirty(false)
+, visible(false) {
+
+}
+
+void Text::Set(const Font &font, const char *text) {
+       font.Render(text, tex);
+       size = font.TextSize(text);
+       dirty = true;
+}
+
+void Text::Update() {
+       sprite.LoadRect(size.x, size.y, align(pivot, size));
+       dirty = false;
+}
+
+void Text::Render(Viewport &viewport) noexcept {
+       if (dirty) {
+               Update();
+       }
+       BlendedSprite &prog = viewport.SpriteProgram();
+       viewport.SetCursor(pos, grav);
+       prog.SetM(viewport.Cursor());
+       prog.SetTexture(tex);
+       prog.SetBG(bg);
+       prog.SetFG(fg);
+       sprite.Draw();
+}
+
+
 Texture::Texture()
 : handle(0)
 , width(0)
 
 Viewport::Viewport()
 : cam()
 , canv()
-, center(1.0f)
+, cursor(1.0f)
 , chunk_prog()
 , entity_prog()
 , sprite_prog()
        cam.Aspect(fw, fh);
        canv.Resize(fw, fh);
 
-       center = glm::translate(glm::vec3(fw * 0.5f, fh * 0.5f, 0.0f));
-
        chunk_prog.SetProjection(Perspective());
        if (active_prog == HUD) {
                entity_prog.SetProjection(Ortho());
 }
 
 
+void Viewport::SetCursor(const glm::vec3 &pos) {
+       cursor[3].x = pos.x;
+       cursor[3].y = pos.y;
+       cursor[3].z = pos.z;
+}
+
+void Viewport::SetCursor(const glm::vec3 &pos, Gravity grav) {
+       glm::vec2 p(align(grav, canv.Size(), glm::vec2(pos) + canv.Offset()));
+       cursor[3].x = p.x;
+       cursor[3].y = p.y;
+       cursor[3].z = pos.z;
+}
+
+void Viewport::MoveCursor(const glm::vec3 &d) {
+       cursor[3].x += d.x;
+       cursor[3].y += d.y;
+       cursor[3].z += d.z;
+}
+
+
 BlockLighting &Viewport::ChunkProgram() noexcept {
        if (active_prog != CHUNK) {
                chunk_prog.Activate();
 
 #ifndef BLANK_UI_HUD_H_
 #define BLANK_UI_HUD_H_
 
-#include "../graphics/Texture.hpp"
+#include "../graphics/Text.hpp"
 #include "../model/Model.hpp"
 #include "../model/OutlineModel.hpp"
-#include "../model/SpriteModel.hpp"
 
 #include <glm/glm.hpp>
 
        Model::Buffer block_buf;
        glm::mat4 block_transform;
 
-       Texture block_label;
-       SpriteModel label_sprite;
-       glm::mat4 label_transform;
+       Text block_label;
 
        bool block_visible;
 
 
 #include "../app/FPSController.hpp"
 #include "../app/IntervalTimer.hpp"
 #include "../graphics/Font.hpp"
+#include "../graphics/Text.hpp"
 #include "../model/geometry.hpp"
 #include "../model/OutlineModel.hpp"
 #include "../world/Block.hpp"
        void HandleRelease(const SDL_MouseButtonEvent &);
        void Handle(const SDL_MouseWheelEvent &);
 
-       void Resize(const Viewport &);
-
        void FaceBlock();
        void TurnBlock();
 
        OutlineModel outline;
        glm::mat4 outline_transform;
 
-       bool show_counter;
-       Texture counter_tex;
-       SpriteModel counter_sprite;
-       glm::mat4 counter_transform;
-       float counter_x;
+       Text counter_text;
 
        Config config;
 
 
 , block_buf()
 , block_transform(1.0f)
 , block_label()
-, label_sprite()
-, label_transform(1.0f)
 , block_visible(false)
 , crosshair() {
        block_transform = glm::translate(block_transform, glm::vec3(50.0f, 50.0f, 0.0f));
        });
        crosshair.colors.resize(4, { 10.0f, 10.0f, 10.0f });
        crosshair.Invalidate();
+
+       block_label.Position(
+               glm::vec3(50.0f, 85.0f, 0.0f),
+               Gravity::NORTH_WEST,
+               Gravity::NORTH
+       );
+       block_label.Foreground(glm::vec4(1.0f));
+       block_label.Background(glm::vec4(0.5f));
 }
 
 
        type.FillModel(block_buf, b.Transform());
        block.Update(block_buf);
 
-       font.Render(type.label.c_str(), block_label);
-       glm::vec2 size(font.TextSize(type.label.c_str()));
-       label_sprite.LoadRect(size.x, size.y);
-       label_transform = glm::translate(glm::vec3(
-               std::max(5.0f, 50.0f - std::round(size.x * 0.5f)),
-               70.0f + size.y,
-               0.75f
-       ));
+       block_label.Set(font, type.label);
 
        block_visible = type.visible;
 }
        world_prog.SetFogDensity(0.0f);
 
        viewport.EnableInvertBlending();
-       world_prog.SetM(viewport.CenterTransform());
+       viewport.SetCursor(glm::vec3(0.0f), Gravity::CENTER);
+       world_prog.SetM(viewport.Cursor());
        crosshair.Draw();
 
        if (block_visible) {
                viewport.DisableBlending();
                world_prog.SetM(block_transform);
                block.Draw();
-
-               BlendedSprite &sprite_prog = viewport.SpriteProgram();
-               sprite_prog.SetM(label_transform);
-               sprite_prog.SetTexture(block_label);
-               sprite_prog.SetFG(glm::vec4(1.0f));
-               sprite_prog.SetBG(glm::vec4(0.5f));
-               label_sprite.Draw();
+               block_label.Render(viewport);
        }
 }
 
 , aim_normal()
 , outline()
 , outline_transform(1.0f)
-, show_counter(false)
-, counter_tex()
-, counter_sprite()
-, counter_transform(1.0f)
-, counter_x(935.0f)
+, counter_text()
 , config(config)
 , place_timer(256)
 , remove_timer(256)
 , selection(1)
 , fwd(0)
 , rev(0) {
+       counter_text.Hide();
+       counter_text.Position(glm::vec3(-25.0f, 25.0f, 0.0f), Gravity::NORTH_EAST);
+       counter_text.Foreground(glm::vec4(1.0f));
+       counter_text.Background(glm::vec4(0.5f));
        hud.Display(selection);
 }
 
 }
 
 void Interface::ToggleCounter() {
-       if ((show_counter = !show_counter)) {
+       counter_text.Toggle();
+       if (counter_text.Visible()) {
                UpdateCounter();
        }
 }
        std::stringstream s;
        s << std::setprecision(3) << counter.AvgRunning() << "ms";
        std::string text = s.str();
-       font.Render(text.c_str(), counter_tex);
-       glm::vec2 size(font.TextSize(text.c_str()));
-       counter_sprite.LoadRect(size.x, size.y);
-       counter_transform = glm::translate(glm::vec3(
-               counter_x - size.x,
-               25.0f,
-               0.75f
-       ));
+       counter_text.Set(font, text);
 }
 
 
 }
 
 
-void Interface::Resize(const Viewport &viewport) {
-       counter_x = viewport.Width() - 25.0f;
-}
-
-
 void Interface::Update(int dt) {
        ctrl.Velocity(glm::vec3(fwd - rev) * config.move_velocity);
        ctrl.Update(dt);
                CheckAim();
        }
 
-       if (show_counter && counter.Changed()) {
+       if (counter_text.Visible() && counter.Changed()) {
                UpdateCounter();
        }
 }
                outline.Draw();
        }
 
-       if (show_counter) {
-               BlendedSprite &sprite_prog = viewport.SpriteProgram();
-               sprite_prog.SetM(counter_transform);
-               sprite_prog.SetTexture(counter_tex);
-               counter_sprite.Draw();
+       if (counter_text.Visible()) {
+               counter_text.Render(viewport);
        }
 
        hud.Render(viewport);