Generalized.
diff --git a/Lib/lib-stdwin/Sliders.py b/Lib/lib-stdwin/Sliders.py
index 8953efd..ca67f79 100644
--- a/Lib/lib-stdwin/Sliders.py
+++ b/Lib/lib-stdwin/Sliders.py
@@ -1,13 +1,13 @@
 # Module 'Sliders'
 #
-# Sliders are somewhat like buttons but have an extra hook that is
-# called whenever their value is changed.
+# XXX Should split caller interface, appearance and reactivity better
+
 
 import stdwin
 from stdwinevents import *
 import rect
 from minmax import min, max
-from Buttons import ClassicButton
+from Buttons import *
 
 
 # Field indices in event detail
@@ -22,37 +22,7 @@
 # It looks like a button but dragging the mouse left or right
 # changes the controlled value.
 #
-class DragSlider() = ClassicButton():
-	#
-	# INVARIANTS maintained by the define and setval methods:
-	#
-	#	self.min <= self.val <= self.max
-	#	self.text = `self.val`
-	#
-	# (Notice that unlike in Python ranges, the end point belongs
-	# to the range.)
-	#
-	def define(self, (win, bounds)):
-		self.min = 0
-		self.val = 50
-		self.max = 100
-		self.setval_hook = 0
-		self.pretext = self.postext = ''
-		self.text = self.pretext + `self.val` + self.postext
-		self = ClassicButton.define(self, (win, bounds, self.text))
-		return self
-	#
-	def setval(self, val):
-		val = min(self.max, max(self.min, val))
-		if val <> self.val:
-			self.val = val
-			self.text = self.pretext + `self.val` + self.postext
-			if self.setval_hook:
-				self.setval_hook(self)
-			self.redraw()
-	#
-	def settext(self, text):
-		pass # shouldn't be called at all
+class DragSliderReactivity() = NoReactivity():
 	#
 	def mouse_down(self, detail):
 		h, v = hv = detail[_HV]
@@ -72,3 +42,137 @@
 			self.setval(self.oldval + (h - self.anchor))
 			self.active = 0
 	#
+
+class DragSliderAppearance() = ButtonAppearance():
+	#
+	def define(self, (win, bounds)):
+		self.min = 0
+		self.val = -1 # Changed by next setval call
+		self.max = 100
+		self.setval_hook = 0
+		self.pretext = self.postext = ''
+		self = ClassicButton.define(self, (win, bounds, ''))
+		self.setval(50)
+		return self
+	#
+	# INVARIANTS maintained by the setval method:
+	#
+	#	self.min <= self.val <= self.max
+	#	self.text = self.pretext + `self.val` + self.postext
+	#
+	# (Notice that unlike in Python ranges, the end point belongs
+	# to the range.)
+	#
+	def setval(self, val):
+		val = min(self.max, max(self.min, val))
+		if val <> self.val:
+			self.val = val
+			self.setval_trigger()
+			# (The trigger may change val, pretext and postext)
+			self.settext(self.pretext + `self.val` + self.postext)
+	#
+	def setval_trigger(self):
+		if self.setval_hook:
+			self.setval_hook(self)
+	#
+
+class DragSlider() = DragSliderReactivity(), DragSliderAppearance(): pass
+
+
+# Auxiliary class for DragSlider incorporated in ComplexSlider
+#
+class _SubDragSlider() = DragSlider():
+	def define(self, (win, bounds, parent)):
+		self.parent = parent
+		return DragSlider.define(self, (win, bounds))
+	def setval_trigger(self):
+		self.parent.val = self.val
+		self.parent.setval_trigger()
+
+# Auxiliary class for ClassicButton incorporated in ComplexSlider
+#
+class _SubClassicButton() = ClassicButton():
+	def define(self, (win, bounds, text, step, parent)):
+		self.parent = parent
+		self.step = step
+		return ClassicButton.define(self, (win, bounds, text))
+	def down_trigger(self):
+		self.parent.setval(self.parent.val + self.step)
+		self.delay = 5
+		self.win.settimer(self.delay)
+	def move_trigger(self):
+		self.win.settimer(self.delay)
+	def timer_trigger(self):
+		self.delay = 1
+		self.parent.setval(self.parent.val + self.step)
+		self.win.settimer(self.delay)
+
+# A complex slider is a wrapper around three buttons:
+# One to step down, a dragslider, and one to step up.
+#
+class ComplexSlider() = LabelAppearance(), NoReactivity():
+	#
+	def define(self, (win, bounds)):
+		#
+		self.win = win
+		self.bounds = bounds
+		self.setval_hook = 0
+		#
+		(left, top), (right, bottom) = bounds
+		size = bottom - top
+		#
+		downbox = (left, top), (left+size, bottom)
+		sliderbox = (left+size, top), (right-size, bottom)
+		upbox = (right-size, top), (right, bottom)
+		#
+		self.downbutton = \
+			_SubClassicButton().define(win, downbox, '-', -1, self)
+		#
+		self.sliderbutton = \
+			_SubDragSlider().define(win, sliderbox, self)
+		#
+		self.upbutton = \
+			_SubClassicButton().define(win, upbox, '+', 1, self)
+		#
+		self.min = self.sliderbutton.min
+		self.val = self.sliderbutton.val
+		self.max = self.sliderbutton.max
+		self.pretext = self.sliderbutton.pretext
+		self.postext = self.sliderbutton.postext
+		#
+		self.children = \
+			[self.downbutton, self.sliderbutton, self.upbutton]
+		#
+		return self
+	#
+	def mouse_down(self, detail):
+		for b in self.children:
+			b.mouse_down(detail)
+	#
+	def mouse_move(self, detail):
+		for b in self.children:
+			b.mouse_move(detail)
+	#
+	def mouse_up(self, detail):
+		for b in self.children:
+			b.mouse_up(detail)
+	#
+	def timer(self):
+		for b in self.children:
+			b.timer()
+	#
+	def draw(self, area):
+		for b in self.children:
+			b.draw(area)
+	#
+	def setval(self, val):
+		self.sliderbutton.min = self.min
+		self.sliderbutton.max = self.max
+		self.sliderbutton.pretext = self.pretext
+		self.sliderbutton.postext = self.postext
+		self.sliderbutton.setval(val)
+	#
+	def setval_trigger(self):
+		if self.setval_hook:
+			self.setval_hook(self)
+	#