shithub: tinygl

ref: d360857b634010648582ad3b0413a996106c8fdb
dir: /include-demo/stb_tilemap_editor.h/

View raw version
// stb_tilemap_editor.h - v0.42 - Sean Barrett - http://nothings.org/stb
// placed in the public domain - not copyrighted - first released 2014-09
//
// Embeddable tilemap editor for C/C++
//
//
// TABLE OF CONTENTS
//    FAQ
//    How to compile/use the library
//    Additional configuration macros
//    API documentation
//    Info on editing multiple levels
//    Revision history
//    Todo
//    Credits
//    License
//
//
// FAQ
//
//   Q: What counts as a tilemap for this library?
//
//   A: An array of rectangles, where each rectangle contains a small
//      stack of images.
//
//   Q: What are the limitations?
//
//   A: Maps are limited to 4096x4096 in dimension.
//      Each map square can only contain a stack of at most 32 images.
//      A map can only use up to 32768 distinct image tiles.
//
//   Q: How do I compile this?
//
//   A: You need to #define several symbols before #including it, but only
//      in one file. This will cause all the function definitions to be
//      generated in that file. See the "HOW TO COMPILE" section.
//
//   Q: What advantages does this have over a standalone editor?
//
//   A: For one, you can integrate the editor into your game so you can
//      flip between editing and testing without even switching windows.
//      For another, you don't need an XML parser to get at the map data.
//
//   Q: Can I live-edit my game maps?
//
//   A: Not really, the editor keeps its own map representation.
//
//   Q: How do I save and load maps?
//
//   A: You have to do this yourself. The editor provides serialization
//      functions (get & set) for reading and writing the map it holds.
//      You can choose whatever format you want to store the map to on
//      disk; you just need to provide functions to convert. (For example,
//      I actually store the editor's map representation to disk basically
//      as-is; then I have a single function that converts from the editor
//      map representation to the game representation, which is used both
//      to go from editor-to-game and from loaded-map-to-game.)
//
//   Q: I want to have tiles change appearance based on what's
//      adjacent, or other tile-display/substitution trickiness.
//
//   A: You can do this when you convert from the editor's map
//      representation to the game representation, but there's
//      no way to show this live in the editor.
//
//   Q: The editor appears to be put map location (0,0) at the top left?
//      I want to use a different coordinate system in my game (e.g. y
//      increasing upwards, or origin at the center).
//
//   A: You can do this when you convert from the editor's map
//      representation to the game representation. (Don't forget to
//      translate link coordinates as well!)
//
//   Q: The editor appears to put pixel (0,0) at the top left? I want
//      to use a different coordinate system in my game.
//
//   A: The editor defines an "editor pixel coordinate system" with
//      (0,0) at the top left and requires you to display things in
//      that coordinate system. You can freely remap those coordinates
//      to anything you want on screen.
//
//   Q: How do I scale the user interface?
//
//   A: Since you do all the rendering, you can scale up all the rendering
//      calls that the library makes to you. If you do, (a) you need
//      to also scale up the mouse coordinates, and (b) you may want
//      to scale the map display back down so that you're only scaling
//      the UI and not everything. See the next question.
//
//   Q: How do I scale the map display?
//
//   A: Use stbte_set_spacing() to change the size that the map is displayed
//      at. Note that the "callbacks" to draw tiles are used for both drawing
//      the map and drawing the tile palette, so that callback may need to
//      draw at two different scales. You should choose the scales to match
//       You can tell them apart because the
//      tile palette gets NULL for the property pointer.
//
//   Q: How does object editing work?
//
//   A: One way to think of this is that in the editor, you're placing
//      spawners, not objects. Each spawner must be tile-aligned, because
//      it's only a tile editor. Each tile (stack of layers) gets
//      an associated set of properties, and it's up to you to
//      determine what properties should appear for a given tile,
//      based on e.g. the spawners that are in it.
//
//   Q: How are properties themselves handled?
//
//   A: All properties, regardless of UI behavior, are internally floats.
//      Each tile has an array of floats associated with it, which is
//      passed back to you when drawing the tiles so you can draw
//      objects appropriately modified by the properties.
//
//   Q: What if I want to have two different objects/spawners in
//      one tile, both of which have their own properties?
//
//   A: Make sure STBTE_MAX_PROPERTIES is large enough for the sum of
//      properties in both objects, and then you have to explicitly
//      map the property slot #s to the appropriate objects. They'll
//      still all appear in a single property panel; there's no way
//      to get multiple panels.
//
//   Q: Can I do one-to-many linking?
//
//   A: The library only supports one link per tile. However, you
//      can have multiple tiles all link to a single tile. So, you
//      can fake one-to-many linking by linking in the reverse
//      direction.
//
//   Q: What if I have two objects in the same tile, and they each
//      need an independent link? Or I have two kinds of link associated
//      with a single object?
//
//   A: There is no way to do this. (Unless you can reverse one link.)
//
//   Q: How does cut & paste interact with object properties & links?
//
//   A: Currently the library has no idea which properties or links
//      are associated with which layers of a tile. So currently, the
//      library will only copy properties & links if the layer panel
//      is set to allow all layers to be copied, OR if you set the
//      "props" in the layer panel to "always". Similarly, you can
//      set "props" to "none" so it will never copy.
//
//   Q: What happens if the library gets a memory allocation failure
//      while I'm editing? Will I lose my work?
//
//   A: The library allocates all editor memory when you create
//      the tilemap. It allocates a maximally-sized map and a
//      fixed-size undo buffer (and the fixed-size copy buffer
//      is static), and never allocates memory while it's running.
//      So it can't fail due to running out of memory.
//
//   Q: What happens if the library crashes while I'm editing? Will
//      I lose my work?
//
//   A: Yes. Save often.
//
//
// HOW TO COMPILE
//
//   This header file contains both the header file and the
//   implementation file in one. To create the implementation,
//   in one source file define a few symbols first and then
//   include this header:
//
//      #define STB_TILEMAP_EDITOR_IMPLEMENTATION
//      // this triggers the implementation
//
//      void STBTE_DRAW_RECT(int x0, int y0, int x1, int y1, unsigned int color);
//      // this must draw a filled rectangle (exclusive on right/bottom)
//      // color = (r<<16)|(g<<8)|(b)
//
//      void STBTE_DRAW_TILE(int x0, int y0,
//                    unsigned short id, int highlight, float *data);
//      // this draws the tile image identified by 'id' in one of several
//      // highlight modes (see STBTE_drawmode_* in the header section);
//      // if 'data' is NULL, it's drawing the tile in the palette; if 'data'
//      // is not NULL, it's drawing a tile on the map, and that is the data
//      // associated with that map tile
//
//      #include "stb_tilemap_editor.h"
//
//   Optionally you can define the following functions before the include;
//   note these must be macros (but they can just call a function) so
//   this library can #ifdef to detect if you've defined them:
//
//      #define STBTE_PROP_TYPE(int n, short *tiledata, float *params) ...
//      // Returns the type of the n'th property of a given tile, which
//      // controls how it is edited. Legal types are:
//      //     0                    /* no editable property in this slot */
//      //     STBTE_PROP_int       /* uses a slider to adjust value     */
//      //     STBTE_PROP_float     /* uses a weird multi-axis control   */
//      //     STBTE_PROP_bool      /* uses a checkbox to change value   */
//      // And you can bitwise-OR in the following flags:
//      //     STBTE_PROP_disabled
//      // Note that all of these are stored as floats in the param array.
//      // The integer slider is limited in precision based on the space
//      // available on screen, so for wide-ranged integers you may want
//      // to use floats instead.
//      //
//      // Since the tiledata is passed to you, you can choose which property
//      // is bound to that slot based on that data.
//      //
//      // Changing the type of a parameter does not cause the underlying
//      // value to be clamped to the type min/max except when the tile is
//      // explicitly selected.
//
//      #define STBTE_PROP_NAME(int n, short *tiledata, float *params) ...
//      // these return a string with the name for slot #n in the float
//      // property list for the tile.
//
//      #define STBTE_PROP_MIN(int n, short *tiledata) ...your code here...
//      #define STBTE_PROP_MAX(int n, short *tiledata) ...your code here...
//      // These return the allowable range for the property values for
//      // the specified slot. It is never called for boolean types.
//
//      #define STBTE_PROP_FLOAT_SCALE(int n, short *tiledata, float *params)
//      // This rescales the float control for a given property; by default
//      // left mouse drags add integers, right mouse drags adds fractions,
//      // but you can rescale this per-property.
//
//      #define STBTE_FLOAT_CONTROL_GRANULARITY       ... value ...
//      // This returns the number of pixels of mouse motion necessary
//      // to advance the object float control. Default is 4
//
//      #define STBTE_ALLOW_LINK(short *src, float *src_data,  \
//                               short *dest, float *dest_data) ...your code...
//      // this returns true or false depending on whether you allow a link
//      // to be drawn from a tile 'src' to a tile 'dest'. if you don't
//      // define this, linking will not be supported
//
//      #define STBTE_LINK_COLOR(short *src, float *src_data,  \
//                               short *dest, float *dest_data) ...your code...
//      // return a color encoded as a 24-bit unsigned integer in the
//      // form 0xRRGGBB. If you don't define this, default colors will
//      // be used.
//
//
//      [[ support for those below is not implemented yet ]]
//
//      #define STBTE_HITTEST_TILE(x0,y0,id,mx,my)   ...your code here...
//      // this returns true or false depending on whether the mouse
//      // pointer at mx,my is over (touching) a tile of type 'id'
//      // displayed at x0,y0. Normally stb_tilemap_editor just does
//      // this hittest based on the tile geometry, but if you have
//      // tiles whose images extend out of the tile, you'll need this.
//
// ADDITIONAL CONFIGURATION
//
//   The following symbols set static limits which determine how much
//   memory will be allocated for the editor. You can override them
//   by making similar definitions, but memory usage will increase.
//
//      #define STBTE_MAX_TILEMAP_X      200   // max 4096
//      #define STBTE_MAX_TILEMAP_Y      200   // max 4096
//      #define STBTE_MAX_LAYERS         8     // max 32
//      #define STBTE_MAX_CATEGORIES     100
//      #define STBTE_UNDO_BUFFER_BYTES  (1 << 24) // 16 MB
//      #define STBTE_MAX_COPY           90000  // e.g. 300x300
//      #define STBTE_MAX_PROPERTIES     10     // max properties per tile
//
// API
//
//   Further documentation appears in the header-file section below.
//
// EDITING MULTIPLE LEVELS
//
//   You can only have one active editor instance. To switch between multiple
//   levels, you can either store the levels in your own format and copy them
//   in and out of the editor format, or you can create multiple stbte_tilemap
//   objects and switch between them. The latter has the advantage that each
//   stbte_tilemap keeps its own undo state. (The clipboard is global, so
//   either approach allows cut&pasting between levels.)
//
// REVISION HISTORY
//   0.42  fix compilation errors
//   0.41  fix warnings
//   0.40  fix warning
//   0.39  fix warning
//   0.38  fix warning
//   0.37  fix warning
//   0.36  minor compiler support
//   0.35  layername button changes
//          - layername buttons grow with the layer panel
//          - fix stbte_create_map being declared as stbte_create
//          - fix declaration of stbte_create_map
//   0.30  properties release
//          - properties panel for editing user-defined "object" properties
//          - can link each tile to one other tile
//          - keyboard interface
//          - fix eraser tool bug (worked in complex cases, failed in simple)
//          - undo/redo tools have visible disabled state
//          - tiles on higher layers draw on top of adjacent lower-layer tiles
//   0.20  erasable release
//          - eraser tool
//          - fix bug when pasting into protected layer
//          - better color scheme
//          - internal-use color picker
//   0.10  initial release
//
// TODO
//
//   Separate scroll state for each category
//   Implement paint bucket
//   Support STBTE_HITTEST_TILE above
//  ?Cancel drags by clicking other button? - may be fixed
//   Finish support for toolbar at side
//
// CREDITS
//
//
//   Main editor & features
//      Sean Barrett
//   Additional features:
//      Josh Huelsman
//   Bugfixes:
//      Ryan Whitworth
//      Eugene Opalev
//      Rob Loach
//      github:wernsey
//
// LICENSE
//
//   See end of file for license information.



///////////////////////////////////////////////////////////////////////
//
//   HEADER SECTION

#ifndef STB_TILEMAP_INCLUDE_STB_TILEMAP_EDITOR_H
#define STB_TILEMAP_INCLUDE_STB_TILEMAP_EDITOR_H

#ifdef _WIN32
  #ifndef _CRT_SECURE_NO_WARNINGS
  #define _CRT_SECURE_NO_WARNINGS
  #endif
  #include <stdlib.h>
  #include <stdio.h>
#endif

typedef struct stbte_tilemap stbte_tilemap;

// these are the drawmodes used in STBTE_DRAW_TILE
enum
{
   STBTE_drawmode_deemphasize = -1,
   STBTE_drawmode_normal      =  0,
   STBTE_drawmode_emphasize   =  1,
};

// these are the property types
#define STBTE_PROP_none     0
#define STBTE_PROP_int      1
#define STBTE_PROP_float    2
#define STBTE_PROP_bool     3
#define STBTE_PROP_disabled 4

////////
//
// creation
//

extern stbte_tilemap *stbte_create_map(int map_x, int map_y, int map_layers, int spacing_x, int spacing_y, int max_tiles);
// create an editable tilemap
//   map_x      : dimensions of map horizontally (user can change this in editor), <= STBTE_MAX_TILEMAP_X
//   map_y      : dimensions of map vertically (user can change this in editor)    <= STBTE_MAX_TILEMAP_Y
//   map_layers : number of layers to use (fixed), <= STBTE_MAX_LAYERS
//   spacing_x  : initial horizontal distance between left edges of map tiles in stb_tilemap_editor pixels
//   spacing_y  : initial vertical distance between top edges of map tiles in stb_tilemap_editor pixels
//   max_tiles  : maximum number of tiles that can defined
//
// If insufficient memory, returns NULL

extern void stbte_define_tile(stbte_tilemap *tm, unsigned short id, unsigned int layermask, const char * category);
// call this repeatedly for each tile to install the tile definitions into the editable tilemap
//   tm        : tilemap created by stbte_create_map
//   id        : unique identifier for each tile, 0 <= id < 32768
//   layermask : bitmask of which layers tile is allowed on: 1 = layer 0, 255 = layers 0..7
//               (note that onscreen, the editor numbers the layers from 1 not 0)
//               layer 0 is the furthest back, layer 1 is just in front of layer 0, etc
//   category  : which category this tile is grouped in

extern void stbte_set_display(int x0, int y0, int x1, int y1);
// call this once to set the size; if you resize, call it again


/////////
//
// every frame
//

extern void stbte_draw(stbte_tilemap *tm);

extern void stbte_tick(stbte_tilemap *tm, float time_in_seconds_since_last_frame);

////////////
//
//  user input
//

// if you're using SDL, call the next function for SDL_MOUSEMOTION, SDL_MOUSEBUTTONDOWN, SDL_MOUSEBUTTONUP, SDL_MOUSEWHEEL;
// the transformation lets you scale from SDL mouse coords to stb_tilemap_editor coords
extern void stbte_mouse_sdl(stbte_tilemap *tm, const void *sdl_event, float xscale, float yscale, int xoffset, int yoffset);

// otherwise, hook these up explicitly:
extern void stbte_mouse_move(stbte_tilemap *tm, int x, int y, int shifted, int scrollkey);
extern void stbte_mouse_button(stbte_tilemap *tm, int x, int y, int right, int down, int shifted, int scrollkey);
extern void stbte_mouse_wheel(stbte_tilemap *tm, int x, int y, int vscroll);

// note: at the moment, mouse wheel events (SDL_MOUSEWHEEL) are ignored.

// for keyboard, define your own mapping from keys to the following actions.
// this is totally optional, as all features are accessible with the mouse
enum stbte_action
{
   STBTE_tool_select,
   STBTE_tool_brush,
   STBTE_tool_erase,
   STBTE_tool_rectangle,
   STBTE_tool_eyedropper,
   STBTE_tool_link,
   STBTE_act_toggle_grid,
   STBTE_act_toggle_links,
   STBTE_act_undo,
   STBTE_act_redo,
   STBTE_act_cut,
   STBTE_act_copy,
   STBTE_act_paste,
   STBTE_scroll_left,
   STBTE_scroll_right,
   STBTE_scroll_up,
   STBTE_scroll_down,
};
extern void stbte_action(stbte_tilemap *tm, enum stbte_action act);

////////////////
//
//  save/load
//
//  There is no editor file format. You have to save and load the data yourself
//  through the following functions. You can also use these functions to get the
//  data to generate game-formatted levels directly. (But make sure you save
//  first! You may also want to autosave to a temp file periodically, etc etc.)

#define STBTE_EMPTY    -1

extern void stbte_get_dimensions(stbte_tilemap *tm, int *max_x, int *max_y);
// get the dimensions of the level, since the user can change them

extern short* stbte_get_tile(stbte_tilemap *tm, int x, int y);
// returns an array of shorts that is 'map_layers' in length. each short is
// either one of the tile_id values from define_tile, or STBTE_EMPTY.

extern float *stbte_get_properties(stbte_tilemap *tm, int x, int y);
// get the property array associated with the tile at x,y. this is an
// array of floats that is STBTE_MAX_PROPERTIES in length; you have to
// interpret the slots according to the semantics you've chosen

extern void stbte_get_link(stbte_tilemap *tm, int x, int y, int *destx, int *desty);
// gets the link associated with the tile at x,y.

extern void stbte_set_dimensions(stbte_tilemap *tm, int max_x, int max_y);
// set the dimensions of the level, overrides previous stbte_create_map()
// values or anything the user has changed

extern void stbte_clear_map(stbte_tilemap *tm);
// clears the map, including the region outside the defined region, so if the
// user expands the map, they won't see garbage there

extern void stbte_set_tile(stbte_tilemap *tm, int x, int y, int layer, signed short tile);
// tile is your tile_id from define_tile, or STBTE_EMPTY

extern void stbte_set_property(stbte_tilemap *tm, int x, int y, int n, float val);
// set the value of the n'th slot of the tile at x,y

extern void stbte_set_link(stbte_tilemap *tm, int x, int y, int destx, int desty);
// set a link going from x,y to destx,desty. to force no link,
// use destx=desty=-1

////////
//
// optional
//

extern void stbte_set_background_tile(stbte_tilemap *tm, short id);
// selects the tile to fill the bottom layer with and used to clear bottom tiles to;
// should be same ID as

extern void stbte_set_sidewidths(int left, int right);
// call this once to set the left & right side widths. don't call
// it again since the user can change it

extern void stbte_set_spacing(stbte_tilemap *tm, int spacing_x, int spacing_y, int palette_spacing_x, int palette_spacing_y);
// call this to set the spacing of map tiles and the spacing of palette tiles.
// if you rescale your display, call it again (e.g. you can implement map zooming yourself)

extern void stbte_set_layername(stbte_tilemap *tm, int layer, const char *layername);
// sets a string name for your layer that shows in the layer selector. note that this
// makes the layer selector wider. 'layer' is from 0..(map_layers-1)

#endif

#ifdef STB_TILEMAP_EDITOR_IMPLEMENTATION

#ifndef STBTE_ASSERT
#define STBTE_ASSERT assert
#include <assert.h>
#endif

#ifdef _MSC_VER
#define STBTE__NOTUSED(v)  (void)(v)
#else
#define STBTE__NOTUSED(v)  (void)sizeof(v)
#endif

#ifndef STBTE_MAX_TILEMAP_X
#define STBTE_MAX_TILEMAP_X      200
#endif

#ifndef STBTE_MAX_TILEMAP_Y
#define STBTE_MAX_TILEMAP_Y      200
#endif

#ifndef STBTE_MAX_LAYERS
#define STBTE_MAX_LAYERS         8
#endif

#ifndef STBTE_MAX_CATEGORIES
#define STBTE_MAX_CATEGORIES     100
#endif

#ifndef STBTE_MAX_COPY
#define STBTE_MAX_COPY           65536
#endif

#ifndef STBTE_UNDO_BUFFER_BYTES
#define STBTE_UNDO_BUFFER_BYTES  (1 << 24) // 16 MB
#endif

#ifndef STBTE_PROP_TYPE
#define STBTE__NO_PROPS
#define STBTE_PROP_TYPE(n,td,tp)   0
#endif

#ifndef STBTE_PROP_NAME
#define STBTE_PROP_NAME(n,td,tp)  ""
#endif

#ifndef STBTE_MAX_PROPERTIES
#define STBTE_MAX_PROPERTIES           10
#endif

#ifndef STBTE_PROP_MIN
#define STBTE_PROP_MIN(n,td,tp)  0
#endif

#ifndef STBTE_PROP_MAX
#define STBTE_PROP_MAX(n,td,tp)  100.0
#endif

#ifndef STBTE_PROP_FLOAT_SCALE
#define STBTE_PROP_FLOAT_SCALE(n,td,tp)  1   // default scale size
#endif

#ifndef STBTE_FLOAT_CONTROL_GRANULARITY
#define STBTE_FLOAT_CONTROL_GRANULARITY 4
#endif


#define STBTE__UNDO_BUFFER_COUNT  (STBTE_UNDO_BUFFER_BYTES>>1)

#if STBTE_MAX_TILEMAP_X > 4096 || STBTE_MAX_TILEMAP_Y > 4096
#error "Maximum editable map size is 4096 x 4096"
#endif
#if STBTE_MAX_LAYERS > 32
#error "Maximum layers allowed is 32"
#endif
#if STBTE_UNDO_BUFFER_COUNT & (STBTE_UNDO_BUFFER_COUNT-1)
#error "Undo buffer size must be a power of 2"
#endif

#if STBTE_MAX_PROPERTIES == 0
#define STBTE__NO_PROPS
#endif

#ifdef STBTE__NO_PROPS
#undef STBTE_MAX_PROPERTIES
#define STBTE_MAX_PROPERTIES 1  // so we can declare arrays
#endif

typedef struct
{
   short x,y;
} stbte__link;

enum
{
   STBTE__base,
   STBTE__outline,
   STBTE__text,

   STBTE__num_color_aspects,
};

enum
{
   STBTE__idle,
   STBTE__over,
   STBTE__down,
   STBTE__over_down,
   STBTE__selected,
   STBTE__selected_over,
   STBTE__disabled,
   STBTE__num_color_states,
};

enum
{
   STBTE__cexpander,
   STBTE__ctoolbar,
   STBTE__ctoolbar_button,
   STBTE__cpanel,
   STBTE__cpanel_sider,
   STBTE__cpanel_sizer,
   STBTE__cscrollbar,
   STBTE__cmapsize,
   STBTE__clayer_button,
   STBTE__clayer_hide,
   STBTE__clayer_lock,
   STBTE__clayer_solo,
   STBTE__ccategory_button,

   STBTE__num_color_modes,
};

#ifdef STBTE__COLORPICKER
static char *stbte__color_names[] =
{
   "expander", "toolbar", "tool button", "panel",
   "panel c1", "panel c2", "scollbar", "map button",
   "layer", "hide", "lock", "solo",
   "category",
};
#endif // STBTE__COLORPICKER

      // idle,    over,     down,    over&down, selected, sel&over, disabled
static int stbte__color_table[STBTE__num_color_modes][STBTE__num_color_aspects][STBTE__num_color_states] =
{
   {
      { 0x000000, 0x84987c, 0xdcdca8, 0xdcdca8, 0x40c040, 0x60d060, 0x505050, },
      { 0xa4b090, 0xe0ec80, 0xffffc0, 0xffffc0, 0x80ff80, 0x80ff80, 0x606060, },
      { 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0x909090, },
   }, {
      { 0x808890, 0x606060, 0x606060, 0x606060, 0x606060, 0x606060, 0x606060, },
      { 0x605860, 0x606060, 0x606060, 0x606060, 0x606060, 0x606060, 0x606060, },
      { 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, },
   }, {
      { 0x3c5068, 0x7088a8, 0x647488, 0x94b4dc, 0x8890c4, 0x9caccc, 0x404040, },
      { 0x889cb8, 0x889cb8, 0x889cb8, 0x889cb8, 0x84c4e8, 0xacc8ff, 0x0c0c08, },
      { 0xbcc4cc, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0x707074, },
   }, {
      { 0x403848, 0x403010, 0x403010, 0x403010, 0x403010, 0x403010, 0x303024, },
      { 0x68546c, 0xc08040, 0xc08040, 0xc08040, 0xc08040, 0xc08040, 0x605030, },
      { 0xf4e4ff, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0x909090, },
   }, {
      { 0xb4b04c, 0xacac60, 0xc0ffc0, 0xc0ffc0, 0x40c040, 0x60d060, 0x505050, },
      { 0xa0a04c, 0xd0d04c, 0xffff80, 0xffff80, 0x80ff80, 0x80ff80, 0x606060, },
      { 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0x909090, },
   }, {
      { 0x40c440, 0x60d060, 0xc0ffc0, 0xc0ffc0, 0x40c040, 0x60d060, 0x505050, },
      { 0x40c040, 0x80ff80, 0x80ff80, 0x80ff80, 0x80ff80, 0x80ff80, 0x606060, },
      { 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0x909090, },
   }, {
      { 0x9090ac, 0xa0a0b8, 0xbcb8cc, 0xbcb8cc, 0x909040, 0x909040, 0x909040, },
      { 0xa0a0b8, 0xb0b4d0, 0xa0a0b8, 0xa0a0b8, 0xa0a050, 0xa0a050, 0xa0a050, },
      { 0x808088, 0x808030, 0x808030, 0x808030, 0x808030, 0x808030, 0x808030, },
   }, {
      { 0x704c70, 0x885c8c, 0x9c68a4, 0xb870bc, 0xb490bc, 0xb490bc, 0x302828, },
      { 0x646064, 0xcca8d4, 0xc060c0, 0xa07898, 0xe0b8e0, 0xe0b8e0, 0x403838, },
      { 0xdccce4, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0x909090, },
   }, {
      { 0x704c70, 0x885c8c, 0x9c68a4, 0xb870bc, 0xb490bc, 0xb490bc, 0x302828, },
      { 0xb09cb4, 0xcca8d4, 0xc060c0, 0xa07898, 0xe0b8e0, 0xe0b8e0, 0x403838, },
      { 0xdccce4, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0xffffff, 0x909090, },
   }, {
      { 0x646494, 0x888cb8, 0xb0b0b0, 0xb0b0cc, 0x9c9cf4, 0x8888b0, 0x50506c, },
      { 0x9090a4, 0xb0b4d4, 0xb0b0dc, 0xb0b0cc, 0xd0d0fc, 0xd0d4f0, 0x606060, },
      { 0xb4b4d4, 0xe4e4ff, 0xffffff, 0xffffff, 0xe0e4ff, 0xececff, 0x909090, },
   }, {
      { 0x646444, 0x888c64, 0xb0b0b0, 0xb0b088, 0xaca858, 0x88886c, 0x505050, },
      { 0x88886c, 0xb0b490, 0xb0b0b0, 0xb0b088, 0xd8d898, 0xd0d4b0, 0x606060, },
      { 0xb4b49c, 0xffffd8, 0xffffff, 0xffffd4, 0xffffdc, 0xffffcc, 0x909090, },
   }, {
      { 0x906464, 0xb48c8c, 0xd4b0b0, 0xdcb0b0, 0xff9c9c, 0xc88888, 0x505050, },
      { 0xb47c80, 0xd4b4b8, 0xc4a8a8, 0xdcb0b0, 0xffc0c0, 0xfce8ec, 0x606060, },
      { 0xe0b4b4, 0xffdcd8, 0xffd8d4, 0xffe0e4, 0xffece8, 0xffffff, 0x909090, },
   }, {
      { 0x403848, 0x403848, 0x403848, 0x886894, 0x7c80c8, 0x7c80c8, 0x302828, },
      { 0x403848, 0x403848, 0x403848, 0x403848, 0x7c80c8, 0x7c80c8, 0x403838, },
      { 0xc8c4c8, 0xffffff, 0xffffff, 0xffffff, 0xe8e8ec, 0xffffff, 0x909090, },
   },
};

#define STBTE_COLOR_TILEMAP_BACKGROUND      0x000000
#define STBTE_COLOR_TILEMAP_BORDER          0x203060
#define STBTE_COLOR_TILEMAP_HIGHLIGHT       0xffffff
#define STBTE_COLOR_GRID                    0x404040
#define STBTE_COLOR_SELECTION_OUTLINE1      0xdfdfdf
#define STBTE_COLOR_SELECTION_OUTLINE2      0x303030
#define STBTE_COLOR_TILEPALETTE_OUTLINE     0xffffff
#define STBTE_COLOR_TILEPALETTE_BACKGROUND  0x000000

#ifndef STBTE_LINK_COLOR
#define STBTE_LINK_COLOR(src,sp,dest,dp)    0x5030ff
#endif

#ifndef STBTE_LINK_COLOR_DRAWING
#define STBTE_LINK_COLOR_DRAWING            0xff40ff
#endif

#ifndef STBTE_LINK_COLOR_DISALLOWED
#define STBTE_LINK_COLOR_DISALLOWED         0x602060
#endif


// disabled, selected, down, over
static unsigned char stbte__state_to_index[2][2][2][2] =
{
   {
      { { STBTE__idle    , STBTE__over          }, { STBTE__down    , STBTE__over_down }, },
      { { STBTE__selected, STBTE__selected_over }, { STBTE__down    , STBTE__over_down }, },
   },{
      { { STBTE__disabled, STBTE__disabled      }, { STBTE__disabled, STBTE__disabled  }, },
      { { STBTE__selected, STBTE__selected_over }, { STBTE__disabled, STBTE__disabled  }, },
   }
};
#define STBTE__INDEX_FOR_STATE(disable,select,down,over) stbte__state_to_index[disable][select][down][over]
#define STBTE__INDEX_FOR_ID(id,disable,select) STBTE__INDEX_FOR_STATE(disable,select,STBTE__IS_ACTIVE(id),STBTE__IS_HOT(id))

#define STBTE__FONT_HEIGHT    9
static short stbte__font_offset[95+16];
static short stbte__fontdata[769] =
{
   4,9,6,9,9,9,9,8,9,8,4,9,7,7,7,7,4,2,6,8,6,6,7,3,4,4,8,6,3,6,2,6,6,6,6,6,6,
   6,6,6,6,6,2,3,5,4,5,6,6,6,6,6,6,6,6,6,6,6,6,7,6,7,7,7,6,7,6,6,6,6,7,7,6,6,
   6,4,6,4,7,7,3,6,6,5,6,6,5,6,6,4,5,6,4,7,6,6,6,6,6,6,6,6,6,7,6,6,6,5,2,5,8,
   0,0,0,0,2,253,130,456,156,8,72,184,64,2,125,66,64,160,64,146,511,146,146,
   511,146,146,511,146,511,257,341,297,341,297,341,257,511,16,56,124,16,16,16,
   124,56,16,96,144,270,261,262,136,80,48,224,192,160,80,40,22,14,15,3,448,496,
   496,240,232,20,10,5,2,112,232,452,450,225,113,58,28,63,30,60,200,455,257,
   257,0,0,0,257,257,455,120,204,132,132,159,14,4,4,14,159,132,132,204,120,8,
   24,56,120,56,24,8,32,48,56,60,56,48,32,0,0,0,0,111,111,7,7,0,0,7,7,34,127,
   127,34,34,127,127,34,36,46,107,107,58,18,99,51,24,12,102,99,48,122,79,93,
   55,114,80,4,7,3,62,127,99,65,65,99,127,62,8,42,62,28,28,62,42,8,8,8,62,62,
   8,8,128,224,96,8,8,8,8,8,8,96,96,96,48,24,12,6,3,62,127,89,77,127,62,64,66,
   127,127,64,64,98,115,89,77,71,66,33,97,73,93,119,35,24,28,22,127,127,16,39,
   103,69,69,125,57,62,127,73,73,121,48,1,1,113,121,15,7,54,127,73,73,127,54,
   6,79,73,105,63,30,54,54,128,246,118,8,28,54,99,65,20,20,20,20,65,99,54,28,
   8,2,3,105,109,7,2,30,63,33,45,47,46,124,126,19,19,126,124,127,127,73,73,127,
   54,62,127,65,65,99,34,127,127,65,99,62,28,127,127,73,73,73,65,127,127,9,9,
   9,1,62,127,65,73,121,121,127,127,8,8,127,127,65,65,127,127,65,65,32,96,64,
   64,127,63,127,127,8,28,54,99,65,127,127,64,64,64,64,127,127,6,12,6,127,127,
   127,127,6,12,24,127,127,62,127,65,65,65,127,62,127,127,9,9,15,6,62,127,65,
   81,49,127,94,127,127,9,25,127,102,70,79,73,73,121,49,1,1,127,127,1,1,63,127,
   64,64,127,63,15,31,48,96,48,31,15,127,127,48,24,48,127,127,99,119,28,28,119,
   99,7,15,120,120,15,7,97,113,89,77,71,67,127,127,65,65,3,6,12,24,48,96,65,
   65,127,127,8,12,6,3,6,12,8,64,64,64,64,64,64,64,3,7,4,32,116,84,84,124,120,
   127,127,68,68,124,56,56,124,68,68,68,56,124,68,68,127,127,56,124,84,84,92,
   24,8,124,126,10,10,56,380,324,324,508,252,127,127,4,4,124,120,72,122,122,
   64,256,256,256,506,250,126,126,16,56,104,64,66,126,126,64,124,124,24,56,28,
   124,120,124,124,4,4,124,120,56,124,68,68,124,56,508,508,68,68,124,56,56,124,
   68,68,508,508,124,124,4,4,12,8,72,92,84,84,116,36,4,4,62,126,68,68,60,124,
   64,64,124,124,28,60,96,96,60,28,28,124,112,56,112,124,28,68,108,56,56,108,
   68,284,316,352,320,508,252,68,100,116,92,76,68,8,62,119,65,65,127,127,65,
   65,119,62,8,16,24,12,12,24,24,12,4,
};

typedef struct
{
   short id;
   unsigned short category_id;
   char *category;
   unsigned int layermask;
} stbte__tileinfo;

#define MAX_LAYERMASK    (1 << (8*sizeof(unsigned int)))

typedef short stbte__tiledata;

#define STBTE__NO_TILE   -1

enum
{
   STBTE__panel_toolbar,
   STBTE__panel_colorpick,
   STBTE__panel_info,
   STBTE__panel_layers,
   STBTE__panel_props,
   STBTE__panel_categories,
   STBTE__panel_tiles,

   STBTE__num_panel,
};

enum
{
   STBTE__side_left,
   STBTE__side_right,
   STBTE__side_top,
   STBTE__side_bottom,
};

enum
{
   STBTE__tool_select,
   STBTE__tool_brush,
   STBTE__tool_erase,
   STBTE__tool_rect,
   STBTE__tool_eyedrop,
   STBTE__tool_fill,
   STBTE__tool_link,

   STBTE__tool_showgrid,
   STBTE__tool_showlinks,

   STBTE__tool_undo,
   STBTE__tool_redo,
   // copy/cut/paste aren't included here because they're displayed differently

   STBTE__num_tool,
};

// icons are stored in the 0-31 range of ASCII in the font
static int toolchar[] = { 26,24,25,20,23,22,18, 19,17, 29,28, };

enum
{
   STBTE__propmode_default,
   STBTE__propmode_always,
   STBTE__propmode_never,
};

enum
{
   STBTE__paint,

   // from here down does hittesting
   STBTE__tick,
   STBTE__mousemove,
   STBTE__mousewheel,
   STBTE__leftdown,
   STBTE__leftup,
   STBTE__rightdown,
   STBTE__rightup,
};

typedef struct
{
   int expanded, mode;
   int delta_height;     // number of rows they've requested for this
   int side;
   int width,height;
   int x0,y0;
} stbte__panel;

typedef struct
{
   int x0,y0,x1,y1,color;
} stbte__colorrect;

#define STBTE__MAX_DELAYRECT 256

typedef struct
{
   int tool, active_event;
   int active_id, hot_id, next_hot_id;
   int event;
   int mx,my, dx,dy;
   int ms_time;
   int shift, scrollkey;
   int initted;
   int side_extended[2];
   stbte__colorrect delayrect[STBTE__MAX_DELAYRECT];
   int delaycount;
   int show_grid, show_links;
   int brush_state; // used to decide which kind of erasing
   int eyedrop_x, eyedrop_y, eyedrop_last_layer;
   int pasting, paste_x, paste_y;
   int scrolling, start_x, start_y;
   int last_mouse_x, last_mouse_y;
   int accum_x, accum_y;
   int linking;
   int dragging;
   int drag_x, drag_y, drag_w, drag_h;
   int drag_offx, drag_offy, drag_dest_x, drag_dest_y;
   int undoing;
   int has_selection, select_x0, select_y0, select_x1, select_y1;
   int sx,sy;
   int x0,y0,x1,y1, left_width, right_width; // configurable widths
   float alert_timer;
   const char *alert_msg;
   float dt;
   stbte__panel panel[STBTE__num_panel];
   short copybuffer[STBTE_MAX_COPY][STBTE_MAX_LAYERS];
   float copyprops[STBTE_MAX_COPY][STBTE_MAX_PROPERTIES];
#ifdef STBTE_ALLOW_LINK
   stbte__link copylinks[STBTE_MAX_COPY];
#endif
   int copy_src_x, copy_src_y;
   stbte_tilemap *copy_src;
   int copy_width,copy_height,has_copy,copy_has_props;
} stbte__ui_t;

// there's only one UI system at a time, so we can globalize this
static stbte__ui_t stbte__ui = { STBTE__tool_brush, 0 };

#define STBTE__INACTIVE()     (stbte__ui.active_id == 0)
#define STBTE__IS_ACTIVE(id)  (stbte__ui.active_id == (id))
#define STBTE__IS_HOT(id)     (stbte__ui.hot_id    == (id))

#define STBTE__BUTTON_HEIGHT            (STBTE__FONT_HEIGHT + 2 * STBTE__BUTTON_INTERNAL_SPACING)
#define STBTE__BUTTON_INTERNAL_SPACING  (2 + (STBTE__FONT_HEIGHT>>4))

typedef struct
{
   const char *name;
   int locked;
   int hidden;
} stbte__layer;

enum
{
   STBTE__unlocked,
   STBTE__protected,
   STBTE__locked,
};

struct stbte_tilemap
{
    stbte__tiledata data[STBTE_MAX_TILEMAP_Y][STBTE_MAX_TILEMAP_X][STBTE_MAX_LAYERS];
    float props[STBTE_MAX_TILEMAP_Y][STBTE_MAX_TILEMAP_X][STBTE_MAX_PROPERTIES];
    #ifdef STBTE_ALLOW_LINK
    stbte__link link[STBTE_MAX_TILEMAP_Y][STBTE_MAX_TILEMAP_X];
    int linkcount[STBTE_MAX_TILEMAP_Y][STBTE_MAX_TILEMAP_X];
    #endif
    int max_x, max_y, num_layers;
    int spacing_x, spacing_y;
    int palette_spacing_x, palette_spacing_y;
    int scroll_x,scroll_y;
    int cur_category, cur_tile, cur_layer;
    char *categories[STBTE_MAX_CATEGORIES];
    int num_categories, category_scroll;
    stbte__tileinfo *tiles;
    int num_tiles, max_tiles, digits;
    unsigned char undo_available_valid;
    unsigned char undo_available;
    unsigned char redo_available;
    unsigned char padding;
    int cur_palette_count;
    int palette_scroll;
    int tileinfo_dirty;
    stbte__layer layerinfo[STBTE_MAX_LAYERS];
    int has_layer_names;
    int layername_width;
    int layer_scroll;
    int propmode;
    int solo_layer;
    int undo_pos, undo_len, redo_len;
    short background_tile;
    unsigned char id_in_use[32768>>3];
    short *undo_buffer;
};

static char *default_category = (char*) "[unassigned]";

static void stbte__init_gui(void)
{
   int i,n;
   stbte__ui.initted = 1;
   // init UI state
   stbte__ui.show_links = 1;
   for (i=0; i < STBTE__num_panel; ++i) {
      stbte__ui.panel[i].expanded     = 1; // visible if not autohidden
      stbte__ui.panel[i].delta_height = 0;
      stbte__ui.panel[i].side         = STBTE__side_left;
   }
   stbte__ui.panel[STBTE__panel_toolbar  ].side = STBTE__side_top;
   stbte__ui.panel[STBTE__panel_colorpick].side = STBTE__side_right;

   if (stbte__ui.left_width == 0)
      stbte__ui.left_width = 80;
   if (stbte__ui.right_width == 0)
      stbte__ui.right_width = 80;

   // init font
   n=95+16;
   for (i=0; i < 95+16; ++i) {
      stbte__font_offset[i] = n;
      n += stbte__fontdata[i];
   }
}

stbte_tilemap *stbte_create_map(int map_x, int map_y, int map_layers, int spacing_x, int spacing_y, int max_tiles)
{
   int i;
   stbte_tilemap *tm;
   STBTE_ASSERT(map_layers >= 0 && map_layers <= STBTE_MAX_LAYERS);
   STBTE_ASSERT(map_x >= 0 && map_x <= STBTE_MAX_TILEMAP_X);
   STBTE_ASSERT(map_y >= 0 && map_y <= STBTE_MAX_TILEMAP_Y);
   if (map_x < 0 || map_y < 0 || map_layers < 0 ||
       map_x > STBTE_MAX_TILEMAP_X || map_y > STBTE_MAX_TILEMAP_Y || map_layers > STBTE_MAX_LAYERS)
      return NULL;

   if (!stbte__ui.initted)
      stbte__init_gui();

   tm = (stbte_tilemap *) malloc(sizeof(*tm) + sizeof(*tm->tiles) * max_tiles + STBTE_UNDO_BUFFER_BYTES);
   if (tm == NULL)
      return NULL;

   tm->tiles = (stbte__tileinfo *) (tm+1);
   tm->undo_buffer = (short *) (tm->tiles + max_tiles);
   tm->num_layers = map_layers;
   tm->max_x = map_x;
   tm->max_y = map_y;
   tm->spacing_x = spacing_x;
   tm->spacing_y = spacing_y;
   tm->scroll_x = 0;
   tm->scroll_y = 0;
   tm->palette_scroll = 0;
   tm->palette_spacing_x = spacing_x+1;
   tm->palette_spacing_y = spacing_y+1;
   tm->cur_category = -1;
   tm->cur_tile = 0;
   tm->solo_layer = -1;
   tm->undo_len = 0;
   tm->redo_len = 0;
   tm->undo_pos = 0;
   tm->category_scroll = 0;
   tm->layer_scroll = 0;
   tm->propmode = 0;
   tm->has_layer_names = 0;
   tm->layername_width = 0;
   tm->undo_available_valid = 0;

   for (i=0; i < tm->num_layers; ++i) {
      tm->layerinfo[i].hidden = 0;
      tm->layerinfo[i].locked = STBTE__unlocked;
      tm->layerinfo[i].name   = 0;
   }

   tm->background_tile = STBTE__NO_TILE;
   stbte_clear_map(tm);

   tm->max_tiles = max_tiles;
   tm->num_tiles = 0;
   for (i=0; i < 32768/8; ++i)
      tm->id_in_use[i] = 0;
   tm->tileinfo_dirty = 1;
   return tm;
}

void stbte_set_background_tile(stbte_tilemap *tm, short id)
{
   int i;
   STBTE_ASSERT(id >= -1);
   // STBTE_ASSERT(id < 32768);
   if (id < -1)
      return;
   for (i=0; i < STBTE_MAX_TILEMAP_X * STBTE_MAX_TILEMAP_Y; ++i)
      if (tm->data[0][i][0] == -1)
         tm->data[0][i][0] = id;
   tm->background_tile = id;
}

void stbte_set_spacing(stbte_tilemap *tm, int spacing_x, int spacing_y, int palette_spacing_x, int palette_spacing_y)
{
   tm->spacing_x = spacing_x;
   tm->spacing_y = spacing_y;
   tm->palette_spacing_x = palette_spacing_x;
   tm->palette_spacing_y = palette_spacing_y;
}

void stbte_set_sidewidths(int left, int right)
{
   stbte__ui.left_width  = left;
   stbte__ui.right_width = right;
}

void stbte_set_display(int x0, int y0, int x1, int y1)
{
   stbte__ui.x0 = x0;
   stbte__ui.y0 = y0;
   stbte__ui.x1 = x1;
   stbte__ui.y1 = y1;
}

void stbte_define_tile(stbte_tilemap *tm, unsigned short id, unsigned int layermask, const char * category_c)
{
   char *category = (char *) category_c;
   STBTE_ASSERT(id < 32768);
   STBTE_ASSERT(tm->num_tiles < tm->max_tiles);
   STBTE_ASSERT((tm->id_in_use[id>>3]&(1<<(id&7))) == 0);
   if (id >= 32768 || tm->num_tiles >= tm->max_tiles || (tm->id_in_use[id>>3]&(1<<(id&7))))
      return;

   if (category == NULL)
      category = (char*) default_category;
   tm->id_in_use[id>>3] |= 1 << (id&7);
   tm->tiles[tm->num_tiles].category    = category;
   tm->tiles[tm->num_tiles].id        = id;
   tm->tiles[tm->num_tiles].layermask = layermask;
   ++tm->num_tiles;
   tm->tileinfo_dirty = 1;
}

static int stbte__text_width(const char *str);

void stbte_set_layername(stbte_tilemap *tm, int layer, const char *layername)
{
   STBTE_ASSERT(layer >= 0 && layer < tm->num_layers);
   if (layer >= 0 && layer < tm->num_layers) {
      int width;
      tm->layerinfo[layer].name = layername;
      tm->has_layer_names = 1;
      width = stbte__text_width(layername);
      tm->layername_width = (width > tm->layername_width ? width : tm->layername_width);
   }
}

void stbte_get_dimensions(stbte_tilemap *tm, int *max_x, int *max_y)
{
   *max_x = tm->max_x;
   *max_y = tm->max_y;
}

short* stbte_get_tile(stbte_tilemap *tm, int x, int y)
{
   STBTE_ASSERT(x >= 0 && x < tm->max_x && y >= 0 && y < tm->max_y);
   if (x < 0 || x >= STBTE_MAX_TILEMAP_X || y < 0 || y >= STBTE_MAX_TILEMAP_Y)
      return NULL;
   return tm->data[y][x];
}

float *stbte_get_properties(stbte_tilemap *tm, int x, int y)
{
   STBTE_ASSERT(x >= 0 && x < tm->max_x && y >= 0 && y < tm->max_y);
   if (x < 0 || x >= STBTE_MAX_TILEMAP_X || y < 0 || y >= STBTE_MAX_TILEMAP_Y)
      return NULL;
   return tm->props[y][x];
}

void stbte_get_link(stbte_tilemap *tm, int x, int y, int *destx, int *desty)
{
   int gx=-1,gy=-1;
   STBTE_ASSERT(x >= 0 && x < tm->max_x && y >= 0 && y < tm->max_y);
#ifdef STBTE_ALLOW_LINK
   if (x >= 0 && x < STBTE_MAX_TILEMAP_X && y >= 0 && y < STBTE_MAX_TILEMAP_Y) {
      gx = tm->link[y][x].x;
      gy = tm->link[y][x].y;
      if (gx >= 0)
         if (!STBTE_ALLOW_LINK(tm->data[y][x], tm->props[y][x], tm->data[gy][gx], tm->props[gy][gx]))
            gx = gy = -1;
   }
#endif
   *destx = gx;
   *desty = gy;
}

void stbte_set_property(stbte_tilemap *tm, int x, int y, int n, float val)
{
   tm->props[y][x][n] = val;
}

#ifdef STBTE_ALLOW_LINK
static void stbte__set_link(stbte_tilemap *tm, int src_x, int src_y, int dest_x, int dest_y, int undo_mode);
#endif

enum
{
   STBTE__undo_none,
   STBTE__undo_record,
   STBTE__undo_block,
};

void stbte_set_link(stbte_tilemap *tm, int x, int y, int destx, int desty)
{
#ifdef STBTE_ALLOW_LINK
   stbte__set_link(tm, x, y, destx, desty, STBTE__undo_none);
#else
   STBTE_ASSERT(0);
#endif
}


// returns an array of map_layers shorts. each short is either
// one of the tile_id values from define_tile, or STBTE_EMPTY

void stbte_set_dimensions(stbte_tilemap *tm, int map_x, int map_y)
{
   STBTE_ASSERT(map_x >= 0 && map_x <= STBTE_MAX_TILEMAP_X);
   STBTE_ASSERT(map_y >= 0 && map_y <= STBTE_MAX_TILEMAP_Y);
   if (map_x < 0 || map_y < 0 || map_x > STBTE_MAX_TILEMAP_X || map_y > STBTE_MAX_TILEMAP_Y)
      return;
   tm->max_x = map_x;
   tm->max_y = map_y;
}

void stbte_clear_map(stbte_tilemap *tm)
{
   int i,j;
   for (i=0; i < STBTE_MAX_TILEMAP_X * STBTE_MAX_TILEMAP_Y; ++i) {
      tm->data[0][i][0] = tm->background_tile;
      for (j=1; j < tm->num_layers; ++j)
         tm->data[0][i][j] = STBTE__NO_TILE;
      for (j=0; j < STBTE_MAX_PROPERTIES; ++j)
         tm->props[0][i][j] = 0;
      #ifdef STBTE_ALLOW_LINK
      tm->link[0][i].x = -1;
      tm->link[0][i].y = -1;
      tm->linkcount[0][i] = 0;
      #endif
   }
}

void stbte_set_tile(stbte_tilemap *tm, int x, int y, int layer, signed short tile)
{
   STBTE_ASSERT(x >= 0 && x < tm->max_x && y >= 0 && y < tm->max_y);
   STBTE_ASSERT(layer >= 0 && layer < tm->num_layers);
   STBTE_ASSERT(tile >= -1);
   //STBTE_ASSERT(tile < 32768);
   if (x < 0 || x >= STBTE_MAX_TILEMAP_X || y < 0 || y >= STBTE_MAX_TILEMAP_Y)
      return;
   if (layer < 0 || layer >= tm->num_layers || tile < -1)
      return;
   tm->data[y][x][layer] = tile;
}

static void stbte__choose_category(stbte_tilemap *tm, int category)
{
   int i,n=0;
   tm->cur_category = category;
   for (i=0; i < tm->num_tiles; ++i)
      if (tm->tiles[i].category_id == category || category == -1)
         ++n;
   tm->cur_palette_count = n;
   tm->palette_scroll = 0;
}

static int stbte__strequal(char *p, char *q)
{
   while (*p)
      if (*p++ != *q++) return 0;
   return *q == 0;
}

static void stbte__compute_tileinfo(stbte_tilemap *tm)
{
   int i,j;

   tm->num_categories=0;

   for (i=0; i < tm->num_tiles; ++i) {
      stbte__tileinfo *t = &tm->tiles[i];
      // find category
      for (j=0; j < tm->num_categories; ++j)
         if (stbte__strequal(t->category, tm->categories[j]))
            goto found;
      tm->categories[j] = t->category;
      ++tm->num_categories;
     found:
      t->category_id = (unsigned short) j;
   }

   // currently number of categories can never decrease because you
   // can't remove tile definitions, but let's get it right anyway
   if (tm->cur_category > tm->num_categories) {
      tm->cur_category = -1;
   }

   stbte__choose_category(tm, tm->cur_category);

   tm->tileinfo_dirty = 0;
}

static void stbte__prepare_tileinfo(stbte_tilemap *tm)
{
   if (tm->tileinfo_dirty)
      stbte__compute_tileinfo(tm);
}


/////////////////////// undo system ////////////////////////

// the undo system works by storing "commands" into a buffer, and
// then playing back those commands. undo and redo have to store
// the commands in different order.
//
// the commands are:
//
// 1)  end_of_undo_record
//       -1:short
//
// 2)  end_of_redo_record
//       -2:short
//
// 3)  tile update
//       tile_id:short (-1..32767)
//       x_coord:short
//       y_coord:short
//       layer:short (0..31)
//
// 4)  property update (also used for links)
//       value_hi:short
//       value_lo:short
//       y_coord:short
//       x_coord:short
//       property:short (256+prop#)
//
// Since we use a circular buffer, we might overwrite the undo storage.
// To detect this, before playing back commands we scan back and see
// if we see an end_of_undo_record before hitting the relevant boundary,
// it's wholly contained.
//
// When we read back through, we see them in reverse order, so
// we'll see the layer number or property number first
//
// To be clearer about the circular buffer, there are two cases:
//     1. a single record is larger than the whole buffer.
//        this is caught because the end_of_undo_record will
//        get overwritten.
//     2. multiple records written are larger than the whole
//        buffer, so some of them have been overwritten by
//        the later ones. this is handled by explicitly tracking
//        the undo length; we never try to parse the data that
//        got overwritten

// given two points, compute the length between them
#define stbte__wrap(pos)            ((pos) & (STBTE__UNDO_BUFFER_COUNT-1))

#define STBTE__undo_record  -2
#define STBTE__redo_record  -3
#define STBTE__undo_junk    -4  // this is written underneath the undo pointer, never used

static void stbte__write_undo(stbte_tilemap *tm, short value)
{
   int pos = tm->undo_pos;
   tm->undo_buffer[pos] = value;
   tm->undo_pos = stbte__wrap(pos+1);
   tm->undo_len += (tm->undo_len < STBTE__UNDO_BUFFER_COUNT-2);
   tm->redo_len -= (tm->redo_len > 0);
   tm->undo_available_valid = 0;
}

static void stbte__write_redo(stbte_tilemap *tm, short value)
{
   int pos = tm->undo_pos;
   tm->undo_buffer[pos] = value;
   tm->undo_pos = stbte__wrap(pos-1);
   tm->redo_len += (tm->redo_len < STBTE__UNDO_BUFFER_COUNT-2);
   tm->undo_len -= (tm->undo_len > 0);
   tm->undo_available_valid = 0;
}

static void stbte__begin_undo(stbte_tilemap *tm)
{
   tm->redo_len = 0;
   stbte__write_undo(tm, STBTE__undo_record);
   stbte__ui.undoing = 1;
   stbte__ui.alert_msg = 0; // clear alert if they start doing something
}

static void stbte__end_undo(stbte_tilemap *tm)
{
   if (stbte__ui.undoing) {
      // check if anything got written
      int pos = stbte__wrap(tm->undo_pos-1);
      if (tm->undo_buffer[pos] == STBTE__undo_record) {
         // empty undo record, move back
         tm->undo_pos = pos;
         STBTE_ASSERT(tm->undo_len > 0);
         tm->undo_len -= 1;
      }
      tm->undo_buffer[tm->undo_pos] = STBTE__undo_junk;
      // otherwise do nothing

      stbte__ui.undoing = 0;
   }
}

static void stbte__undo_record(stbte_tilemap *tm, int x, int y, int i, int v)
{
   STBTE_ASSERT(stbte__ui.undoing);
   if (stbte__ui.undoing) {
      stbte__write_undo(tm, v);
      stbte__write_undo(tm, x);
      stbte__write_undo(tm, y);
      stbte__write_undo(tm, i);
   }
}

static void stbte__redo_record(stbte_tilemap *tm, int x, int y, int i, int v)
{
   stbte__write_redo(tm, v);
   stbte__write_redo(tm, x);
   stbte__write_redo(tm, y);
   stbte__write_redo(tm, i);
}

static float stbte__extract_float(short s0, short s1)
{
   union { float f; short s[2]; } converter;
   converter.s[0] = s0;
   converter.s[1] = s1;
   return converter.f;
}

static short stbte__extract_short(float f, int slot)
{
   union { float f; short s[2]; } converter;
   converter.f = f;
   return converter.s[slot];
}

static void stbte__undo_record_prop(stbte_tilemap *tm, int x, int y, int i, short s0, short s1)
{
   STBTE_ASSERT(stbte__ui.undoing);
   if (stbte__ui.undoing) {
      stbte__write_undo(tm, s1);
      stbte__write_undo(tm, s0);
      stbte__write_undo(tm, x);
      stbte__write_undo(tm, y);
      stbte__write_undo(tm, 256+i);
   }
}

static void stbte__undo_record_prop_float(stbte_tilemap *tm, int x, int y, int i, float f)
{
   stbte__undo_record_prop(tm, x,y,i, stbte__extract_short(f,0), stbte__extract_short(f,1));
}

static void stbte__redo_record_prop(stbte_tilemap *tm, int x, int y, int i, short s0, short s1)
{
   stbte__write_redo(tm, s1);
   stbte__write_redo(tm, s0);
   stbte__write_redo(tm, x);
   stbte__write_redo(tm, y);
   stbte__write_redo(tm, 256+i);
}


static int stbte__undo_find_end(stbte_tilemap *tm)
{
   // first scan through for the end record
   int i, pos = stbte__wrap(tm->undo_pos-1);
   for (i=0; i < tm->undo_len;) {
      STBTE_ASSERT(tm->undo_buffer[pos] != STBTE__undo_junk);
      if (tm->undo_buffer[pos] == STBTE__undo_record)
         break;
      if (tm->undo_buffer[pos] >= 255)
         pos = stbte__wrap(pos-5), i += 5;
      else
         pos = stbte__wrap(pos-4), i += 4;
   }
   if (i >= tm->undo_len)
      return -1;
   return pos;
}

static void stbte__undo(stbte_tilemap *tm)
{
   int i, pos, endpos;
   endpos = stbte__undo_find_end(tm);
   if (endpos < 0)
      return;

   // we found a complete undo record
   pos = stbte__wrap(tm->undo_pos-1);

   // start a redo record
   stbte__write_redo(tm, STBTE__redo_record);

   // so now go back through undo and apply in reverse
   // order, and copy it to redo
   for (i=0; endpos != pos; i += 4) {
      int x,y,n,v;
      // get the undo entry
      n = tm->undo_buffer[pos];
      y = tm->undo_buffer[stbte__wrap(pos-1)];
      x = tm->undo_buffer[stbte__wrap(pos-2)];
      v = tm->undo_buffer[stbte__wrap(pos-3)];
      if (n >= 255) {
         short s0=0,s1=0;
         int v2 = tm->undo_buffer[stbte__wrap(pos-4)];
         pos = stbte__wrap(pos-5);
         if (n > 255) {
            float vf = stbte__extract_float(v, v2);
            s0 = stbte__extract_short(tm->props[y][x][n-256], 0);
            s1 = stbte__extract_short(tm->props[y][x][n-256], 1);
            tm->props[y][x][n-256] = vf;
         } else {
#ifdef STBTE_ALLOW_LINK
            s0 = tm->link[y][x].x;
            s1 = tm->link[y][x].y;
            stbte__set_link(tm, x,y, v, v2, STBTE__undo_none);
#endif
         }
         // write the redo entry
         stbte__redo_record_prop(tm, x, y, n-256, s0,s1);
         // apply the undo entry
      } else {
         pos = stbte__wrap(pos-4);
         // write the redo entry
         stbte__redo_record(tm, x, y, n, tm->data[y][x][n]);
         // apply the undo entry
         tm->data[y][x][n] = (short) v;
      }
   }
   // overwrite undo record with junk
   tm->undo_buffer[tm->undo_pos] = STBTE__undo_junk;
}

static int stbte__redo_find_end(stbte_tilemap *tm)
{
   // first scan through for the end record
   int i, pos = stbte__wrap(tm->undo_pos+1);
   for (i=0; i < tm->redo_len;) {
      STBTE_ASSERT(tm->undo_buffer[pos] != STBTE__undo_junk);
      if (tm->undo_buffer[pos] == STBTE__redo_record)
         break;
      if (tm->undo_buffer[pos] >= 255)
         pos = stbte__wrap(pos+5), i += 5;
      else
         pos = stbte__wrap(pos+4), i += 4;
   }
   if (i >= tm->redo_len)
      return -1; // this should only ever happen if redo buffer is empty
   return pos;
}

static void stbte__redo(stbte_tilemap *tm)
{
   // first scan through for the end record
   int i, pos, endpos;
   endpos = stbte__redo_find_end(tm);
   if (endpos < 0)
      return;

   // we found a complete redo record
   pos = stbte__wrap(tm->undo_pos+1);

   // start an undo record
   stbte__write_undo(tm, STBTE__undo_record);

   for (i=0; pos != endpos; i += 4) {
      int x,y,n,v;
      n = tm->undo_buffer[pos];
      y = tm->undo_buffer[stbte__wrap(pos+1)];
      x = tm->undo_buffer[stbte__wrap(pos+2)];
      v = tm->undo_buffer[stbte__wrap(pos+3)];
      if (n >= 255) {
         int v2 = tm->undo_buffer[stbte__wrap(pos+4)];
         short s0=0,s1=0;
         pos = stbte__wrap(pos+5);
         if (n > 255) {
            float vf = stbte__extract_float(v, v2);
            s0 = stbte__extract_short(tm->props[y][x][n-256],0);
            s1 = stbte__extract_short(tm->props[y][x][n-256],1);
            tm->props[y][x][n-256] = vf;
         } else {
#ifdef STBTE_ALLOW_LINK
            s0 = tm->link[y][x].x;
            s1 = tm->link[y][x].y;
            stbte__set_link(tm, x,y,v,v2, STBTE__undo_none);
#endif
         }
         // don't use stbte__undo_record_prop because it's guarded
         stbte__write_undo(tm, s1);
         stbte__write_undo(tm, s0);
         stbte__write_undo(tm, x);
         stbte__write_undo(tm, y);
         stbte__write_undo(tm, n);
      } else {
         pos = stbte__wrap(pos+4);
         // don't use stbte__undo_record because it's guarded
         stbte__write_undo(tm, tm->data[y][x][n]);
         stbte__write_undo(tm, x);
         stbte__write_undo(tm, y);
         stbte__write_undo(tm, n);
         tm->data[y][x][n] = (short) v;
      }
   }
   tm->undo_buffer[tm->undo_pos] = STBTE__undo_junk;
}

// because detecting that undo is available
static void stbte__recompute_undo_available(stbte_tilemap *tm)
{
   tm->undo_available = (stbte__undo_find_end(tm) >= 0);
   tm->redo_available = (stbte__redo_find_end(tm) >= 0);
}

static int stbte__undo_available(stbte_tilemap *tm)
{
   if (!tm->undo_available_valid)
      stbte__recompute_undo_available(tm);
   return tm->undo_available;
}

static int stbte__redo_available(stbte_tilemap *tm)
{
   if (!tm->undo_available_valid)
      stbte__recompute_undo_available(tm);
   return tm->redo_available;
}

///////////////////////////////////////////////////////////////////////////////////////////////////

#ifdef STBTE_ALLOW_LINK
static void stbte__set_link(stbte_tilemap *tm, int src_x, int src_y, int dest_x, int dest_y, int undo_mode)
{
   stbte__link *a;
   STBTE_ASSERT(src_x >= 0 && src_x < STBTE_MAX_TILEMAP_X && src_y >= 0 && src_y < STBTE_MAX_TILEMAP_Y);
   a = &tm->link[src_y][src_x];
   // check if it's a do nothing
   if (a->x == dest_x && a->y == dest_y)
      return;
   if (undo_mode != STBTE__undo_none ) {
      if (undo_mode == STBTE__undo_block) stbte__begin_undo(tm);
      stbte__undo_record_prop(tm, src_x, src_y, -1, a->x, a->y);
      if (undo_mode == STBTE__undo_block) stbte__end_undo(tm);
   }
   // check if there's an existing link
   if (a->x >= 0) {
      // decrement existing link refcount
      STBTE_ASSERT(tm->linkcount[a->y][a->x] > 0);
      --tm->linkcount[a->y][a->x];
   }
   // increment new dest
   if (dest_x >= 0) {
      ++tm->linkcount[dest_y][dest_x];
   }
   a->x = dest_x;
   a->y = dest_y;
}
#endif


static void stbte__draw_rect(int x0, int y0, int x1, int y1, unsigned int color)
{
   STBTE_DRAW_RECT(x0,y0,x1,y1, color);
}

#ifdef STBTE_ALLOW_LINK
static void stbte__draw_line(int x0, int y0, int x1, int y1, unsigned int color)
{
   int temp;
   if (x1 < x0) temp=x0,x0=x1,x1=temp;
   if (y1 < y0) temp=y0,y0=y1,y1=temp;
   stbte__draw_rect(x0,y0,x1+1,y1+1,color);
}

static void stbte__draw_link(int x0, int y0, int x1, int y1, unsigned int color)
{
   stbte__draw_line(x0,y0,x0,y1, color);
   stbte__draw_line(x0,y1,x1,y1, color);
}
#endif

static void stbte__draw_frame(int x0, int y0, int x1, int y1, unsigned int color)
{
   stbte__draw_rect(x0,y0,x1-1,y0+1,color);
   stbte__draw_rect(x1-1,y0,x1,y1-1,color);
   stbte__draw_rect(x0+1,y1-1,x1,y1,color);
   stbte__draw_rect(x0,y0+1,x0+1,y1,color);
}

static int stbte__get_char_width(int ch)
{
   return stbte__fontdata[ch-16];
}

static short *stbte__get_char_bitmap(int ch)
{
   return stbte__fontdata + stbte__font_offset[ch-16];
}

static void stbte__draw_bitmask_as_columns(int x, int y, short bitmask, int color)
{
   int start_i = -1, i=0;
   while (bitmask) {
      if (bitmask & (1<<i)) {
         if (start_i < 0)
            start_i = i;
      } else if (start_i >= 0) {
         stbte__draw_rect(x, y+start_i, x+1, y+i, color);
         start_i = -1;
         bitmask &= ~((1<<i)-1); // clear all the old bits; we don't clear them as we go to save code
      }
      ++i;
   }
}

static void stbte__draw_bitmap(int x, int y, int w, short *bitmap, int color)
{
   int i;
   for (i=0; i < w; ++i)
      stbte__draw_bitmask_as_columns(x+i, y, *bitmap++, color);
}

static void stbte__draw_text_core(int x, int y, const char *str, int w, int color, int digitspace)
{
   int x_end = x+w;
   while (*str) {
      int c = *str++;
      int cw = stbte__get_char_width(c);
      if (x + cw > x_end)
         break;
      stbte__draw_bitmap(x, y, cw, stbte__get_char_bitmap(c), color);
      if (digitspace && c == ' ')
         cw = stbte__get_char_width('0');
      x += cw+1;
   }
}

static void stbte__draw_text(int x, int y, const char *str, int w, int color)
{
   stbte__draw_text_core(x,y,str,w,color,0);
}

static int stbte__text_width(const char *str)
{
   int x = 0;
   while (*str) {
      int c = *str++;
      int cw = stbte__get_char_width(c);
      x += cw+1;
   }
   return x;
}

static void stbte__draw_frame_delayed(int x0, int y0, int x1, int y1, int color)
{
   if (stbte__ui.delaycount < STBTE__MAX_DELAYRECT) {
      stbte__colorrect r = { x0,y0,x1,y1,color };
      stbte__ui.delayrect[stbte__ui.delaycount++] = r;
   }
}

static void stbte__flush_delay(void)
{
   stbte__colorrect *r;
   int i;
   r = stbte__ui.delayrect;
   for (i=0; i < stbte__ui.delaycount; ++i,++r)
      stbte__draw_frame(r->x0,r->y0,r->x1,r->y1,r->color);
   stbte__ui.delaycount = 0;
}

static void stbte__activate(int id)
{
   stbte__ui.active_id = id;
   stbte__ui.active_event = stbte__ui.event;
   stbte__ui.accum_x = 0;
   stbte__ui.accum_y = 0;
}

static int stbte__hittest(int x0, int y0, int x1, int y1, int id)
{
   int over =    stbte__ui.mx >= x0 && stbte__ui.my >= y0
              && stbte__ui.mx <  x1 && stbte__ui.my <  y1;

   if (over && stbte__ui.event >= STBTE__tick)
      stbte__ui.next_hot_id = id;

   return over;
}

static int stbte__button_core(int id)
{
   switch (stbte__ui.event) {
      case STBTE__leftdown:
         if (stbte__ui.hot_id == id && STBTE__INACTIVE())
            stbte__activate(id);
         break;
      case STBTE__leftup:
         if (stbte__ui.active_id == id && STBTE__IS_HOT(id)) {
            stbte__activate(0);
            return 1;
         }
         break;
      case STBTE__rightdown:
         if (stbte__ui.hot_id == id && STBTE__INACTIVE())
            stbte__activate(id);
         break;
      case STBTE__rightup:
         if (stbte__ui.active_id == id && STBTE__IS_HOT(id)) {
            stbte__activate(0);
            return -1;
         }
         break;
   }
   return 0;
}

static void stbte__draw_box(int x0, int y0, int x1, int y1, int colormode, int colorindex)
{
   stbte__draw_rect (x0,y0,x1,y1, stbte__color_table[colormode][STBTE__base   ][colorindex]);
   stbte__draw_frame(x0,y0,x1,y1, stbte__color_table[colormode][STBTE__outline][colorindex]);
}

static void stbte__draw_textbox(int x0, int y0, int x1, int y1, char *text, int xoff, int yoff, int colormode, int colorindex)
{
   stbte__draw_box(x0,y0,x1,y1,colormode,colorindex);
   stbte__draw_text(x0+xoff,y0+yoff, text, x1-x0-xoff-1, stbte__color_table[colormode][STBTE__text][colorindex]);
}

static int stbte__button(int colormode, const char *label, int x, int y, int textoff, int width, int id, int toggled, int disabled)
{
   int x0=x,y0=y, x1=x+width,y1=y+STBTE__BUTTON_HEIGHT;
   int s = STBTE__BUTTON_INTERNAL_SPACING;

   if(!disabled) stbte__hittest(x0,y0,x1,y1,id);

   if (stbte__ui.event == STBTE__paint)
      stbte__draw_textbox(x0,y0,x1,y1, (char*) label,s+textoff,s, colormode, STBTE__INDEX_FOR_ID(id,disabled,toggled));
   if (disabled)
      return 0;
   return (stbte__button_core(id) == 1);
}

static int stbte__button_icon(int colormode, char ch, int x, int y, int width, int id, int toggled, int disabled)
{
   int x0=x,y0=y, x1=x+width,y1=y+STBTE__BUTTON_HEIGHT;
   int s = STBTE__BUTTON_INTERNAL_SPACING;

   stbte__hittest(x0,y0,x1,y1,id);

   if (stbte__ui.event == STBTE__paint) {
      char label[2] = { ch, 0 };
      int pad = (9 - stbte__get_char_width(ch))/2;
      stbte__draw_textbox(x0,y0,x1,y1, label,s+pad,s, colormode, STBTE__INDEX_FOR_ID(id,disabled,toggled));
   }
   if (disabled)
      return 0;
   return (stbte__button_core(id) == 1);
}

static int stbte__minibutton(int colormode, int x, int y, int ch, int id)
{
   int x0 = x, y0 = y, x1 = x+8, y1 = y+7;
   stbte__hittest(x0,y0,x1,y1,id);
   if (stbte__ui.event == STBTE__paint) {
      char str[2] = { (char)ch, 0 };
      stbte__draw_textbox(x0,y0,x1,y1, str,1,0,colormode, STBTE__INDEX_FOR_ID(id,0,0));
   }
   return stbte__button_core(id);
}

static int stbte__layerbutton(int x, int y, int ch, int id, int toggled, int disabled, int colormode)
{
   int x0 = x, y0 = y, x1 = x+10, y1 = y+11;
   if(!disabled) stbte__hittest(x0,y0,x1,y1,id);
   if (stbte__ui.event == STBTE__paint) {
      char str[2] = { (char)ch, 0 };
      int off = (9-stbte__get_char_width(ch))/2;
      stbte__draw_textbox(x0,y0,x1,y1, str, off+1,2, colormode, STBTE__INDEX_FOR_ID(id,disabled,toggled));
   }
   if (disabled)
      return 0;
   return stbte__button_core(id);
}

static int stbte__microbutton(int x, int y, int size, int id, int colormode)
{
   int x0 = x, y0 = y, x1 = x+size, y1 = y+size;
   stbte__hittest(x0,y0,x1,y1,id);
   if (stbte__ui.event == STBTE__paint) {
      stbte__draw_box(x0,y0,x1,y1, colormode, STBTE__INDEX_FOR_ID(id,0,0));
   }
   return stbte__button_core(id);
}

static int stbte__microbutton_dragger(int x, int y, int size, int id, int *pos)
{
   int x0 = x, y0 = y, x1 = x+size, y1 = y+size;
   stbte__hittest(x0,y0,x1,y1,id);
   switch (stbte__ui.event) {
      case STBTE__paint:
         stbte__draw_box(x0,y0,x1,y1, STBTE__cexpander, STBTE__INDEX_FOR_ID(id,0,0));
         break;
      case STBTE__leftdown:
         if (STBTE__IS_HOT(id) && STBTE__INACTIVE()) {
            stbte__activate(id);
            stbte__ui.sx = stbte__ui.mx - *pos;
         }
         break;
      case STBTE__mousemove:
         if (STBTE__IS_ACTIVE(id) && stbte__ui.active_event == STBTE__leftdown) {
            *pos = stbte__ui.mx - stbte__ui.sx;
         }
         break;
      case STBTE__leftup:
         if (STBTE__IS_ACTIVE(id))
            stbte__activate(0);
         break;
      default:
         return stbte__button_core(id);
   }
   return 0;
}

static int stbte__category_button(const char *label, int x, int y, int width, int id, int toggled)
{
   int x0=x,y0=y, x1=x+width,y1=y+STBTE__BUTTON_HEIGHT;
   int s = STBTE__BUTTON_INTERNAL_SPACING;

   stbte__hittest(x0,y0,x1,y1,id);

   if (stbte__ui.event == STBTE__paint)
      stbte__draw_textbox(x0,y0,x1,y1, (char*) label, s,s, STBTE__ccategory_button, STBTE__INDEX_FOR_ID(id,0,toggled));

   return (stbte__button_core(id) == 1);
}

enum
{
   STBTE__none,
   STBTE__begin,
   STBTE__end,
   STBTE__change,
};

// returns -1 if value changes, 1 at end of drag
static int stbte__slider(int x0, int w, int y, int range, int *value, int id)
{
   int x1 = x0+w;
   int pos = *value * w / (range+1);
   stbte__hittest(x0,y-2,x1,y+3,id);
   int event_mouse_move = STBTE__change;
   switch (stbte__ui.event) {
      case STBTE__paint:
         stbte__draw_rect(x0,y,x1,y+1, 0x808080);
         stbte__draw_rect(x0+pos-1,y-1,x0+pos+2,y+2, 0xffffff);
         break;
      case STBTE__leftdown:
         if (STBTE__IS_HOT(id) && STBTE__INACTIVE()) {
            stbte__activate(id);
            event_mouse_move = STBTE__begin;
         }
         // fall through
      case STBTE__mousemove:
         if (STBTE__IS_ACTIVE(id)) {
            int v = (stbte__ui.mx-x0)*(range+1)/w;
            if (v < 0) v = 0; else if (v > range) v = range;
            *value = v;
            return event_mouse_move;
         }
         break;
      case STBTE__leftup:
         if (STBTE__IS_ACTIVE(id)) {
            stbte__activate(0);
            return STBTE__end;
         }
         break;
   }
   return STBTE__none;
}

#if defined(_WIN32) && defined(__STDC_WANT_SECURE_LIB__)
   #define stbte__sprintf      sprintf_s
   #define stbte__sizeof(s)    , sizeof(s)
#else
   #define stbte__sprintf      sprintf
   #define stbte__sizeof(s)
#endif

static int stbte__float_control(int x0, int y0, int w, float minv, float maxv, float scale, const char *fmt, float *value, int colormode, int id)
{
   int x1 = x0+w;
   int y1 = y0+11;
   stbte__hittest(x0,y0,x1,y1,id);
   switch (stbte__ui.event) {
      case STBTE__paint: {
         char text[32];
         stbte__sprintf(text stbte__sizeof(text), fmt ? fmt : "%6.2f", *value);
         stbte__draw_textbox(x0,y0,x1,y1, text, 1,2, colormode, STBTE__INDEX_FOR_ID(id,0,0));
         break;
      }
      case STBTE__leftdown:
      case STBTE__rightdown:
         if (STBTE__IS_HOT(id) && STBTE__INACTIVE())
            stbte__activate(id);
         return STBTE__begin;
         break;
      case STBTE__leftup:
      case STBTE__rightup:
         if (STBTE__IS_ACTIVE(id)) {
            stbte__activate(0);
            return STBTE__end;
         }
         break;
      case STBTE__mousemove:
         if (STBTE__IS_ACTIVE(id)) {
            float v = *value, delta;
            int ax = stbte__ui.accum_x/STBTE_FLOAT_CONTROL_GRANULARITY;
            int ay = stbte__ui.accum_y/STBTE_FLOAT_CONTROL_GRANULARITY;
            stbte__ui.accum_x -= ax*STBTE_FLOAT_CONTROL_GRANULARITY;
            stbte__ui.accum_y -= ay*STBTE_FLOAT_CONTROL_GRANULARITY;
            if (stbte__ui.shift) {
               if (stbte__ui.active_event == STBTE__leftdown)
                  delta = ax * 16.0f + ay;
               else
                  delta = ax / 16.0f + ay / 256.0f;
            } else {
               if (stbte__ui.active_event == STBTE__leftdown)
                  delta = ax*10.0f + ay;
               else
                  delta = ax * 0.1f + ay * 0.01f;
            }
            v += delta * scale;
            if (v < minv) v = minv;
            if (v > maxv) v = maxv;
            *value = v;
            return STBTE__change;
         }
         break;
   }
   return STBTE__none;
}

static void stbte__scrollbar(int x, int y0, int y1, int *val, int v0, int v1, int num_vis, int id)
{
   int thumbpos;
   if (v1 - v0 <= num_vis)
      return;

   // generate thumbpos from numvis
   thumbpos = y0+2 + (y1-y0-4) * *val / (v1 - v0 - num_vis);
   if (thumbpos < y0) thumbpos = y0;
   if (thumbpos >= y1) thumbpos = y1;
   stbte__hittest(x-1,y0,x+2,y1,id);
   switch (stbte__ui.event) {
      case STBTE__paint:
         stbte__draw_rect(x,y0,x+1,y1, stbte__color_table[STBTE__cscrollbar][STBTE__text][STBTE__idle]);
         stbte__draw_box(x-1,thumbpos-3,x+2,thumbpos+4, STBTE__cscrollbar, STBTE__INDEX_FOR_ID(id,0,0));
         break;
      case STBTE__leftdown:
         if (STBTE__IS_HOT(id) && STBTE__INACTIVE()) {
            // check if it's over the thumb
            stbte__activate(id);
            *val = ((stbte__ui.my-y0) * (v1 - v0 - num_vis) + (y1-y0)/2)/ (y1-y0);
         }
         break;
      case STBTE__mousemove:
         if (STBTE__IS_ACTIVE(id) && stbte__ui.mx >= x-15 && stbte__ui.mx <= x+15)
            *val = ((stbte__ui.my-y0) * (v1 - v0 - num_vis) + (y1-y0)/2)/ (y1-y0);
         break;
      case STBTE__leftup:
         if (STBTE__IS_ACTIVE(id))
            stbte__activate(0);
         break;

   }

   if (*val >= v1-num_vis)
      *val = v1-num_vis;
   if (*val <= v0)
      *val = v0;
}


static void stbte__compute_digits(stbte_tilemap *tm)
{
   if (tm->max_x >= 1000 || tm->max_y >= 1000)
      tm->digits = 4;
   else if (tm->max_x >= 100 || tm->max_y >= 100)
      tm->digits = 3;
   else
      tm->digits = 2;
}

static int stbte__is_single_selection(void)
{
   return stbte__ui.has_selection
       && stbte__ui.select_x0 == stbte__ui.select_x1
       && stbte__ui.select_y0 == stbte__ui.select_y1;
}

typedef struct
{
   int width, height;
   int x,y;
   int active;
   float retracted;
} stbte__region_t;

static stbte__region_t stbte__region[4];

#define STBTE__TOOLBAR_ICON_SIZE   (9+2*2)
#define STBTE__TOOLBAR_PASTE_SIZE  (34+2*2)

// This routine computes where every panel goes onscreen: computes
// a minimum width for each side based on which panels are on that
// side, and accounts for width-dependent layout of certain panels.
static void stbte__compute_panel_locations(stbte_tilemap *tm)
{
   int i, limit, w, k;
   int window_width  = stbte__ui.x1 - stbte__ui.x0;
   int window_height = stbte__ui.y1 - stbte__ui.y0;
   int min_width[STBTE__num_panel]={0,0,0,0,0,0,0};
   int height[STBTE__num_panel]={0,0,0,0,0,0,0};
   int panel_active[STBTE__num_panel]={1,0,1,1,1,1,1};
   int vpos[4] = { 0,0,0,0 };
   stbte__panel *p = stbte__ui.panel;
   stbte__panel *pt = &p[STBTE__panel_toolbar];
#ifdef STBTE__NO_PROPS
   int props = 0;
#else
   int props = 1;
#endif

   for (i=0; i < 4; ++i) {
      stbte__region[i].active = 0;
      stbte__region[i].width = 0;
      stbte__region[i].height = 0;
   }

   // compute number of digits needs for info panel
   stbte__compute_digits(tm);

   // determine which panels are active
   panel_active[STBTE__panel_categories] = tm->num_categories != 0;
   panel_active[STBTE__panel_layers    ] = tm->num_layers     >  1;
#ifdef STBTE__COLORPICKER
   panel_active[STBTE__panel_colorpick ] = 1;
#endif

   panel_active[STBTE__panel_props     ] = props && stbte__is_single_selection();

   // compute minimum widths for each panel (assuming they're on sides not top)
   min_width[STBTE__panel_info      ] = 8 + 11 + 7*tm->digits+17+7;               // estimate min width of "w:0000"
   min_width[STBTE__panel_colorpick ] = 120;
   min_width[STBTE__panel_tiles     ] = 4 + tm->palette_spacing_x + 5;            // 5 for scrollbar
   min_width[STBTE__panel_categories] = 4 + 42 + 5;                               // 42 is enough to show ~7 chars; 5 for scrollbar
   min_width[STBTE__panel_layers    ] = 4 + 54 + 30*tm->has_layer_names;          // 2 digits plus 3 buttons plus scrollbar
   min_width[STBTE__panel_toolbar   ] = 4 + STBTE__TOOLBAR_PASTE_SIZE;            // wide enough for 'Paste' button
   min_width[STBTE__panel_props     ] = 80;                    // narrowest info panel

   // compute minimum widths for left & right panels based on the above
   stbte__region[0].width = stbte__ui.left_width;
   stbte__region[1].width = stbte__ui.right_width;

   for (i=0; i < STBTE__num_panel; ++i) {
      if (panel_active[i]) {
         int side = stbte__ui.panel[i].side;
         if (min_width[i] > stbte__region[side].width)
            stbte__region[side].width = min_width[i];
         stbte__region[side].active = 1;
      }
   }

   // now compute the heights of each panel

   // if toolbar at top, compute its size & push the left and right start points down
   if (stbte__region[STBTE__side_top].active) {
      int height = STBTE__TOOLBAR_ICON_SIZE+2;
      pt->x0     = stbte__ui.x0;
      pt->y0     = stbte__ui.y0;
      pt->width  = window_width;
      pt->height = height;
      vpos[STBTE__side_left] = vpos[STBTE__side_right] = height;
   } else {
      int num_rows = STBTE__num_tool * ((stbte__region[pt->side].width-4)/STBTE__TOOLBAR_ICON_SIZE);
      height[STBTE__panel_toolbar] = num_rows*13 + 3*15 + 4; // 3*15 for cut/copy/paste, which are stacked vertically
   }

   for (i=0; i < 4; ++i)
      stbte__region[i].y = stbte__ui.y0 + vpos[i];

   for (i=0; i < 2; ++i) {
      int anim = (int) (stbte__region[i].width * stbte__region[i].retracted);
      stbte__region[i].x = (i == STBTE__side_left) ? stbte__ui.x0 - anim : stbte__ui.x1 - stbte__region[i].width + anim;
   }

   // color picker
   height[STBTE__panel_colorpick] = 300;

   // info panel
   w = stbte__region[p[STBTE__panel_info].side].width;
   p[STBTE__panel_info].mode = (w >= 8 + (11+7*tm->digits+17)*2 + 4);
   if (p[STBTE__panel_info].mode)
      height[STBTE__panel_info] = 5 + 11*2 + 2 + tm->palette_spacing_y;
   else
      height[STBTE__panel_info] = 5 + 11*4 + 2 + tm->palette_spacing_y;

   // layers
   limit = 6 + stbte__ui.panel[STBTE__panel_layers].delta_height;
   height[STBTE__panel_layers] = (tm->num_layers > limit ? limit : tm->num_layers)*15 + 7 + (tm->has_layer_names ? 0 : 11) + props*13;

   // categories
   limit = 6 + stbte__ui.panel[STBTE__panel_categories].delta_height;
   height[STBTE__panel_categories] = (tm->num_categories+1 > limit ? limit : tm->num_categories+1)*11 + 14;
   if (stbte__ui.panel[STBTE__panel_categories].side == stbte__ui.panel[STBTE__panel_categories].side)
      height[STBTE__panel_categories] -= 4;

   // palette
   k =  (stbte__region[p[STBTE__panel_tiles].side].width - 8) / tm->palette_spacing_x;
   if (k == 0) k = 1;
   height[STBTE__panel_tiles] = ((tm->num_tiles+k-1)/k) * tm->palette_spacing_y + 8;

   // properties panel
   height[STBTE__panel_props] = 9 + STBTE_MAX_PROPERTIES*14;

   // now compute the locations of all the panels
   for (i=0; i < STBTE__num_panel; ++i) {
      if (panel_active[i]) {
         int side = p[i].side;
         if (side == STBTE__side_left || side == STBTE__side_right) {
            p[i].width  = stbte__region[side].width;
            p[i].x0     = stbte__region[side].x;
            p[i].y0     = stbte__ui.y0 + vpos[side];
            p[i].height = height[i];
            vpos[side] += height[i];
            if (vpos[side] > window_height) {
               vpos[side] = window_height;
               p[i].height = stbte__ui.y1 - p[i].y0;
            }
         } else {
            ; // it's at top, it's already been explicitly set up earlier
         }
      } else {
         // inactive panel
         p[i].height = 0;
         p[i].width  = 0;
         p[i].x0     = stbte__ui.x1;
         p[i].y0     = stbte__ui.y1;
      }
   }
}

// unique identifiers for imgui
enum
{
   STBTE__map=1,
   STBTE__region,
   STBTE__panel,                          // panel background to hide map, and misc controls
   STBTE__info,                           // info data
   STBTE__toolbarA, STBTE__toolbarB,      // toolbar buttons: param is tool number
   STBTE__palette,                        // palette selectors: param is tile index
   STBTE__categories,                     // category selectors: param is category index
   STBTE__layer,                          //
   STBTE__solo, STBTE__hide, STBTE__lock, // layer controls: param is layer
   STBTE__scrollbar,                      // param is panel ID
   STBTE__panel_mover,                    // p1 is panel ID, p2 is destination side
   STBTE__panel_sizer,                    // param panel ID
   STBTE__scrollbar_id,
   STBTE__colorpick_id,
   STBTE__prop_flag,
   STBTE__prop_float,
   STBTE__prop_int,
};

// id is:      [      24-bit data     : 7-bit identifier ]
// map id is:  [  12-bit y : 12 bit x : 7-bit identifier ]

#define STBTE__ID(n,p)     ((n) + ((p)<<7))
#define STBTE__ID2(n,p,q)  STBTE__ID(n, ((p)<<12)+(q) )
#define STBTE__IDMAP(x,y)  STBTE__ID2(STBTE__map, x,y)

static void stbte__activate_map(int x, int y)
{
   stbte__ui.active_id = STBTE__IDMAP(x,y);
   stbte__ui.active_event = stbte__ui.event;
   stbte__ui.sx = x;
   stbte__ui.sy = y;
}

static void stbte__alert(const char *msg)
{
   stbte__ui.alert_msg = msg;
   stbte__ui.alert_timer = 3;
}

#define STBTE__BG(tm,layer) ((layer) == 0 ? (tm)->background_tile : STBTE__NO_TILE)



static void stbte__brush_predict(stbte_tilemap *tm, short result[])
{
   stbte__tileinfo *ti;
   int i;

   if (tm->cur_tile < 0) return;

   ti = &tm->tiles[tm->cur_tile];

   // find lowest legit layer to paint it on, and put it there
   for (i=0; i < tm->num_layers; ++i) {
      // check if object is allowed on layer
      if (!(ti->layermask & (1 << i)))
         continue;

      if (i != tm->solo_layer) {
         // if there's a selected layer, can only paint on that
         if (tm->cur_layer >= 0 && i != tm->cur_layer)
            continue;

         // if the layer is hidden, we can't see it
         if (tm->layerinfo[i].hidden)
            continue;

         // if the layer is locked, we can't write to it
         if (tm->layerinfo[i].locked == STBTE__locked)
            continue;

         // if the layer is non-empty and protected, can't write to it
         if (tm->layerinfo[i].locked == STBTE__protected && result[i] != STBTE__BG(tm,i))
            continue;
      }

      result[i] = ti->id;
      return;
   }
}

static void stbte__brush(stbte_tilemap *tm, int x, int y)
{
   stbte__tileinfo *ti;

   // find lowest legit layer to paint it on, and put it there
   int i;

   if (tm->cur_tile < 0) return;

   ti = &tm->tiles[tm->cur_tile];

   for (i=0; i < tm->num_layers; ++i) {
      // check if object is allowed on layer
      if (!(ti->layermask & (1 << i)))
         continue;

      if (i != tm->solo_layer) {
         // if there's a selected layer, can only paint on that
         if (tm->cur_layer >= 0 && i != tm->cur_layer)
            continue;

         // if the layer is hidden, we can't see it
         if (tm->layerinfo[i].hidden)
            continue;

         // if the layer is locked, we can't write to it
         if (tm->layerinfo[i].locked == STBTE__locked)
            continue;

         // if the layer is non-empty and protected, can't write to it
         if (tm->layerinfo[i].locked == STBTE__protected && tm->data[y][x][i] != STBTE__BG(tm,i))
            continue;
      }

      stbte__undo_record(tm,x,y,i,tm->data[y][x][i]);
      tm->data[y][x][i] = ti->id;
      return;
   }

   //stbte__alert("Selected tile not valid on active layer(s)");
}

enum
{
   STBTE__erase_none = -1,
   STBTE__erase_brushonly = 0,
   STBTE__erase_any = 1,
   STBTE__erase_all = 2,
};

static int stbte__erase_predict(stbte_tilemap *tm, short result[], int allow_any)
{
   stbte__tileinfo *ti = tm->cur_tile >= 0 ? &tm->tiles[tm->cur_tile] : NULL;
   int i;

   if (allow_any == STBTE__erase_none)
      return allow_any;

   // first check if only one layer is legit
   i = tm->cur_layer;
   if (tm->solo_layer >= 0)
      i = tm->solo_layer;

   // if only one layer is legit, directly process that one for clarity
   if (i >= 0) {
      short bg = (i == 0 ? tm->background_tile : -1);
      if (tm->solo_layer < 0) {
         // check that we're allowed to write to it
         if (tm->layerinfo[i].hidden) return STBTE__erase_none;
         if (tm->layerinfo[i].locked) return STBTE__erase_none;
      }
      if (result[i] == bg)
         return STBTE__erase_none; // didn't erase anything
      if (ti && result[i] == ti->id && (i != 0 || ti->id != tm->background_tile)) {
         result[i] = bg;
         return STBTE__erase_brushonly;
      }
      if (allow_any == STBTE__erase_any) {
         result[i] = bg;
         return STBTE__erase_any;
      }
      return STBTE__erase_none;
   }

   // if multiple layers are legit, first scan all for brush data

   if (ti && allow_any != STBTE__erase_all) {
      for (i=tm->num_layers-1; i >= 0; --i) {
         if (result[i] != ti->id)
            continue;
         if (tm->layerinfo[i].locked || tm->layerinfo[i].hidden)
            continue;
         if (i == 0 && result[i] == tm->background_tile)
            return STBTE__erase_none;
         result[i] = STBTE__BG(tm,i);
         return STBTE__erase_brushonly;
      }
   }

   if (allow_any != STBTE__erase_any && allow_any != STBTE__erase_all)
      return STBTE__erase_none;

   // apply layer filters, erase from top
   for (i=tm->num_layers-1; i >= 0; --i) {
      if (result[i] < 0)
         continue;
      if (tm->layerinfo[i].locked || tm->layerinfo[i].hidden)
         continue;
      if (i == 0 && result[i] == tm->background_tile)
         return STBTE__erase_none;
      result[i] = STBTE__BG(tm,i);
      if (allow_any != STBTE__erase_all)
         return STBTE__erase_any;
   }

   if (allow_any == STBTE__erase_all)
      return allow_any;
   return STBTE__erase_none;
}

static int stbte__erase(stbte_tilemap *tm, int x, int y, int allow_any)
{
   stbte__tileinfo *ti = tm->cur_tile >= 0 ? &tm->tiles[tm->cur_tile] : NULL;
   int i;

   if (allow_any == STBTE__erase_none)
      return allow_any;

   // first check if only one layer is legit
   i = tm->cur_layer;
   if (tm->solo_layer >= 0)
      i = tm->solo_layer;

   // if only one layer is legit, directly process that one for clarity
   if (i >= 0) {
      short bg = (i == 0 ? tm->background_tile : -1);
      if (tm->solo_layer < 0) {
         // check that we're allowed to write to it
         if (tm->layerinfo[i].hidden) return STBTE__erase_none;
         if (tm->layerinfo[i].locked) return STBTE__erase_none;
      }
      if (tm->data[y][x][i] == bg)
         return -1; // didn't erase anything
      if (ti && tm->data[y][x][i] == ti->id && (i != 0 || ti->id != tm->background_tile)) {
         stbte__undo_record(tm,x,y,i,tm->data[y][x][i]);
         tm->data[y][x][i] = bg;
         return STBTE__erase_brushonly;
      }
      if (allow_any == STBTE__erase_any) {
         stbte__undo_record(tm,x,y,i,tm->data[y][x][i]);
         tm->data[y][x][i] = bg;
         return STBTE__erase_any;
      }
      return STBTE__erase_none;
   }

   // if multiple layers are legit, first scan all for brush data

   if (ti && allow_any != STBTE__erase_all) {
      for (i=tm->num_layers-1; i >= 0; --i) {
         if (tm->data[y][x][i] != ti->id)
            continue;
         if (tm->layerinfo[i].locked || tm->layerinfo[i].hidden)
            continue;
         if (i == 0 && tm->data[y][x][i] == tm->background_tile)
            return STBTE__erase_none;
         stbte__undo_record(tm,x,y,i,tm->data[y][x][i]);
         tm->data[y][x][i] = STBTE__BG(tm,i);
         return STBTE__erase_brushonly;
      }
   }

   if (allow_any != STBTE__erase_any && allow_any != STBTE__erase_all)
      return STBTE__erase_none;

   // apply layer filters, erase from top
   for (i=tm->num_layers-1; i >= 0; --i) {
      if (tm->data[y][x][i] < 0)
         continue;
      if (tm->layerinfo[i].locked || tm->layerinfo[i].hidden)
         continue;
      if (i == 0 && tm->data[y][x][i] == tm->background_tile)
         return STBTE__erase_none;
      stbte__undo_record(tm,x,y,i,tm->data[y][x][i]);
      tm->data[y][x][i] = STBTE__BG(tm,i);
      if (allow_any != STBTE__erase_all)
         return STBTE__erase_any;
   }
   if (allow_any == STBTE__erase_all)
      return allow_any;
   return STBTE__erase_none;
}

static int stbte__find_tile(stbte_tilemap *tm, int tile_id)
{
   int i;
   for (i=0; i < tm->num_tiles; ++i)
      if (tm->tiles[i].id == tile_id)
         return i;
   stbte__alert("Eyedropped tile that isn't in tileset");
   return -1;
}

static void stbte__eyedrop(stbte_tilemap *tm, int x, int y)
{
   int i,j;

   // flush eyedropper state
   if (stbte__ui.eyedrop_x != x || stbte__ui.eyedrop_y != y) {
      stbte__ui.eyedrop_x = x;
      stbte__ui.eyedrop_y = y;
      stbte__ui.eyedrop_last_layer = tm->num_layers;
   }

   // if only one layer is active, query that
   i = tm->cur_layer;
   if (tm->solo_layer >= 0)
      i = tm->solo_layer;
   if (i >= 0) {
      if (tm->data[y][x][i] == STBTE__NO_TILE)
         return;
      tm->cur_tile = stbte__find_tile(tm, tm->data[y][x][i]);
      return;
   }

   // if multiple layers, continue from previous
   i = stbte__ui.eyedrop_last_layer;
   for (j=0; j < tm->num_layers; ++j) {
      if (--i < 0)
         i = tm->num_layers-1;
      if (tm->layerinfo[i].hidden)
         continue;
      if (tm->data[y][x][i] == STBTE__NO_TILE)
         continue;
      stbte__ui.eyedrop_last_layer = i;
      tm->cur_tile = stbte__find_tile(tm, tm->data[y][x][i]);
      return;
   }
}

static int stbte__should_copy_properties(stbte_tilemap *tm)
{
   int i;
   if (tm->propmode == STBTE__propmode_always)
      return 1;
   if (tm->propmode == STBTE__propmode_never)
      return 0;
   if (tm->solo_layer >= 0 || tm->cur_layer >= 0)
      return 0;
   for (i=0; i < tm->num_layers; ++i)
      if (tm->layerinfo[i].hidden || tm->layerinfo[i].locked)
         return 0;
   return 1;
}

// compute the result of pasting into a tile non-destructively so we can preview it
static void stbte__paste_stack(stbte_tilemap *tm, short result[], short dest[], short src[], int dragging)
{
   int i;

   // special case single-layer
   i = tm->cur_layer;
   if (tm->solo_layer >= 0)
      i = tm->solo_layer;
   if (i >= 0) {
      if (tm->solo_layer < 0) {
         // check that we're allowed to write to it
         if (tm->layerinfo[i].hidden) return;
         if (tm->layerinfo[i].locked == STBTE__locked) return;
         // if protected, dest has to be empty
         if (tm->layerinfo[i].locked == STBTE__protected && dest[i] != STBTE__BG(tm,i)) return;
         // if dragging w/o copy, we will try to erase stuff, which protection disallows
         if (dragging && tm->layerinfo[i].locked == STBTE__protected)
             return;
      }
      result[i] = dest[i];
      if (src[i] != STBTE__BG(tm,i))
         result[i] = src[i];
      return;
   }

   for (i=0; i < tm->num_layers; ++i) {
      result[i] = dest[i];
      if (src[i] != STBTE__NO_TILE)
         if (!tm->layerinfo[i].hidden && tm->layerinfo[i].locked != STBTE__locked)
            if (tm->layerinfo[i].locked == STBTE__unlocked || (!dragging && dest[i] == STBTE__BG(tm,i)))
               result[i] = src[i];
   }
}

// compute the result of dragging away from a tile
static void stbte__clear_stack(stbte_tilemap *tm, short result[])
{
   int i;
   // special case single-layer
   i = tm->cur_layer;
   if (tm->solo_layer >= 0)
      i = tm->solo_layer;
   if (i >= 0)
      result[i] = STBTE__BG(tm,i);
   else
      for (i=0; i < tm->num_layers; ++i)
         if (!tm->layerinfo[i].hidden && tm->layerinfo[i].locked == STBTE__unlocked)
            result[i] = STBTE__BG(tm,i);
}

// check if some map square is active
#define STBTE__IS_MAP_ACTIVE()  ((stbte__ui.active_id & 127) == STBTE__map)
#define STBTE__IS_MAP_HOT()     ((stbte__ui.hot_id & 127) == STBTE__map)

static void stbte__fillrect(stbte_tilemap *tm, int x0, int y0, int x1, int y1, int fill)
{
   int i,j;

   stbte__begin_undo(tm);
   if (x0 > x1) i=x0,x0=x1,x1=i;
   if (y0 > y1) j=y0,y0=y1,y1=j;
   for (j=y0; j <= y1; ++j)
      for (i=x0; i <= x1; ++i)
         if (fill)
            stbte__brush(tm, i,j);
         else
            stbte__erase(tm, i,j,STBTE__erase_any);
   stbte__end_undo(tm);
   // suppress warning from brush
   stbte__ui.alert_msg = 0;
}

static void stbte__select_rect(stbte_tilemap *tm, int x0, int y0, int x1, int y1)
{
   stbte__ui.has_selection = 1;
   stbte__ui.select_x0 = (x0 < x1 ? x0 : x1);
   stbte__ui.select_x1 = (x0 < x1 ? x1 : x0);
   stbte__ui.select_y0 = (y0 < y1 ? y0 : y1);
   stbte__ui.select_y1 = (y0 < y1 ? y1 : y0);
}

static void stbte__copy_properties(float *dest, float *src)
{
   int i;
   for (i=0; i < STBTE_MAX_PROPERTIES; ++i)
      dest[i] = src[i];
}

static void stbte__copy_cut(stbte_tilemap *tm, int cut)
{
   int i,j,n,w,h,p=0;
   int copy_props = stbte__should_copy_properties(tm);
   if (!stbte__ui.has_selection)
      return;
   w = stbte__ui.select_x1 - stbte__ui.select_x0 + 1;
   h = stbte__ui.select_y1 - stbte__ui.select_y0 + 1;
   if (STBTE_MAX_COPY / w < h) {
      stbte__alert("Selection too large for copy buffer, increase STBTE_MAX_COPY");
      return;
   }

   for (i=0; i < w*h; ++i)
      for (n=0; n < tm->num_layers; ++n)
         stbte__ui.copybuffer[i][n] = STBTE__NO_TILE;

   if (cut)
      stbte__begin_undo(tm);
   for (j=stbte__ui.select_y0; j <= stbte__ui.select_y1; ++j) {
      for (i=stbte__ui.select_x0; i <= stbte__ui.select_x1; ++i) {
         for (n=0; n < tm->num_layers; ++n) {
            if (tm->solo_layer >= 0) {
               if (tm->solo_layer != n)
                  continue;
            } else {
               if (tm->cur_layer >= 0)
                  if (tm->cur_layer != n)
                     continue;
               if (tm->layerinfo[n].hidden)
                  continue;
               if (cut && tm->layerinfo[n].locked)
                  continue;
            }
            stbte__ui.copybuffer[p][n] = tm->data[j][i][n];
            if (cut) {
               stbte__undo_record(tm,i,j,n, tm->data[j][i][n]);
               tm->data[j][i][n] = (n==0 ? tm->background_tile : -1);
            }
         }
         if (copy_props) {
            stbte__copy_properties(stbte__ui.copyprops[p], tm->props[j][i]);
#ifdef STBTE_ALLOW_LINK
            stbte__ui.copylinks[p] = tm->link[j][i];
            if (cut)
               stbte__set_link(tm, i,j,-1,-1, STBTE__undo_record);
#endif
         }
         ++p;
      }
   }
   if (cut)
      stbte__end_undo(tm);
   stbte__ui.copy_width = w;
   stbte__ui.copy_height = h;
   stbte__ui.has_copy = 1;
   //stbte__ui.has_selection = 0;
   stbte__ui.copy_has_props = copy_props;
   stbte__ui.copy_src = tm; // used to give better semantics when copying links
   stbte__ui.copy_src_x = stbte__ui.select_x0;
   stbte__ui.copy_src_y = stbte__ui.select_y0;
}

static int stbte__in_rect(int x, int y, int x0, int y0, int w, int h)
{
   return x >= x0 && x < x0+w && y >= y0 && y < y0+h;
}

#ifdef STBTE_ALLOW_LINK
static int stbte__in_src_rect(int x, int y)
{
   return stbte__in_rect(x,y, stbte__ui.copy_src_x, stbte__ui.copy_src_y, stbte__ui.copy_width, stbte__ui.copy_height);
}

static int stbte__in_dest_rect(int x, int y, int destx, int desty)
{
   return stbte__in_rect(x,y, destx, desty, stbte__ui.copy_width, stbte__ui.copy_height);
}
#endif

static void stbte__paste(stbte_tilemap *tm, int mapx, int mapy)
{
   int w = stbte__ui.copy_width;
   int h = stbte__ui.copy_height;
   int i,j,k,p;
   int x = mapx - (w>>1);
   int y = mapy - (h>>1);
   int copy_props = stbte__should_copy_properties(tm) && stbte__ui.copy_has_props;
   if (stbte__ui.has_copy == 0)
      return;
   stbte__begin_undo(tm);
   p = 0;
   for (j=0; j < h; ++j) {
      for (i=0; i < w; ++i) {
         if (y+j >= 0 && y+j < tm->max_y && x+i >= 0 && x+i < tm->max_x) {
            // compute the new stack
            short tilestack[STBTE_MAX_LAYERS];
            for (k=0; k < tm->num_layers; ++k)
               tilestack[k] = tm->data[y+j][x+i][k];
            stbte__paste_stack(tm, tilestack, tilestack, stbte__ui.copybuffer[p], 0);
            // update anything that changed
            for (k=0; k < tm->num_layers; ++k) {
               if (tilestack[k] != tm->data[y+j][x+i][k]) {
                  stbte__undo_record(tm, x+i,y+j,k, tm->data[y+j][x+i][k]);
                  tm->data[y+j][x+i][k] = tilestack[k];
               }
            }
         }
         if (copy_props) {
#ifdef STBTE_ALLOW_LINK
            // need to decide how to paste a link, so there's a few cases
            int destx = -1, desty = -1;
            stbte__link *link = &stbte__ui.copylinks[p];

            // check if link is within-rect
            if (stbte__in_src_rect(link->x, link->y)) {
               // new link should point to copy (but only if copy is within map)
               destx = x + (link->x - stbte__ui.copy_src_x);
               desty = y + (link->y - stbte__ui.copy_src_y);
            } else if (tm == stbte__ui.copy_src) {
               // if same map, then preserve link unless target is overwritten
               if (!stbte__in_dest_rect(link->x,link->y,x,y)) {
                  destx = link->x;
                  desty = link->y;
               }
            }
            // this is necessary for offset-copy, but also in case max_x/max_y has changed
            if (destx < 0 || destx >= tm->max_x || desty < 0 || desty >= tm->max_y)
               destx = -1, desty = -1;
            stbte__set_link(tm, x+i, y+j, destx, desty, STBTE__undo_record);
#endif
            for (k=0; k < STBTE_MAX_PROPERTIES; ++k) {
               if (tm->props[y+j][x+i][k] != stbte__ui.copyprops[p][k])
                  stbte__undo_record_prop_float(tm, x+i, y+j, k, tm->props[y+j][x+i][k]);
            }
            stbte__copy_properties(tm->props[y+j][x+i], stbte__ui.copyprops[p]);
         }
         ++p;
      }
   }
   stbte__end_undo(tm);
}

static void stbte__drag_update(stbte_tilemap *tm, int mapx, int mapy, int copy_props)
{
   int w = stbte__ui.drag_w, h = stbte__ui.drag_h;
   int ox,oy,i,deleted=0,written=0;
   short temp[STBTE_MAX_LAYERS];
   short *data = NULL;

   STBTE__NOTUSED(deleted);
   STBTE__NOTUSED(written);

   if (!stbte__ui.shift) {
      ox = mapx - stbte__ui.drag_x;
      oy = mapy - stbte__ui.drag_y;
      if (ox >= 0 && ox < w && oy >= 0 && oy < h) {
         deleted=1;
         for (i=0; i < tm->num_layers; ++i)
            temp[i] = tm->data[mapy][mapx][i];
         data = temp;
         stbte__clear_stack(tm, data);
      }
   }
   ox = mapx - stbte__ui.drag_dest_x;
   oy = mapy - stbte__ui.drag_dest_y;
   // if this map square is in the target drag region
   if (ox >= 0 && ox < w && oy >= 0 && oy < h) {
      // and the src map square is on the map
      if (stbte__in_rect(stbte__ui.drag_x+ox, stbte__ui.drag_y+oy, 0, 0, tm->max_x, tm->max_y)) {
         written = 1;
         if (data == NULL) {
            for (i=0; i < tm->num_layers; ++i)
               temp[i] = tm->data[mapy][mapx][i];
            data = temp;
         }
         stbte__paste_stack(tm, data, data, tm->data[stbte__ui.drag_y+oy][stbte__ui.drag_x+ox], !stbte__ui.shift);
         if (copy_props) {
            for (i=0; i < STBTE_MAX_PROPERTIES; ++i) {
               if (tm->props[mapy][mapx][i] != tm->props[stbte__ui.drag_y+oy][stbte__ui.drag_x+ox][i]) {
                  stbte__undo_record_prop_float(tm, mapx, mapy, i, tm->props[mapy][mapx][i]);
                  tm->props[mapy][mapx][i] = tm->props[stbte__ui.drag_y+oy][stbte__ui.drag_x+ox][i];
               }
            }
         }
      }
   }
   if (data) {
      for (i=0; i < tm->num_layers; ++i) {
         if (tm->data[mapy][mapx][i] != data[i]) {
            stbte__undo_record(tm, mapx, mapy, i, tm->data[mapy][mapx][i]);
            tm->data[mapy][mapx][i] = data[i];
         }
      }
   }
   #ifdef STBTE_ALLOW_LINK
   if (copy_props) {
      int overwritten=0, moved=0, copied=0;
      // since this function is called on EVERY tile, we can fix up even tiles not
      // involved in the move

      stbte__link *k;
      // first, determine what src link ends up here
      k = &tm->link[mapy][mapx]; // by default, it's the one currently here
      if (deleted)               // if dragged away, it's erased
         k = NULL;
      if (written)               // if dragged into, it gets that link
         k = &tm->link[stbte__ui.drag_y+oy][stbte__ui.drag_x+ox];

      // now check whether the *target* gets moved or overwritten
      if (k && k->x >= 0) {
         overwritten = stbte__in_rect(k->x, k->y, stbte__ui.drag_dest_x, stbte__ui.drag_dest_y, w, h);
         if (!stbte__ui.shift)
            moved    = stbte__in_rect(k->x, k->y, stbte__ui.drag_x     , stbte__ui.drag_y     , w, h);
         else
            copied   = stbte__in_rect(k->x, k->y, stbte__ui.drag_x     , stbte__ui.drag_y     , w, h);
      }

      if (deleted || written || overwritten || moved || copied) {
         // choose the final link value based on the above
         if (k == NULL || k->x < 0)
            stbte__set_link(tm, mapx, mapy, -1, -1, STBTE__undo_record);
         else if (moved || (copied && written)) {
            // if we move the target, we update to point to the new target;
            // or, if we copy the target and the source is part of the copy, then update to new target
            int x = k->x + (stbte__ui.drag_dest_x - stbte__ui.drag_x);
            int y = k->y + (stbte__ui.drag_dest_y - stbte__ui.drag_y);
            if (!(x >= 0 && y >= 0 && x < tm->max_x && y < tm->max_y))
               x = -1, y = -1;
            stbte__set_link(tm, mapx, mapy, x, y, STBTE__undo_record);
         } else if (overwritten) {
            stbte__set_link(tm, mapx, mapy, -1, -1, STBTE__undo_record);
         } else
            stbte__set_link(tm, mapx, mapy, k->x, k->y, STBTE__undo_record);
      }
   }
   #endif
}

static void stbte__drag_place(stbte_tilemap *tm, int mapx, int mapy)
{
   int i,j;
   int copy_props = stbte__should_copy_properties(tm);
   int move_x = (stbte__ui.drag_dest_x - stbte__ui.drag_x);
   int move_y = (stbte__ui.drag_dest_y - stbte__ui.drag_y);
   if (move_x == 0 && move_y == 0)
      return;

   stbte__begin_undo(tm);
   // we now need a 2D memmove-style mover that doesn't
   // overwrite any data as it goes. this requires being
   // direction sensitive in the same way as memmove
   if (move_y > 0 || (move_y == 0 && move_x > 0)) {
      for (j=tm->max_y-1; j >= 0; --j)
         for (i=tm->max_x-1; i >= 0; --i)
            stbte__drag_update(tm,i,j,copy_props);
   } else {
      for (j=0; j < tm->max_y; ++j)
         for (i=0; i < tm->max_x; ++i)
            stbte__drag_update(tm,i,j,copy_props);
   }
   stbte__end_undo(tm);

   stbte__ui.has_selection = 1;
   stbte__ui.select_x0 = stbte__ui.drag_dest_x;
   stbte__ui.select_y0 = stbte__ui.drag_dest_y;
   stbte__ui.select_x1 = stbte__ui.select_x0 + stbte__ui.drag_w - 1;
   stbte__ui.select_y1 = stbte__ui.select_y0 + stbte__ui.drag_h - 1;
}

static void stbte__tile_paint(stbte_tilemap *tm, int sx, int sy, int mapx, int mapy, int layer)
{
   int i;
   int id = STBTE__IDMAP(mapx,mapy);
   int x0=sx, y0=sy;
   int x1=sx+tm->spacing_x, y1=sy+tm->spacing_y;
   stbte__hittest(x0,y0,x1,y1, id);
   short *data = tm->data[mapy][mapx];
   short temp[STBTE_MAX_LAYERS];

   if (STBTE__IS_MAP_HOT()) {
      if (stbte__ui.pasting) {
         int ox = mapx - stbte__ui.paste_x;
         int oy = mapy - stbte__ui.paste_y;
         if (ox >= 0 && ox < stbte__ui.copy_width && oy >= 0 && oy < stbte__ui.copy_height) {
            stbte__paste_stack(tm, temp, tm->data[mapy][mapx], stbte__ui.copybuffer[oy*stbte__ui.copy_width+ox], 0);
            data = temp;
         }
      } else if (stbte__ui.dragging) {
         int ox,oy;
         for (i=0; i < tm->num_layers; ++i)
            temp[i] = tm->data[mapy][mapx][i];
         data = temp;

         // if it's in the source area, remove things unless shift-dragging
         ox = mapx - stbte__ui.drag_x;
         oy = mapy - stbte__ui.drag_y;
         if (!stbte__ui.shift && ox >= 0 && ox < stbte__ui.drag_w && oy >= 0 && oy < stbte__ui.drag_h) {
            stbte__clear_stack(tm, temp);
         }

         ox = mapx - stbte__ui.drag_dest_x;
         oy = mapy - stbte__ui.drag_dest_y;
         if (ox >= 0 && ox < stbte__ui.drag_w && oy >= 0 && oy < stbte__ui.drag_h) {
            stbte__paste_stack(tm, temp, temp, tm->data[stbte__ui.drag_y+oy][stbte__ui.drag_x+ox], !stbte__ui.shift);
         }
      } else if (STBTE__IS_MAP_ACTIVE()) {
         if (stbte__ui.tool == STBTE__tool_rect) {
            if ((stbte__ui.ms_time & 511) < 380) {
               int ex = ((stbte__ui.hot_id >> 19) & 4095);
               int ey = ((stbte__ui.hot_id >>  7) & 4095);
               int sx = stbte__ui.sx;
               int sy = stbte__ui.sy;

               if (   ((mapx >= sx && mapx < ex+1) || (mapx >= ex && mapx < sx+1))
                   && ((mapy >= sy && mapy < ey+1) || (mapy >= ey && mapy < sy+1))) {
                  int i;
                  for (i=0; i < tm->num_layers; ++i)
                     temp[i] = tm->data[mapy][mapx][i];
                  data = temp;
                  if (stbte__ui.active_event == STBTE__leftdown)
                     stbte__brush_predict(tm, temp);
                  else
                     stbte__erase_predict(tm, temp, STBTE__erase_any);
               }
            }
         }
      }
   }

   if (STBTE__IS_HOT(id) && STBTE__INACTIVE() && !stbte__ui.pasting) {
      if (stbte__ui.tool == STBTE__tool_brush) {
         if ((stbte__ui.ms_time & 511) < 300) {
            data = temp;
            for (i=0; i < tm->num_layers; ++i)
               temp[i] = tm->data[mapy][mapx][i];
            stbte__brush_predict(tm, temp);
         }
      }
   }

   {
      i = layer;
      if (i == tm->solo_layer || (!tm->layerinfo[i].hidden && tm->solo_layer < 0))
         if (data[i] >= 0)
            STBTE_DRAW_TILE(sx,sy, (unsigned short) data[i], 0, tm->props[mapy][mapx]);
   }
}

static void stbte__tile(stbte_tilemap *tm, int sx, int sy, int mapx, int mapy)
{
   int tool = stbte__ui.tool;
   int x0=sx, y0=sy;
   int x1=sx+tm->spacing_x, y1=sy+tm->spacing_y;
   int id = STBTE__IDMAP(mapx,mapy);
   int over = stbte__hittest(x0,y0,x1,y1, id);
   switch (stbte__ui.event) {
      case STBTE__paint: {
         if (stbte__ui.pasting || stbte__ui.dragging || stbte__ui.scrolling)
            break;
         if (stbte__ui.scrollkey && !STBTE__IS_MAP_ACTIVE())
            break;
         if (STBTE__IS_HOT(id) && STBTE__IS_MAP_ACTIVE() && (tool == STBTE__tool_rect || tool == STBTE__tool_select)) {
            int rx0,ry0,rx1,ry1,t;
            // compute the center of each rect
            rx0 = x0 + tm->spacing_x/2;
            ry0 = y0 + tm->spacing_y/2;
            rx1 = rx0 + (stbte__ui.sx - mapx) * tm->spacing_x;
            ry1 = ry0 + (stbte__ui.sy - mapy) * tm->spacing_y;
            if (rx0 > rx1) t=rx0,rx0=rx1,rx1=t;
            if (ry0 > ry1) t=ry0,ry0=ry1,ry1=t;
            rx0 -= tm->spacing_x/2;
            ry0 -= tm->spacing_y/2;
            rx1 += tm->spacing_x/2;
            ry1 += tm->spacing_y/2;
            stbte__draw_frame(rx0-1,ry0-1,rx1+1,ry1+1, STBTE_COLOR_TILEMAP_HIGHLIGHT);
            break;
         }
         if (STBTE__IS_HOT(id) && STBTE__INACTIVE()) {
            stbte__draw_frame(x0-1,y0-1,x1+1,y1+1, STBTE_COLOR_TILEMAP_HIGHLIGHT);
         }
#ifdef STBTE_ALLOW_LINK
         if (stbte__ui.show_links && tm->link[mapy][mapx].x >= 0) {
            int tx = tm->link[mapy][mapx].x;
            int ty = tm->link[mapy][mapx].y;
            int lx0,ly0,lx1,ly1;
            if (STBTE_ALLOW_LINK(tm->data[mapy][mapx], tm->props[mapy][mapx],
                                 tm->data[ty  ][tx  ], tm->props[ty  ][tx  ]))
            {
               lx0 =  x0 + (tm->spacing_x >> 1) - 1;
               ly0 =  y0 + (tm->spacing_y >> 1) - 1;
               lx1 = lx0 + (tx - mapx) * tm->spacing_x + 2;
               ly1 = ly0 + (ty - mapy) * tm->spacing_y + 2;
               stbte__draw_link(lx0,ly0,lx1,ly1,
                   STBTE_LINK_COLOR(tm->data[mapy][mapx], tm->props[mapy][mapx],
                                    tm->data[ty  ][tx  ], tm->props[ty  ][tx]));
            }
         }
#endif
         break;
      }
   }

   if (stbte__ui.pasting) {
      switch (stbte__ui.event) {
         case STBTE__leftdown:
            if (STBTE__IS_HOT(id)) {
               stbte__ui.pasting = 0;
               stbte__paste(tm, mapx, mapy);
               stbte__activate(0);
            }
            break;
         case STBTE__leftup:
            // just clear it no matter what, since they might click away to clear it
            stbte__activate(0);
            break;
         case STBTE__rightdown:
            if (STBTE__IS_HOT(id)) {
               stbte__activate(0);
               stbte__ui.pasting = 0;
            }
            break;
      }
      return;
   }

   if (stbte__ui.scrolling) {
      if (stbte__ui.event == STBTE__leftup) {
         stbte__activate(0);
         stbte__ui.scrolling = 0;
      }
      if (stbte__ui.event == STBTE__mousemove) {
         tm->scroll_x += (stbte__ui.start_x - stbte__ui.mx);
         tm->scroll_y += (stbte__ui.start_y - stbte__ui.my);
         stbte__ui.start_x = stbte__ui.mx;
         stbte__ui.start_y = stbte__ui.my;
      }
      return;
   }

   // regardless of tool, leftdown is a scrolldrag
   if (STBTE__IS_HOT(id) && stbte__ui.scrollkey && stbte__ui.event == STBTE__leftdown) {
      stbte__ui.scrolling = 1;
      stbte__ui.start_x = stbte__ui.mx;
      stbte__ui.start_y = stbte__ui.my;
      return;
   }

   switch (tool) {
      case STBTE__tool_brush:
         switch (stbte__ui.event) {
            case STBTE__mousemove:
               if (STBTE__IS_MAP_ACTIVE() && over) {
                  // don't brush/erase same tile multiple times unless they move away and back @TODO should just be only once, but that needs another data structure
                  if (!STBTE__IS_ACTIVE(id)) {
                     if (stbte__ui.active_event == STBTE__leftdown)
                        stbte__brush(tm, mapx, mapy);
                     else
                        stbte__erase(tm, mapx, mapy, stbte__ui.brush_state);
                     stbte__ui.active_id = id; // switch to this map square so we don't rebrush IT multiple times
                  }
               }
               break;
            case STBTE__leftdown:
               if (STBTE__IS_HOT(id) && STBTE__INACTIVE()) {
                  stbte__activate(id);
                  stbte__begin_undo(tm);
                  stbte__brush(tm, mapx, mapy);
               }
               break;
            case STBTE__rightdown:
               if (STBTE__IS_HOT(id) && STBTE__INACTIVE()) {
                  stbte__activate(id);
                  stbte__begin_undo(tm);
                  if (stbte__erase(tm, mapx, mapy, STBTE__erase_any) == STBTE__erase_brushonly)
                     stbte__ui.brush_state = STBTE__erase_brushonly;
                  else
                     stbte__ui.brush_state = STBTE__erase_any;
               }
               break;
            case STBTE__leftup:
            case STBTE__rightup:
               if (STBTE__IS_MAP_ACTIVE()) {
                  stbte__end_undo(tm);
                  stbte__activate(0);
               }
               break;
         }
         break;

#ifdef STBTE_ALLOW_LINK
      case STBTE__tool_link:
         switch (stbte__ui.event) {
            case STBTE__leftdown:
               if (STBTE__IS_HOT(id) && STBTE__INACTIVE()) {
                  stbte__activate(id);
                  stbte__ui.linking = 1;
                  stbte__ui.sx = mapx;
                  stbte__ui.sy = mapy;
                  // @TODO: undo
               }
               break;
            case STBTE__leftup:
               if (STBTE__IS_HOT(id) && STBTE__IS_MAP_ACTIVE()) {
                  if ((mapx != stbte__ui.sx || mapy != stbte__ui.sy) &&
                         STBTE_ALLOW_LINK(tm->data[stbte__ui.sy][stbte__ui.sx], tm->props[stbte__ui.sy][stbte__ui.sx],
                                          tm->data[mapy][mapx], tm->props[mapy][mapx]))
                     stbte__set_link(tm, stbte__ui.sx, stbte__ui.sy, mapx, mapy, STBTE__undo_block);
                  else
                     stbte__set_link(tm, stbte__ui.sx, stbte__ui.sy, -1,-1, STBTE__undo_block);
                  stbte__ui.linking = 0;
                  stbte__activate(0);
               }
               break;

            case STBTE__rightdown:
               if (STBTE__IS_ACTIVE(id)) {
                  stbte__activate(0);
                  stbte__ui.linking = 0;
               }
               break;
         }
         break;
#endif

      case STBTE__tool_erase:
         switch (stbte__ui.event) {
            case STBTE__mousemove:
               if (STBTE__IS_MAP_ACTIVE() && over)
                  stbte__erase(tm, mapx, mapy, STBTE__erase_all);
               break;
            case STBTE__leftdown:
               if (STBTE__IS_HOT(id) && STBTE__INACTIVE()) {
                  stbte__activate(id);
                  stbte__begin_undo(tm);
                  stbte__erase(tm, mapx, mapy, STBTE__erase_all);
               }
               break;
            case STBTE__leftup:
               if (STBTE__IS_MAP_ACTIVE()) {
                  stbte__end_undo(tm);
                  stbte__activate(0);
               }
               break;
         }
         break;

      case STBTE__tool_select:
         if (STBTE__IS_HOT(id)) {
            switch (stbte__ui.event) {
               case STBTE__leftdown:
                  if (STBTE__INACTIVE()) {
                     // if we're clicking in an existing selection...
                     if (stbte__ui.has_selection) {
                        if (  mapx >= stbte__ui.select_x0 && mapx <= stbte__ui.select_x1
                           && mapy >= stbte__ui.select_y0 && mapy <= stbte__ui.select_y1)
                        {
                           stbte__ui.dragging = 1;
                           stbte__ui.drag_x = stbte__ui.select_x0;
                           stbte__ui.drag_y = stbte__ui.select_y0;
                           stbte__ui.drag_w = stbte__ui.select_x1 - stbte__ui.select_x0 + 1;
                           stbte__ui.drag_h = stbte__ui.select_y1 - stbte__ui.select_y0 + 1;
                           stbte__ui.drag_offx = mapx - stbte__ui.select_x0;
                           stbte__ui.drag_offy = mapy - stbte__ui.select_y0;
                        }
                     }
                     stbte__ui.has_selection = 0; // no selection until it completes
                     stbte__activate_map(mapx,mapy);
                  }
                  break;
               case STBTE__leftup:
                  if (STBTE__IS_MAP_ACTIVE()) {
                     if (stbte__ui.dragging) {
                        stbte__drag_place(tm, mapx,mapy);
                        stbte__ui.dragging = 0;
                        stbte__activate(0);
                     } else {
                        stbte__select_rect(tm, stbte__ui.sx, stbte__ui.sy, mapx, mapy);
                        stbte__activate(0);
                     }
                  }
                  break;
               case STBTE__rightdown:
                  stbte__ui.has_selection = 0;
                  break;
            }
         }
         break;

      case STBTE__tool_rect:
         if (STBTE__IS_HOT(id)) {
            switch (stbte__ui.event) {
               case STBTE__leftdown:
                  if (STBTE__INACTIVE())
                     stbte__activate_map(mapx,mapy);
                  break;
               case STBTE__leftup:
                  if (STBTE__IS_MAP_ACTIVE()) {
                     stbte__fillrect(tm, stbte__ui.sx, stbte__ui.sy, mapx, mapy, 1);
                     stbte__activate(0);
                  }
                  break;
               case STBTE__rightdown:
                  if (STBTE__INACTIVE())
                     stbte__activate_map(mapx,mapy);
                  break;
               case STBTE__rightup:
                  if (STBTE__IS_MAP_ACTIVE()) {
                     stbte__fillrect(tm, stbte__ui.sx, stbte__ui.sy, mapx, mapy, 0);
                     stbte__activate(0);
                  }
                  break;
            }
         }
         break;


      case STBTE__tool_eyedrop:
         switch (stbte__ui.event) {
            case STBTE__leftdown:
               if (STBTE__IS_HOT(id) && STBTE__INACTIVE())
                  stbte__eyedrop(tm,mapx,mapy);
               break;
         }
         break;
   }
}

static void stbte__start_paste(stbte_tilemap *tm)
{
   if (stbte__ui.has_copy) {
      stbte__ui.pasting = 1;
      stbte__activate(STBTE__ID(STBTE__toolbarB,3));
   }
}

static void stbte__toolbar(stbte_tilemap *tm, int x0, int y0, int w, int h)
{
   int i;
   int estimated_width = 13 * STBTE__num_tool + 8+8+ 120+4 - 30;
   int x = x0 + w/2 - estimated_width/2;
   int y = y0+1;

   for (i=0; i < STBTE__num_tool; ++i) {
      int highlight=0, disable=0;
      highlight = (stbte__ui.tool == i);
      if (i == STBTE__tool_undo || i == STBTE__tool_showgrid)
          x += 8;
      if (i == STBTE__tool_showgrid && stbte__ui.show_grid)
         highlight = 1;
      if (i == STBTE__tool_showlinks && stbte__ui.show_links)
         highlight = 1;
      if (i == STBTE__tool_fill)
         continue;
      #ifndef STBTE_ALLOW_LINK
      if (i == STBTE__tool_link || i == STBTE__tool_showlinks)
         disable = 1;
      #endif
      if (i == STBTE__tool_undo && !stbte__undo_available(tm))
         disable = 1;
      if (i == STBTE__tool_redo && !stbte__redo_available(tm))
         disable = 1;
      if (stbte__button_icon(STBTE__ctoolbar_button, toolchar[i], x, y, 13, STBTE__ID(STBTE__toolbarA, i), highlight, disable)) {
         switch (i) {
            case STBTE__tool_eyedrop:
               stbte__ui.eyedrop_last_layer = tm->num_layers; // flush eyedropper state
               // fallthrough
            default:
               stbte__ui.tool = i;
               stbte__ui.has_selection = 0;
               break;
            case STBTE__tool_showlinks:
               stbte__ui.show_links = !stbte__ui.show_links;
               break;
            case STBTE__tool_showgrid:
               stbte__ui.show_grid = (stbte__ui.show_grid+1)%3;
               break;
            case STBTE__tool_undo:
               stbte__undo(tm);
               break;
            case STBTE__tool_redo:
               stbte__redo(tm);
               break;
         }
      }
      x += 13;
   }

   x += 8;
   if (stbte__button(STBTE__ctoolbar_button, "cut"  , x, y,10, 40, STBTE__ID(STBTE__toolbarB,0), 0, !stbte__ui.has_selection))
      stbte__copy_cut(tm, 1);
   x += 42;
   if (stbte__button(STBTE__ctoolbar_button, "copy" , x, y, 5, 40, STBTE__ID(STBTE__toolbarB,1), 0, !stbte__ui.has_selection))
      stbte__copy_cut(tm, 0);
   x += 42;
   if (stbte__button(STBTE__ctoolbar_button, "paste", x, y, 0, 40, STBTE__ID(STBTE__toolbarB,2), stbte__ui.pasting, !stbte__ui.has_copy))
      stbte__start_paste(tm);
}

#define STBTE__TEXTCOLOR(n)  stbte__color_table[n][STBTE__text][STBTE__idle]

static int stbte__info_value(const char *label, int x, int y, int val, int digits, int id)
{
   if (stbte__ui.event == STBTE__paint) {
      int off = 9-stbte__get_char_width(label[0]);
      char text[16];
      stbte__sprintf(text stbte__sizeof(text), label, digits, val);
      stbte__draw_text_core(x+off,y, text, 999, STBTE__TEXTCOLOR(STBTE__cpanel),1);
   }
   if (id) {
      x += 9+7*digits+4;
      if (stbte__minibutton(STBTE__cmapsize, x,y, '+', STBTE__ID2(id,1,0)))
         val += (stbte__ui.shift ? 10 : 1);
      x += 9;
      if (stbte__minibutton(STBTE__cmapsize, x,y, '-', STBTE__ID2(id,2,0)))
         val -= (stbte__ui.shift ? 10 : 1);
      if (val < 1) val = 1; else if (val > 4096) val = 4096;
   }
   return val;
}

static void stbte__info(stbte_tilemap *tm, int x0, int y0, int w, int h)
{
   int mode = stbte__ui.panel[STBTE__panel_info].mode;
   int s = 11+7*tm->digits+4+15;
   int x,y;
   int in_region;

   x = x0+2;
   y = y0+2;
   tm->max_x = stbte__info_value("w:%*d",x,y, tm->max_x, tm->digits, STBTE__ID(STBTE__info,0));
   if (mode)
      x += s;
   else
      y += 11;
   tm->max_y = stbte__info_value("h:%*d",x,y, tm->max_y, tm->digits, STBTE__ID(STBTE__info,1));
   x = x0+2;
   y += 11;
   in_region = (stbte__ui.hot_id & 127) == STBTE__map;
   stbte__info_value(in_region ? "x:%*d" : "x:",x,y, (stbte__ui.hot_id>>19)&4095, tm->digits, 0);
   if (mode)
      x += s;
   else
      y += 11;
   stbte__info_value(in_region ? "y:%*d" : "y:",x,y, (stbte__ui.hot_id>> 7)&4095, tm->digits, 0);
   y += 15;
   x = x0+2;
   stbte__draw_text(x,y,"brush:",40,STBTE__TEXTCOLOR(STBTE__cpanel));
   if (tm->cur_tile >= 0)
      STBTE_DRAW_TILE(x+43,y-3,tm->tiles[tm->cur_tile].id,1,0);
}

static void stbte__layers(stbte_tilemap *tm, int x0, int y0, int w, int h)
{
   static const char *propmodes[3] = {
      "default", "always", "never"
   };
   int num_rows;
   int i, y, n;
   int x1 = x0+w;
   int y1 = y0+h;
   int xoff = 20;

   if (tm->has_layer_names) {
      int side = stbte__ui.panel[STBTE__panel_layers].side;
      xoff = stbte__region[side].width - 42;
      xoff = (xoff < tm->layername_width + 10 ? xoff : tm->layername_width + 10);
   }

   x0 += 2;
   y0 += 5;
   if (!tm->has_layer_names) {
      if (stbte__ui.event == STBTE__paint) {
         stbte__draw_text(x0,y0, "Layers", w-4, STBTE__TEXTCOLOR(STBTE__cpanel));
      }
      y0 += 11;
   }
   num_rows = (y1-y0)/15;
#ifndef STBTE_NO_PROPS
   --num_rows;
#endif
   y = y0;
   for (i=0; i < tm->num_layers; ++i) {
      char text[3], *str = (char *) tm->layerinfo[i].name;
      static char lockedchar[3] = { 'U', 'P', 'L' };
      int locked = tm->layerinfo[i].locked;
      int disabled = (tm->solo_layer >= 0 && tm->solo_layer != i);
      if (i-tm->layer_scroll >= 0 && i-tm->layer_scroll < num_rows) {
         if (str == NULL)
            stbte__sprintf(str=text stbte__sizeof(text), "%2d", i+1);
         if (stbte__button(STBTE__clayer_button, str, x0,y,(i+1<10)*2,xoff-2, STBTE__ID(STBTE__layer,i), tm->cur_layer==i,0))
            tm->cur_layer = (tm->cur_layer == i ? -1 : i);
         if (stbte__layerbutton(x0+xoff +  0,y+1,'H',STBTE__ID(STBTE__hide,i), tm->layerinfo[i].hidden,disabled,STBTE__clayer_hide))
            tm->layerinfo[i].hidden = !tm->layerinfo[i].hidden;
         if (stbte__layerbutton(x0+xoff + 12,y+1,lockedchar[locked],STBTE__ID(STBTE__lock,i), locked!=0,disabled,STBTE__clayer_lock))
            tm->layerinfo[i].locked = (locked+1)%3;
         if (stbte__layerbutton(x0+xoff + 24,y+1,'S',STBTE__ID(STBTE__solo,i), tm->solo_layer==i,0,STBTE__clayer_solo))
            tm->solo_layer = (tm->solo_layer == i ? -1 : i);
         y += 15;
      }
   }
   stbte__scrollbar(x1-4, y0,y-2, &tm->layer_scroll, 0, tm->num_layers, num_rows, STBTE__ID(STBTE__scrollbar_id, STBTE__layer));
#ifndef STBTE_NO_PROPS
   n = stbte__text_width("prop:")+2;
   stbte__draw_text(x0,y+2, "prop:", w, STBTE__TEXTCOLOR(STBTE__cpanel));
   i = w - n - 4;
   if (i > 50) i = 50;
   if (stbte__button(STBTE__clayer_button, propmodes[tm->propmode], x0+n,y,0,i, STBTE__ID(STBTE__layer,256), 0,0))
      tm->propmode = (tm->propmode+1)%3;
#endif
}

static void stbte__categories(stbte_tilemap *tm, int x0, int y0, int w, int h)
{
   int s=11, x,y, i;
   int num_rows = h / s;

   w -= 4;
   x = x0+2;
   y = y0+4;
   if (tm->category_scroll == 0) {
      if (stbte__category_button("*ALL*", x,y, w, STBTE__ID(STBTE__categories, 65535), tm->cur_category == -1)) {
         stbte__choose_category(tm, -1);
      }
      y += s;
   }

   for (i=0; i < tm->num_categories; ++i) {
      if (i+1 - tm->category_scroll >= 0 && i+1 - tm->category_scroll < num_rows) {
         if (y + 10 > y0+h)
            return;
         if (stbte__category_button(tm->categories[i], x,y,w, STBTE__ID(STBTE__categories,i), tm->cur_category == i))
            stbte__choose_category(tm, i);
         y += s;
      }
   }
   stbte__scrollbar(x0+w, y0+4, y0+h-4, &tm->category_scroll, 0, tm->num_categories+1, num_rows, STBTE__ID(STBTE__scrollbar_id, STBTE__categories));
}

static void stbte__tile_in_palette(stbte_tilemap *tm, int x, int y, int slot)
{
   stbte__tileinfo *t = &tm->tiles[slot];
   int x0=x, y0=y, x1 = x+tm->palette_spacing_x - 1, y1 = y+tm->palette_spacing_y;
   int id = STBTE__ID(STBTE__palette, slot);
   stbte__hittest(x0,y0,x1,y1, id);
   switch (stbte__ui.event) {
      case STBTE__paint:
         stbte__draw_rect(x,y,x+tm->palette_spacing_x-1,y+tm->palette_spacing_x-1, STBTE_COLOR_TILEPALETTE_BACKGROUND);
         STBTE_DRAW_TILE(x,y,id, slot == tm->cur_tile,0);
         if (slot == tm->cur_tile)
            stbte__draw_frame_delayed(x-1,y-1,x+tm->palette_spacing_x,y+tm->palette_spacing_y, STBTE_COLOR_TILEPALETTE_OUTLINE);
         break;
      default:
         if (stbte__button_core(id))
            tm->cur_tile = slot;
         break;
   }
}

static void stbte__palette_of_tiles(stbte_tilemap *tm, int x0, int y0, int w, int h)
{
   int i,x,y;
   int num_vis_rows = (h-6) / tm->palette_spacing_y;
   int num_columns = (w-2-6) / tm->palette_spacing_x;
   int num_total_rows;
   int column,row;
   int x1 = x0+w, y1=y0+h;
   x = x0+2;
   y = y0+6;

   if (num_columns == 0)
      return;

   num_total_rows = (tm->cur_palette_count + num_columns-1) / num_columns; // ceil()

   column = 0;
   row    = -tm->palette_scroll;
   for (i=0; i < tm->num_tiles; ++i) {
      stbte__tileinfo *t = &tm->tiles[i];

      // filter based on category
      if (tm->cur_category >= 0 && t->category_id != tm->cur_category)
         continue;

      // display it
      if (row >= 0 && row < num_vis_rows) {
         x = x0 + 2 + tm->palette_spacing_x * column;
         y = y0 + 6 + tm->palette_spacing_y * row;
         stbte__tile_in_palette(tm,x,y,i);
      }

      ++column;
      if (column == num_columns) {
         column = 0;
         ++row;
      }
   }
   stbte__flush_delay();
   stbte__scrollbar(x1-4, y0+6, y1-2, &tm->palette_scroll, 0, num_total_rows, num_vis_rows, STBTE__ID(STBTE__scrollbar_id, STBTE__palette));
}

static float stbte__saved;
static void stbte__props_panel(stbte_tilemap *tm, int x0, int y0, int w, int h)
{
   int x1 = x0+w;
   int i;
   int y = y0 + 5, x = x0+2;
   int slider_width = 60;
   int mx,my;
   float *p;
   short *data;
   if (!stbte__is_single_selection())
      return;
   mx = stbte__ui.select_x0;
   my = stbte__ui.select_y0;
   p = tm->props[my][mx];
   data = tm->data[my][mx];
   STBTE__NOTUSED(data);
   for (i=0; i < STBTE_MAX_PROPERTIES; ++i) {
      unsigned int n = STBTE_PROP_TYPE(i, data, p);
      if (n) {
         char *s = (char*) STBTE_PROP_NAME(i, data, p);
         if (s == NULL) s = (char*) "";
         switch (n & 3) {
            case STBTE_PROP_bool: {
               int flag = (int) p[i];
               if (stbte__layerbutton(x,y, flag ? 'x' : ' ', STBTE__ID(STBTE__prop_flag,i), flag, 0, 2)) {
                  stbte__begin_undo(tm);
                  stbte__undo_record_prop_float(tm,mx,my,i,(float) flag);
                  p[i] = (float) !flag;
                  stbte__end_undo(tm);
               }
               stbte__draw_text(x+13,y+1,s,x1-(x+13)-2,STBTE__TEXTCOLOR(STBTE__cpanel));
               y += 13;
               break;
            }
            case STBTE_PROP_int: {
               int a = (int) STBTE_PROP_MIN(i,data,p);
               int b = (int) STBTE_PROP_MAX(i,data,p);
               int v = (int) p[i] - a;
               if (a+v != p[i] || v < 0 || v > b-a) {
                  if (v < 0) v = 0;
                  if (v > b-a) v = b-a;
                  p[i] = (float) (a+v); // @TODO undo
               }
               switch (stbte__slider(x, slider_width, y+7, b-a, &v, STBTE__ID(STBTE__prop_int,i)))
               {
                  case STBTE__begin:
                     stbte__saved = p[i];
                     // fallthrough
                  case STBTE__change:
                     p[i] = (float) (a+v); // @TODO undo
                     break;
                  case STBTE__end:
                     if (p[i] != stbte__saved) {
                        stbte__begin_undo(tm);
                        stbte__undo_record_prop_float(tm,mx,my,i,stbte__saved);
                        stbte__end_undo(tm);
                     }
                     break;
               }
               stbte__draw_text(x+slider_width+2,y+2, s, x1-1-(x+slider_width+2), STBTE__TEXTCOLOR(STBTE__cpanel));
               y += 12;
               break;
            }
            case STBTE_PROP_float: {
               float a = (float) STBTE_PROP_MIN(i, data,p);
               float b = (float) STBTE_PROP_MAX(i, data,p);
               float c = STBTE_PROP_FLOAT_SCALE(i, data, p);
               float old;
               if (p[i] < a || p[i] > b) {
                  // @TODO undo
                  if (p[i] < a) p[i] = a;
                  if (p[i] > b) p[i] = b;
               }
               old = p[i];
               switch (stbte__float_control(x, y, 50, a, b, c, "%8.4f", &p[i], STBTE__layer,STBTE__ID(STBTE__prop_float,i))) {
                  case STBTE__begin:
                     stbte__saved = old;
                     break;
                  case STBTE__end:
                     if (stbte__saved != p[i]) {
                        stbte__begin_undo(tm);
                        stbte__undo_record_prop_float(tm,mx,my,i, stbte__saved);
                        stbte__end_undo(tm);
                     }
                     break;
               }
               stbte__draw_text(x+53,y+1, s, x1-1-(x+53), STBTE__TEXTCOLOR(STBTE__cpanel));
               y += 12;
               break;
            }
         }
      }
   }
}

static int stbte__cp_mode, stbte__cp_aspect, stbte__save, stbte__cp_altered;
#ifdef STBTE__COLORPICKER
static int stbte__cp_state, stbte__cp_index, stbte__color_copy;
static void stbte__dump_colorstate(void)
{
   int i,j,k;
   printf("static int stbte__color_table[STBTE__num_color_modes][STBTE__num_color_aspects][STBTE__num_color_states] =\n");
   printf("{\n");
   printf("   {\n");
   for (k=0; k < STBTE__num_color_modes; ++k) {
      for (j=0; j < STBTE__num_color_aspects; ++j) {
         printf("      { ");
         for (i=0; i < STBTE__num_color_states; ++i) {
            printf("0x%06x, ", stbte__color_table[k][j][i]);
         }
         printf("},\n");
      }
      if (k+1 < STBTE__num_color_modes)
         printf("   }, {\n");
      else
         printf("   },\n");
   }
   printf("};\n");
}

static void stbte__colorpicker(int x0, int y0, int w, int h)
{
   int x1 = x0+w, y1 = y0+h, x,y, i;

   x =  x0+2; y = y0+6;

   y += 5;
   x += 8;


   {
      int color = stbte__color_table[stbte__cp_mode][stbte__cp_aspect][stbte__cp_index];
      int rgb[3];
      if (stbte__cp_altered && stbte__cp_index == STBTE__idle)
         color = stbte__save;

      if (stbte__minibutton(STBTE__cmapsize, x1-20,y+ 5, 'C', STBTE__ID2(STBTE__colorpick_id,4,0)))
         stbte__color_copy = color;
      if (stbte__minibutton(STBTE__cmapsize, x1-20,y+15, 'P', STBTE__ID2(STBTE__colorpick_id,4,1)))
         color = stbte__color_copy;

      rgb[0] = color >> 16; rgb[1] = (color>>8)&255; rgb[2] = color & 255;
      for (i=0; i < 3; ++i) {
         if (stbte__slider(x+8,64, y, 255, rgb+i, STBTE__ID2(STBTE__colorpick_id,3,i)) > 0)
            stbte__dump_colorstate();
         y += 15;
      }
      if (stbte__ui.event != STBTE__paint && stbte__ui.event != STBTE__tick)
         stbte__color_table[stbte__cp_mode][stbte__cp_aspect][stbte__cp_index] = (rgb[0]<<16)|(rgb[1]<<8)|(rgb[2]);
   }

   y += 5;

   // states
   x = x0+2+35;
   if (stbte__ui.event == STBTE__paint) {
      static char *states[] = { "idle", "over", "down", "down&over", "selected", "selected&over", "disabled" };
      stbte__draw_text(x, y+1, states[stbte__cp_index], x1-x-1, 0xffffff);
   }

   x = x0+24; y += 12;

   for (i=3; i >= 0; --i) {
      int state = 0 != (stbte__cp_state & (1 << i));
      if (stbte__layerbutton(x,y, "OASD"[i], STBTE__ID2(STBTE__colorpick_id, 0,i), state,0, STBTE__clayer_button)) {
         stbte__cp_state ^= (1 << i);
         stbte__cp_index = stbte__state_to_index[0][0][0][stbte__cp_state];
      }
      x += 16;
   }
   x = x0+2; y += 18;

   for (i=0; i < 3; ++i) {
      static char *labels[] = { "Base", "Edge", "Text" };
      if (stbte__button(STBTE__ctoolbar_button, labels[i], x,y,0,36, STBTE__ID2(STBTE__colorpick_id,1,i), stbte__cp_aspect==i,0))
         stbte__cp_aspect = i;
      x += 40;
   }

   y += 18;
   x = x0+2;

   for (i=0; i < STBTE__num_color_modes; ++i) {
      if (stbte__button(STBTE__ctoolbar_button, stbte__color_names[i], x, y, 0,80, STBTE__ID2(STBTE__colorpick_id,2,i), stbte__cp_mode == i,0))
         stbte__cp_mode = i;
      y += 12;
   }

   // make the currently selected aspect flash, unless we're actively dragging color slider etc
   if (stbte__ui.event == STBTE__tick) {
      stbte__save = stbte__color_table[stbte__cp_mode][stbte__cp_aspect][STBTE__idle];
      if ((stbte__ui.active_id & 127) != STBTE__colorpick_id) {
         if ((stbte__ui.ms_time & 2047) < 200) {
            stbte__color_table[stbte__cp_mode][stbte__cp_aspect][STBTE__idle] ^= 0x1f1f1f;
            stbte__cp_altered = 1;
         }
      }
   }
}
#endif

static void stbte__editor_traverse(stbte_tilemap *tm)
{
   int i,j,i0,j0,i1,j1,n;

   if (tm == NULL)
      return;
   if (stbte__ui.x0 == stbte__ui.x1 || stbte__ui.y0 == stbte__ui.y1)
      return;

   stbte__prepare_tileinfo(tm);

   stbte__compute_panel_locations(tm); // @OPTIMIZE: we don't need to recompute this every time

   if (stbte__ui.event == STBTE__paint) {
      // fill screen with border
      stbte__draw_rect(stbte__ui.x0, stbte__ui.y0, stbte__ui.x1, stbte__ui.y1, STBTE_COLOR_TILEMAP_BORDER);
      // fill tilemap with tilemap background
      stbte__draw_rect(stbte__ui.x0 - tm->scroll_x, stbte__ui.y0 - tm->scroll_y,
                       stbte__ui.x0 - tm->scroll_x + tm->spacing_x * tm->max_x,
                       stbte__ui.y0 - tm->scroll_y + tm->spacing_y * tm->max_y, STBTE_COLOR_TILEMAP_BACKGROUND);
   }

   // step 1: traverse all the tilemap data...

   i0 = (tm->scroll_x - tm->spacing_x) / tm->spacing_x;
   j0 = (tm->scroll_y - tm->spacing_y) / tm->spacing_y;
   i1 = (tm->scroll_x + stbte__ui.x1 - stbte__ui.x0) / tm->spacing_x + 1;
   j1 = (tm->scroll_y + stbte__ui.y1 - stbte__ui.y0) / tm->spacing_y + 1;

   if (i0 < 0) i0 = 0;
   if (j0 < 0) j0 = 0;
   if (i1 > tm->max_x) i1 = tm->max_x;
   if (j1 > tm->max_y) j1 = tm->max_y;

   if (stbte__ui.event == STBTE__paint) {
      // draw all of layer 0, then all of layer 1, etc, instead of old
      // way which drew entire stack of each tile at once
      for (n=0; n < tm->num_layers; ++n) {
         for (j=j0; j < j1; ++j) {
            for (i=i0; i < i1; ++i) {
               int x = stbte__ui.x0 + i * tm->spacing_x - tm->scroll_x;
               int y = stbte__ui.y0 + j * tm->spacing_y - tm->scroll_y;
               stbte__tile_paint(tm, x, y, i, j, n);
            }
         }
         if (n == 0 && stbte__ui.show_grid == 1) {
            int x = stbte__ui.x0 + i0 * tm->spacing_x - tm->scroll_x;
            int y = stbte__ui.y0 + j0 * tm->spacing_y - tm->scroll_y;
            for (i=0; x < stbte__ui.x1 && i <= i1; ++i, x += tm->spacing_x)
               stbte__draw_rect(x, stbte__ui.y0, x+1, stbte__ui.y1, STBTE_COLOR_GRID);
            for (j=0; y < stbte__ui.y1 && j <= j1; ++j, y += tm->spacing_y)
               stbte__draw_rect(stbte__ui.x0, y, stbte__ui.x1, y+1, STBTE_COLOR_GRID);
         }
      }
   }

   if (stbte__ui.event == STBTE__paint) {
      // draw grid on top of everything except UI
      if (stbte__ui.show_grid == 2) {
         int x = stbte__ui.x0 + i0 * tm->spacing_x - tm->scroll_x;
         int y = stbte__ui.y0 + j0 * tm->spacing_y - tm->scroll_y;
         for (i=0; x < stbte__ui.x1 && i <= i1; ++i, x += tm->spacing_x)
            stbte__draw_rect(x, stbte__ui.y0, x+1, stbte__ui.y1, STBTE_COLOR_GRID);
         for (j=0; y < stbte__ui.y1 && j <= j1; ++j, y += tm->spacing_y)
            stbte__draw_rect(stbte__ui.x0, y, stbte__ui.x1, y+1, STBTE_COLOR_GRID);
      }
   }

   for (j=j0; j < j1; ++j) {
      for (i=i0; i < i1; ++i) {
         int x = stbte__ui.x0 + i * tm->spacing_x - tm->scroll_x;
         int y = stbte__ui.y0 + j * tm->spacing_y - tm->scroll_y;
         stbte__tile(tm, x, y, i, j);
      }
   }

   if (stbte__ui.event == STBTE__paint) {
      // draw the selection border
      if (stbte__ui.has_selection) {
         int x0,y0,x1,y1;
         x0 = stbte__ui.x0 + (stbte__ui.select_x0    ) * tm->spacing_x - tm->scroll_x;
         y0 = stbte__ui.y0 + (stbte__ui.select_y0    ) * tm->spacing_y - tm->scroll_y;
         x1 = stbte__ui.x0 + (stbte__ui.select_x1 + 1) * tm->spacing_x - tm->scroll_x + 1;
         y1 = stbte__ui.y0 + (stbte__ui.select_y1 + 1) * tm->spacing_y - tm->scroll_y + 1;
         stbte__draw_frame(x0,y0,x1,y1, (stbte__ui.ms_time & 256 ? STBTE_COLOR_SELECTION_OUTLINE1 : STBTE_COLOR_SELECTION_OUTLINE2));
      }

      stbte__flush_delay(); // draw a dynamic link on top of the queued links

      #ifdef STBTE_ALLOW_LINK
      if (stbte__ui.linking && STBTE__IS_MAP_HOT()) {
         int x0,y0,x1,y1;
         int color;
         int ex = ((stbte__ui.hot_id >> 19) & 4095);
         int ey = ((stbte__ui.hot_id >>  7) & 4095);
         x0 = stbte__ui.x0 + (stbte__ui.sx    ) * tm->spacing_x - tm->scroll_x + (tm->spacing_x>>1)+1;
         y0 = stbte__ui.y0 + (stbte__ui.sy    ) * tm->spacing_y - tm->scroll_y + (tm->spacing_y>>1)+1;
         x1 = stbte__ui.x0 + (ex              ) * tm->spacing_x - tm->scroll_x + (tm->spacing_x>>1)-1;
         y1 = stbte__ui.y0 + (ey              ) * tm->spacing_y - tm->scroll_y + (tm->spacing_y>>1)-1;
         if (STBTE_ALLOW_LINK(tm->data[stbte__ui.sy][stbte__ui.sx], tm->props[stbte__ui.sy][stbte__ui.sx], tm->data[ey][ex], tm->props[ey][ex]))
            color = STBTE_LINK_COLOR_DRAWING;
         else
            color = STBTE_LINK_COLOR_DISALLOWED;
         stbte__draw_link(x0,y0,x1,y1, color);
      }
      #endif
   }
   stbte__flush_delay();

   // step 2: traverse the panels
   for (i=0; i < STBTE__num_panel; ++i) {
      stbte__panel *p = &stbte__ui.panel[i];
      if (stbte__ui.event == STBTE__paint) {
         stbte__draw_box(p->x0,p->y0,p->x0+p->width,p->y0+p->height, STBTE__cpanel, STBTE__idle);
      }
      // obscure tilemap data underneath panel
      stbte__hittest(p->x0,p->y0,p->x0+p->width,p->y0+p->height, STBTE__ID2(STBTE__panel, i, 0));
      switch (i) {
         case STBTE__panel_toolbar:
            if (stbte__ui.event == STBTE__paint)
               stbte__draw_rect(p->x0,p->y0,p->x0+p->width,p->y0+p->height, stbte__color_table[STBTE__ctoolbar][STBTE__base][STBTE__idle]);
            stbte__toolbar(tm,p->x0,p->y0,p->width,p->height);
            break;
         case STBTE__panel_info:
            stbte__info(tm,p->x0,p->y0,p->width,p->height);
            break;
         case STBTE__panel_layers:
            stbte__layers(tm,p->x0,p->y0,p->width,p->height);
            break;
         case STBTE__panel_categories:
            stbte__categories(tm,p->x0,p->y0,p->width,p->height);
            break;
         case STBTE__panel_colorpick:
#ifdef STBTE__COLORPICKER
            stbte__colorpicker(p->x0,p->y0,p->width,p->height);
#endif
            break;
         case STBTE__panel_tiles:
            // erase boundary between categories and tiles if they're on same side
            if (stbte__ui.event == STBTE__paint && p->side == stbte__ui.panel[STBTE__panel_categories].side)
               stbte__draw_rect(p->x0+1,p->y0-1,p->x0+p->width-1,p->y0+1, stbte__color_table[STBTE__cpanel][STBTE__base][STBTE__idle]);
            stbte__palette_of_tiles(tm,p->x0,p->y0,p->width,p->height);
            break;
         case STBTE__panel_props:
            stbte__props_panel(tm,p->x0,p->y0,p->width,p->height);
            break;
      }
      // draw the panel side selectors
      for (j=0; j < 2; ++j) {
         int result;
         if (i == STBTE__panel_toolbar) continue;
         result = stbte__microbutton(p->x0+p->width - 1 - 2*4 + 4*j,p->y0+2,3, STBTE__ID2(STBTE__panel, i, j+1), STBTE__cpanel_sider+j);
         if (result) {
            switch (j) {
               case 0: p->side = result > 0 ? STBTE__side_left : STBTE__side_right; break;
               case 1: p->delta_height += result; break;
            }
         }
      }
   }

   if (stbte__ui.panel[STBTE__panel_categories].delta_height < -5) stbte__ui.panel[STBTE__panel_categories].delta_height = -5;
   if (stbte__ui.panel[STBTE__panel_layers    ].delta_height < -5) stbte__ui.panel[STBTE__panel_layers    ].delta_height = -5;


   // step 3: traverse the regions to place expander controls on them
   for (i=0; i < 2; ++i) {
      if (stbte__region[i].active) {
         int x = stbte__region[i].x;
         int width;
         if (i == STBTE__side_left)
            width =  stbte__ui.left_width , x += stbte__region[i].width + 1;
         else
            width = -stbte__ui.right_width, x -= 6;
         if (stbte__microbutton_dragger(x, stbte__region[i].y+2, 5, STBTE__ID(STBTE__region,i), &width)) {
            // if non-0, it is expanding, so retract it
            if (stbte__region[i].retracted == 0.0)
               stbte__region[i].retracted = 0.01f;
            else
               stbte__region[i].retracted = 0.0;
         }
         if (i == STBTE__side_left)
            stbte__ui.left_width  =  width;
         else
            stbte__ui.right_width = -width;
         if (stbte__ui.event == STBTE__tick) {
            if (stbte__region[i].retracted && stbte__region[i].retracted < 1.0f) {
               stbte__region[i].retracted += stbte__ui.dt*4;
               if (stbte__region[i].retracted > 1)
                  stbte__region[i].retracted = 1;
            }
         }
      }
   }

   if (stbte__ui.event == STBTE__paint && stbte__ui.alert_msg) {
      int w = stbte__text_width(stbte__ui.alert_msg);
      int x = (stbte__ui.x0+stbte__ui.x1)/2;
      int y = (stbte__ui.y0+stbte__ui.y1)*5/6;
      stbte__draw_rect (x-w/2-4,y-8, x+w/2+4,y+8, 0x604020);
      stbte__draw_frame(x-w/2-4,y-8, x+w/2+4,y+8, 0x906030);
      stbte__draw_text (x-w/2,y-4, stbte__ui.alert_msg, w+1, 0xff8040);
   }

#ifdef STBTE_SHOW_CURSOR
   if (stbte__ui.event == STBTE__paint)
      stbte__draw_bitmap(stbte__ui.mx, stbte__ui.my, stbte__get_char_width(26), stbte__get_char_bitmap(26), 0xe0e0e0);
#endif

   if (stbte__ui.event == STBTE__tick && stbte__ui.alert_msg) {
      stbte__ui.alert_timer -= stbte__ui.dt;
      if (stbte__ui.alert_timer < 0) {
         stbte__ui.alert_timer = 0;
         stbte__ui.alert_msg = 0;
      }
   }

   if (stbte__ui.event == STBTE__paint) {
      stbte__color_table[stbte__cp_mode][stbte__cp_aspect][STBTE__idle] = stbte__save;
      stbte__cp_altered = 0;
   }
}

static void stbte__do_event(stbte_tilemap *tm)
{
   stbte__ui.next_hot_id = 0;
   stbte__editor_traverse(tm);
   stbte__ui.hot_id = stbte__ui.next_hot_id;

   // automatically cancel on mouse-up in case the object that triggered it
   // doesn't exist anymore
   if (stbte__ui.active_id) {
      if (stbte__ui.event == STBTE__leftup || stbte__ui.event == STBTE__rightup) {
         if (!stbte__ui.pasting) {
            stbte__activate(0);
            if (stbte__ui.undoing)
               stbte__end_undo(tm);
            stbte__ui.scrolling = 0;
            stbte__ui.dragging = 0;
            stbte__ui.linking = 0;
         }
      }
   }

   // we could do this stuff in the widgets directly, but it would keep recomputing
   // the same thing on every tile, which seems dumb.

   if (stbte__ui.pasting) {
      if (STBTE__IS_MAP_HOT()) {
         // compute pasting location based on last hot
         stbte__ui.paste_x = ((stbte__ui.hot_id >> 19) & 4095) - (stbte__ui.copy_width >> 1);
         stbte__ui.paste_y = ((stbte__ui.hot_id >>  7) & 4095) - (stbte__ui.copy_height >> 1);
      }
   }
   if (stbte__ui.dragging) {
      if (STBTE__IS_MAP_HOT()) {
         stbte__ui.drag_dest_x = ((stbte__ui.hot_id >> 19) & 4095) - stbte__ui.drag_offx;
         stbte__ui.drag_dest_y = ((stbte__ui.hot_id >>  7) & 4095) - stbte__ui.drag_offy;
      }
   }
}

static void stbte__set_event(int event, int x, int y)
{
   stbte__ui.event = event;
   stbte__ui.mx    = x;
   stbte__ui.my    = y;
   stbte__ui.dx    = x - stbte__ui.last_mouse_x;
   stbte__ui.dy    = y - stbte__ui.last_mouse_y;
   stbte__ui.last_mouse_x = x;
   stbte__ui.last_mouse_y = y;
   stbte__ui.accum_x += stbte__ui.dx;
   stbte__ui.accum_y += stbte__ui.dy;
}

void stbte_draw(stbte_tilemap *tm)
{
   stbte__ui.event = STBTE__paint;
   stbte__editor_traverse(tm);
}

void stbte_mouse_move(stbte_tilemap *tm, int x, int y, int shifted, int scrollkey)
{
   stbte__set_event(STBTE__mousemove, x,y);
   stbte__ui.shift = shifted;
   stbte__ui.scrollkey = scrollkey;
   stbte__do_event(tm);
}

void stbte_mouse_button(stbte_tilemap *tm, int x, int y, int right, int down, int shifted, int scrollkey)
{
   static int events[2][2] = { { STBTE__leftup , STBTE__leftdown  },
                               { STBTE__rightup, STBTE__rightdown } };
   stbte__set_event(events[right][down], x,y);
   stbte__ui.shift = shifted;
   stbte__ui.scrollkey = scrollkey;

   stbte__do_event(tm);
}

void stbte_mouse_wheel(stbte_tilemap *tm, int x, int y, int vscroll)
{
   // not implemented yet -- need different way of hittesting
}

void stbte_action(stbte_tilemap *tm, enum stbte_action act)
{
   switch (act) {
      case STBTE_tool_select:      stbte__ui.tool = STBTE__tool_select;               break;
      case STBTE_tool_brush:       stbte__ui.tool = STBTE__tool_brush;                break;
      case STBTE_tool_erase:       stbte__ui.tool = STBTE__tool_erase;                break;
      case STBTE_tool_rectangle:   stbte__ui.tool = STBTE__tool_rect;                 break;
      case STBTE_tool_eyedropper:  stbte__ui.tool = STBTE__tool_eyedrop;              break;
      case STBTE_tool_link:        stbte__ui.tool = STBTE__tool_link;                 break;
      case STBTE_act_toggle_grid:  stbte__ui.show_grid = (stbte__ui.show_grid+1) % 3; break;
      case STBTE_act_toggle_links: stbte__ui.show_links ^= 1;                         break;
      case STBTE_act_undo:         stbte__undo(tm);                                   break;
      case STBTE_act_redo:         stbte__redo(tm);                                   break;
      case STBTE_act_cut:          stbte__copy_cut(tm, 1);                            break;
      case STBTE_act_copy:         stbte__copy_cut(tm, 0);                            break;
      case STBTE_act_paste:        stbte__start_paste(tm);                            break;
      case STBTE_scroll_left:      tm->scroll_x -= tm->spacing_x;                     break;
      case STBTE_scroll_right:     tm->scroll_x += tm->spacing_x;                     break;
      case STBTE_scroll_up:        tm->scroll_y -= tm->spacing_y;                     break;
      case STBTE_scroll_down:      tm->scroll_y += tm->spacing_y;                     break;
   }
}

void stbte_tick(stbte_tilemap *tm, float dt)
{
   stbte__ui.event = STBTE__tick;
   stbte__ui.dt    = dt;
   stbte__do_event(tm);
   stbte__ui.ms_time += (int) (dt * 1024) + 1; // make sure if time is superfast it always updates a little
}

void stbte_mouse_sdl(stbte_tilemap *tm, const void *sdl_event, float xs, float ys, int xo, int yo)
{
#ifdef _SDL_H
   SDL_Event *event = (SDL_Event *) sdl_event;
   SDL_Keymod km = SDL_GetModState();
   int shift = (km & KMOD_LCTRL) || (km & KMOD_RCTRL);
   int scrollkey = 0 != SDL_GetKeyboardState(NULL)[SDL_SCANCODE_SPACE];
   switch (event->type) {
      case SDL_MOUSEMOTION:
         stbte_mouse_move(tm, (int) (xs*event->motion.x+xo), (int) (ys*event->motion.y+yo), shift, scrollkey);
         break;
      case SDL_MOUSEBUTTONUP:
         stbte_mouse_button(tm, (int) (xs*event->button.x+xo), (int) (ys*event->button.y+yo), event->button.button != SDL_BUTTON_LEFT, 0, shift, scrollkey);
         break;
      case SDL_MOUSEBUTTONDOWN:
         stbte_mouse_button(tm, (int) (xs*event->button.x+xo), (int) (ys*event->button.y+yo), event->button.button != SDL_BUTTON_LEFT, 1, shift, scrollkey);
         break;
      case SDL_MOUSEWHEEL:
         stbte_mouse_wheel(tm, stbte__ui.mx, stbte__ui.my, event->wheel.y);
         break;
   }
#else
   STBTE__NOTUSED(tm);
   STBTE__NOTUSED(sdl_event);
   STBTE__NOTUSED(xs);
   STBTE__NOTUSED(ys);
   STBTE__NOTUSED(xo);
   STBTE__NOTUSED(yo);
#endif
}

#endif // STB_TILEMAP_EDITOR_IMPLEMENTATION

/*
------------------------------------------------------------------------------
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2017 Sean Barrett
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
*/