Index: trac/ticket/api.py
===================================================================
--- trac/ticket/api.py	(revision 2983)
+++ trac/ticket/api.py	(working copy)
@@ -47,6 +47,12 @@
     def get_custom_fields():
         """ Return an iterable of trac.ticket.field.Field objects. """
 
+class ITicketFieldTypeProvider(Interface):
+    """ Provide custom ticket field types. """
+    def get_ticket_field_types():
+        """ Return list of (name, type) tuples, where type is a
+            subclass of trac.ticket.field.Field. """
+
 class ITicketWorkflow(Interface):
     """ This interface controls what actions can be performed on a ticket. """
 
@@ -195,15 +201,16 @@
         pass
 
 class TicketSystem(Component):
-    implements(IPermissionRequestor, IWikiSyntaxProvider, ISearchSource)
+    implements(IPermissionRequestor, IWikiSyntaxProvider, ISearchSource,
+               ITicketFieldTypeProvider)
 
-    ticket_workflow = ExtensionPoint(ITicketWorkflow)
+    field_type_providers = ExtensionPoint(ITicketFieldTypeProvider)
     field_providers = ExtensionPoint(ITicketFieldProvider)
     workflow = SingletonExtensionPoint(ITicketWorkflow, 'ticket', 'workflow',
                                        DefaultTicketWorkflow)
 
     # Public API
-    def _add_enum_field(self, db, fields, cls, **kwargs):
+    def _add_enum_field(self, db, fields, cls, type=Select, **kwargs):
         # Insert a field representing one of the AbstractEnum model types.
         options = [val.name for val in cls.select(self.env, db=db)]
         if not options:
@@ -211,7 +218,7 @@
             # exist
             return
         name = cls.__name__.lower()
-        field = Select(name, label=name.title(),
+        field = type(name, label=name.title(),
                        value=self.config.get('ticket', 'default_' + name),
                        options=options,
                        **kwargs)
@@ -225,7 +232,7 @@
         fields = []
 
         # Insert fields in order they will appear
-        self._add_enum_field(db, fields, model.Status, type='radio',
+        self._add_enum_field(db, fields, model.Status, type=Radio,
                              hidden=True)
         fields.append(Text('summary', label='Summary', fullrow=True))
         self._add_enum_field(db, fields, model.Type, fullrow=True)
@@ -236,7 +243,7 @@
         self._add_enum_field(db, fields, model.Component)
         self._add_enum_field(db, fields, model.Version, optional=True)
         self._add_enum_field(db, fields, model.Severity)
-        self._add_enum_field(db, fields, model.Resolution, type='radio')
+        self._add_enum_field(db, fields, model.Resolution, type=Radio)
         fields.append(Text('keywords', label='Keywords'))
 
         # Owner field, can be text or drop-down depending on configuration
@@ -270,11 +277,22 @@
 
     def get_custom_fields(self):
         fields = []
+
+        field_types = {}
+        for provider in self.field_type_providers:
+            for name, field_type in provider.get_ticket_field_types():
+                field_types[name] = field_type
+            
         for name in [option for option, value
                      in self.config.options('ticket-custom')
                      if '.' not in option]:
-            field = Field(name,
-                type=self.config.get('ticket-custom', name),
+            field_type = self.config.get('ticket-custom', name)
+
+            if not field_type in field_types:
+                raise TracError("Custom field has invalid type '%s'" % field_type)
+
+            field_type = field_types[field_type]
+            field = field_type(name,
                 order=int(self.config.get('ticket-custom', name + '.order', '0')),
                 label=self.config.get('ticket-custom', name + '.label', name),
                 value=self.config.get('ticket-custom', name + '.value', ''),
@@ -298,8 +316,15 @@
         fields.sort(lambda x, y: cmp(x['order'], y['order']))
         return fields
 
+    # ITicketFieldTypeProvider methods
+    def get_ticket_field_types(self):
+        yield ('text', Text)
+        yield ('checkbox', CheckBox)
+        yield ('textarea', TextArea)
+        yield ('radio', Radio)
+        yield ('select', Select)
+
     # IPermissionRequestor methods
-
     def get_permission_actions(self):
         return ['TICKET_APPEND', 'TICKET_CREATE', 'TICKET_CHGPROP',
                 'TICKET_VIEW',  
Index: trac/ticket/web_ui.py
===================================================================
--- trac/ticket/web_ui.py	(revision 2964)
+++ trac/ticket/web_ui.py	(working copy)
@@ -26,6 +26,7 @@
 from trac.ticket.notification import TicketNotifyEmail
 from trac.Timeline import ITimelineEventProvider
 from trac.web import IRequestHandler
+from trac.web.clearsilver import HDFWrapper
 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
 from trac.wiki import wiki_to_html, wiki_to_oneliner
 from trac.ticket.field import *
@@ -42,18 +43,17 @@
                 options.append(value)
             field['options'] = options
 
+        hdf = HDFWrapper(loadpaths=[env.get_templates_dir(),
+                         env.config.get('trac', 'templates_dir')])
+        field['value'] = value
+        hdf['field'] = field
+
         # Wiki format labels and values if appropriate
         if 'html_value' not in field:
-            if value and not field['plaintext']:
-                if field['type'] == 'textarea':
-                    field['html_value'] = wiki_to_html(value, env, req, db)
-                else:
-                    field['html_value'] = wiki_to_oneliner(value, env, db)
-            else:
-                field['html_value'] = value
+            field['html_value'] = hdf.render(field.render_view(req, hdf))
+        if 'html_control' not in field:
+            field['html_control'] = hdf.render(field.render_control(req, hdf))
 
-        field['value'] = value
-
         if not field['plaintext']:
             field['label'] = wiki_to_oneliner(field['label'], env, db)
     return fields
Index: trac/ticket/field.py
===================================================================
--- trac/ticket/field.py	(revision 2963)
+++ trac/ticket/field.py	(working copy)
@@ -61,16 +61,19 @@
             'type': type,
         })
 
+    def render_view(self, req, hdf):
+        return 'field_view.cs'
+
+    def render_control(self, req, hdf):
+        return 'field_control.cs'
+
 class Text(Field):
     def __init__(self, name, size=None, **kwargs):
         Field.__init__(self, name, **kwargs)
         if size is not None:
             self['size'] = size
 
-class CheckBox(Field):
-    def __init__(self, name, options=[], **kwargs):
-        Field.__init__(self, name, **kwargs)
-        self['options'] = options
+class CheckBox(Field): pass
 
 class TextArea(Field):
     def __init__(self, name, width=None, height=None, **kwargs):
Index: templates/field_view.cs
===================================================================
--- templates/field_view.cs	(revision 0)
+++ templates/field_view.cs	(revision 0)
@@ -0,0 +1 @@
+<?cs var:field.value ?>
Index: templates/field_control.cs
===================================================================
--- templates/field_control.cs	(revision 0)
+++ templates/field_control.cs	(revision 0)
@@ -0,0 +1,2 @@
+<?cs include "macros.cs" ?>
+<?cs call:form_control(field, 1) ?>
