diff --git a/guichan/include/guichan/widgets/textfield.h b/guichan/include/guichan/widgets/textfield.h
index 44de480ff..12e569189 100644
--- a/guichan/include/guichan/widgets/textfield.h
+++ b/guichan/include/guichan/widgets/textfield.h
@@ -97,6 +97,37 @@ namespace gcn
          */
         virtual const std::string& getText() const;
 
+        /**
+         * Sets the maximum number of bytes that the user can type in
+         * the TextField.  Because TextField uses the UTF-8 encoding,
+         * each character may require several bytes.
+         *
+         * If the text in the field is already too long, this function
+         * truncates it immediately.
+         *
+         * @param maxLengthBytes The new maximum length, in bytes.
+         * Must not be negative.
+         *
+         * In addition to this UTF-8 byte limit, we could also
+         * implement limits on the number of code points, the number
+         * of UTF-16 code units, the number of grapheme clusters, or
+         * the width of the string in a given font.  However, there
+         * haven't been any use cases for those so far.
+         *
+         * @seealso #getMaxLengthBytes
+         */
+        void setMaxLengthBytes(int maxLengthBytes);
+
+        /**
+         * Gets the maximum number of bytes that the user can type in
+         * the TextField.
+         *
+         * @return The current maximum length, in bytes.
+         *
+         * @seealso #setMaxLengthBytes
+         */
+        int getMaxLengthBytes() const;
+
         /**
          * Draws the caret (the little marker in the text that shows where the
          * letters you type will appear). Easily overloaded if you want to
@@ -163,10 +194,49 @@ namespace gcn
          */
         void fixScroll();
 
+        /**
+         * Truncates #mText to #mMaxLengthBytes, and #mCaretPosition
+         * and #mSelectStart to the length of #mText.  Marks the
+         * widget dirty if it changes anything.
+         */
+        void truncateToMaxLength();
+
+        /**
+         * Inserts a string at the caret, replacing the selection if any.
+         *
+         * This function also marks the widget dirty.  However, it
+         * does not call #fixScroll because that is somewhat
+         * expensive and should not be called redundantly.  Therefore,
+         * the caller must do that.
+         *
+         * @param text The text to insert.
+         */
+        void insertAtCaret(const std::string& str);
+
+        /**
+         * Finds the last UTF-8 character boundary in the first
+         * @a maxBytes bytes of @a str.
+         *
+         * @param str UTF-8 string.
+         *
+         * @param maxBytes How many bytes to consider at the beginning
+         * of @a str.  Must not be negative.
+         *
+         * @return The position of the last character boundary.  Always
+         * <= @a maxBytes.
+         */
+        static int UTF8LastCharacterBoundary(const std::string &str, int maxBytes);
+
         std::string mText;
         int mCaretPosition;
         int mXScroll;
 		int mSelectStart;
+
+        /**
+         * The maximum length of #mText, in bytes.  Might be zero but
+         * never negative.
+         */
+        int mMaxLengthBytes;
     };
 }
 
diff --git a/guichan/widgets/textfield.cpp b/guichan/widgets/textfield.cpp
index 2510fd575..4ff8bbff7 100644
--- a/guichan/widgets/textfield.cpp
+++ b/guichan/widgets/textfield.cpp
@@ -56,6 +56,7 @@
  * For comments regarding functions please see the header file. 
  */
 
+#include <limits>
 #include "guichan/keyinput.h"
 #include "guichan/mouseinput.h"
 #include "guichan/widgets/textfield.h"
@@ -69,6 +70,7 @@ namespace gcn
         mCaretPosition = 0;
         mXScroll = 0;
         mSelectStart = 0;
+        mMaxLengthBytes = std::numeric_limits<int>::max();
 
         setFocusable(true);
 
@@ -83,8 +85,11 @@ namespace gcn
         mCaretPosition = 0;
         mXScroll = 0;
         mSelectStart = 0;
+        mMaxLengthBytes = std::numeric_limits<int>::max();
 
         mText = text;
+        // std::string::max_size() might exceed std::numeric_limits<int>::max().
+        truncateToMaxLength();
         adjustSize();
         setBorderSize(1);
         
@@ -96,14 +101,10 @@ namespace gcn
 
     void TextField::setText(const std::string& text)
     {
-        if ((int)text.size() < mCaretPosition )
-        {
-            mCaretPosition = text.size();
-        }
-    
+        mText = text;
+        truncateToMaxLength();
         mSelectStart = mCaretPosition;
-
-        mText = text;    
+        setDirty(true);
     }
   
     void TextField::draw(Graphics* graphics)
@@ -191,9 +192,10 @@ namespace gcn
         {
             std::string str;
             if (GetClipboard(str) >= 0) {
-                for (size_t i = 0; i < str.size(); ++i) {
-                    keyPress(Key(str[i]));
-                }
+                // GetClipboard ensures that the string does not
+                // contain control characters.
+                insertAtCaret(str);
+                fixScroll();
             }
         }
     }
@@ -319,36 +321,20 @@ namespace gcn
         {
             std::string str;
 
-            if (selLen > 0) {
-                mText.erase(selFirst, selLen);
-                mCaretPosition = selFirst;
-                mSelectStart = selFirst;
-            }
-
             if (GetClipboard(str) >= 0) {
-                for (size_t i = 0; i < str.size(); ++i) {
-                    keyPress(Key(str[i]));
-                }
+                // GetClipboard ensures that the string does not
+                // contain control characters.
+                insertAtCaret(str);
             }
 
+            // Even if GetClipboard failed, we did recognize the key
+            // and the caller should not treat it as a hot key.
             ret = true;
         }
 
         else if (key.isCharacter())
         {
-            if (selLen > 0) {
-                mText.erase(selFirst, selLen);
-                mCaretPosition = selFirst;
-                mSelectStart = selFirst;
-            }
-
-            mText.insert(mCaretPosition,key.toString());
-            int newpos = UTF8GetNext(mText, mCaretPosition);
-            if (newpos > (int)mText.size()) {
-                throw GCN_EXCEPTION("Invalid UTF8.");
-            }
-            mCaretPosition = newpos;
-            mSelectStart = newpos;
+            insertAtCaret(key.toString());
             ret = true;
         }
 
@@ -434,4 +420,131 @@ namespace gcn
     {
         fixScroll();
     }
+
+    int TextField::getMaxLengthBytes() const
+    {
+        return mMaxLengthBytes;
+    }
+
+    void TextField::setMaxLengthBytes(int maxLengthBytes)
+    {
+        if (maxLengthBytes < 0)
+        {
+            maxLengthBytes = 0;
+        }
+
+        mMaxLengthBytes = maxLengthBytes;
+        truncateToMaxLength();
+    }
+
+    void TextField::truncateToMaxLength()
+    {
+        bool changedSomething = false;
+
+        // Because we never let mMaxLengthBytes become negative,
+        // the following static_cast cannot wrap around.
+        if (mText.size() > static_cast<unsigned int>(mMaxLengthBytes))
+        {
+            int newLength = UTF8LastCharacterBoundary(mText, mMaxLengthBytes);
+            mText.resize(newLength);
+            changedSomething = true;
+        }
+
+        // Because of mMaxLengthBytes, the following static_casts
+        // cannot overflow.
+        if (mCaretPosition > static_cast<int>(mText.size()))
+        {
+            mCaretPosition = mText.size();
+            changedSomething = true;
+        }
+        if (mSelectStart > static_cast<int>(mText.size()))
+        {
+            mSelectStart = mText.size();
+            changedSomething = true;
+        }
+
+        if (changedSomething)
+        {
+            fixScroll();
+            setDirty(true);
+        }
+    }
+
+    int TextField::UTF8LastCharacterBoundary(const std::string &str,
+                                             int maxBytes)
+    {
+        if (maxBytes < 0)
+        {
+            // A bug in the caller.
+            maxBytes = 0;
+        }
+
+        // The following static_cast cannot wrap around because
+        // maxBytes cannot be negative at this point.
+        if (str.size() < static_cast<unsigned int>(maxBytes))
+        {
+            // The following static_cast cannot overflow because
+            // str.size() is less than int maxBytes.
+            maxBytes = static_cast<int>(str.size());
+        }
+
+        if (maxBytes == 0)
+        {
+            // There is no space for any character.
+            return 0;
+        }
+
+        // The following would be easy to implement as:
+        //   return UTF8GetPrev(str, maxBytes + 1);
+        // but maxBytes + 1 might overflow, so don't do that.
+
+        // Find the previous character boundary.
+        int prev = UTF8GetPrev(str, maxBytes);
+        // Assert(prev < maxBytes);
+
+        // If maxBytes already is at a character
+        // boundary, then that character will fit.
+        int next = UTF8GetNext(str, prev);
+        if (next == maxBytes)
+        {
+            return maxBytes;
+        }
+        else
+        {
+            return prev;
+        }
+    }
+
+    void TextField::insertAtCaret(const std::string& str)
+    {
+        int bytesFree = mMaxLengthBytes - mText.size();
+        if (bytesFree < 0)
+        {
+            bytesFree = 0;
+        }
+
+        int bytesToCopy;
+        if (str.size() > static_cast<unsigned int>(bytesFree))
+        {
+            bytesToCopy = UTF8LastCharacterBoundary(str, bytesFree);
+        }
+        else
+        {
+            bytesToCopy = static_cast<int>(str.size());
+        }
+
+        unsigned int selFirst;
+        unsigned int selLen;
+        getTextSelectionPositions(&selFirst, &selLen);
+        if (selLen > 0) {
+            mText.erase(selFirst, selLen);
+            mCaretPosition = selFirst;
+            mSelectStart = selFirst;
+        }
+
+        mText.insert(mCaretPosition, str, 0, bytesToCopy);
+        mCaretPosition += bytesToCopy;
+        mSelectStart = mCaretPosition;
+        setDirty(true);
+    }
 }
diff --git a/tolua/tolua.cpp b/tolua/tolua.cpp
index f39cf9490..399228124 100644
--- a/tolua/tolua.cpp
+++ b/tolua/tolua.cpp
@@ -1,6 +1,6 @@
 /*
 ** Lua binding: stratagus
-** Generated automatically by tolua++-1.0.92 on Sat Jul 16 22:41:54 2011.
+** Generated automatically by tolua++-1.0.93 on Sun Oct  2 03:06:13 2011.
 */
 
 #ifndef __cplusplus
@@ -9830,6 +9830,71 @@ static int tolua_stratagus_TextField_getText00(lua_State* tolua_S)
 }
 #endif //#ifndef TOLUA_DISABLE
 
+/* method: setMaxLengthBytes of class  TextField */
+#ifndef TOLUA_DISABLE_tolua_stratagus_TextField_setMaxLengthBytes00
+static int tolua_stratagus_TextField_setMaxLengthBytes00(lua_State* tolua_S)
+{
+#ifndef TOLUA_RELEASE
+ tolua_Error tolua_err;
+ if (
+     !tolua_isusertype(tolua_S,1,"TextField",0,&tolua_err) ||
+     !tolua_isnumber(tolua_S,2,0,&tolua_err) ||
+     !tolua_isnoobj(tolua_S,3,&tolua_err)
+ )
+  goto tolua_lerror;
+ else
+#endif
+ {
+  TextField* self = (TextField*)  tolua_tousertype(tolua_S,1,0);
+  int maxLengthBytes = ((int)  tolua_tonumber(tolua_S,2,0));
+#ifndef TOLUA_RELEASE
+  if (!self) tolua_error(tolua_S,"invalid 'self' in function 'setMaxLengthBytes'", NULL);
+#endif
+  {
+   self->setMaxLengthBytes(maxLengthBytes);
+  }
+ }
+ return 0;
+#ifndef TOLUA_RELEASE
+ tolua_lerror:
+ tolua_error(tolua_S,"#ferror in function 'setMaxLengthBytes'.",&tolua_err);
+ return 0;
+#endif
+}
+#endif //#ifndef TOLUA_DISABLE
+
+/* method: getMaxLengthBytes of class  TextField */
+#ifndef TOLUA_DISABLE_tolua_stratagus_TextField_getMaxLengthBytes00
+static int tolua_stratagus_TextField_getMaxLengthBytes00(lua_State* tolua_S)
+{
+#ifndef TOLUA_RELEASE
+ tolua_Error tolua_err;
+ if (
+     !tolua_isusertype(tolua_S,1,"const TextField",0,&tolua_err) ||
+     !tolua_isnoobj(tolua_S,2,&tolua_err)
+ )
+  goto tolua_lerror;
+ else
+#endif
+ {
+  const TextField* self = (const TextField*)  tolua_tousertype(tolua_S,1,0);
+#ifndef TOLUA_RELEASE
+  if (!self) tolua_error(tolua_S,"invalid 'self' in function 'getMaxLengthBytes'", NULL);
+#endif
+  {
+   int tolua_ret = (int)  self->getMaxLengthBytes();
+   tolua_pushnumber(tolua_S,(lua_Number)tolua_ret);
+  }
+ }
+ return 1;
+#ifndef TOLUA_RELEASE
+ tolua_lerror:
+ tolua_error(tolua_S,"#ferror in function 'getMaxLengthBytes'.",&tolua_err);
+ return 0;
+#endif
+}
+#endif //#ifndef TOLUA_DISABLE
+
 /* method: new of class  ListBoxWidget */
 #ifndef TOLUA_DISABLE_tolua_stratagus_ListBoxWidget_new00
 static int tolua_stratagus_ListBoxWidget_new00(lua_State* tolua_S)
@@ -20179,6 +20244,8 @@ TOLUA_API int tolua_stratagus_open (lua_State* tolua_S)
    tolua_function(tolua_S,".call",tolua_stratagus_TextField_new00_local);
    tolua_function(tolua_S,"setText",tolua_stratagus_TextField_setText00);
    tolua_function(tolua_S,"getText",tolua_stratagus_TextField_getText00);
+   tolua_function(tolua_S,"setMaxLengthBytes",tolua_stratagus_TextField_setMaxLengthBytes00);
+   tolua_function(tolua_S,"getMaxLengthBytes",tolua_stratagus_TextField_getMaxLengthBytes00);
   tolua_endmodule(tolua_S);
   tolua_cclass(tolua_S,"ListBox","ListBox","Widget",NULL);
   tolua_beginmodule(tolua_S,"ListBox");
diff --git a/tolua/ui.pkg b/tolua/ui.pkg
index 34d449347..63f99a4f6 100644
--- a/tolua/ui.pkg
+++ b/tolua/ui.pkg
@@ -442,6 +442,8 @@ class TextField : public Widget
 	TextField(const std::string text);
 	virtual void setText(const std::string text);
 	virtual std::string &getText();
+	void setMaxLengthBytes(int maxLengthBytes);
+	int getMaxLengthBytes() const;
 };
 
 class ListBox : public Widget