Michaël Van Canneyt 6 месяцев назад
Родитель
Сommit
dbf87945ae
4 измененных файлов с 1008 добавлено и 3 удалено
  1. 119 0
      src/base/fresnel.cursortimer.pp
  2. 878 0
      src/base/fresnel.edit.pp
  3. 8 0
      src/base/fresnelbase.lpk
  4. 3 3
      src/base/fresnelbase.pas

+ 119 - 0
src/base/fresnel.cursortimer.pp

@@ -0,0 +1,119 @@
+unit Fresnel.CursorTimer;
+
+{$mode objfpc}{$H+}
+{$Interfaces CORBA}
+
+interface
+
+uses
+  Classes, SysUtils, fptimer;
+
+type
+  IBlinkControl = Interface
+    procedure blink(aVisible : boolean);
+  end;
+
+  { TCursorTimer }
+
+  TCursorTimer = class
+  private
+    FBlinkControl: IBlinkControl;
+    FBlink : Boolean;
+    FBlinkRate: Word;
+    FTimer : TFPTimer;
+    class var _Instance: TCursorTimer;
+    procedure HandleBlink(Sender: TObject);
+    procedure SetBlinkControl(const aValue: IBlinkControl);
+    procedure SetBlinkRate(const aValue: Word);
+  protected
+    procedure DisableTimer;
+    procedure EnableTimer;
+    Procedure Blink;
+  Public
+    class constructor Init;
+    class destructor Done;
+    constructor create; virtual;
+    Property BlinkControl : IBlinkControl Read FBlinkControl Write SetBlinkControl;
+    Property BlinkRate : Word Read FBlinkRate Write SetBlinkRate;
+    Class property Instance : TCursorTimer Read _Instance;
+  end;
+
+function CursorTimer : TCursorTimer;
+
+implementation
+
+function CursorTimer: TCursorTimer;
+begin
+  Result:=TCursorTimer.Instance;
+end;
+
+{ TCursorTimer }
+
+procedure TCursorTimer.SetBlinkControl(const aValue: IBlinkControl);
+begin
+  if FBlinkControl=aValue then Exit;
+  FBlink:=False;
+  Blink;
+  DisableTimer;
+  FBlinkControl:=aValue;
+  if assigned(FBlinkControl) then
+    EnableTimer;
+end;
+
+procedure TCursorTimer.HandleBlink(Sender: TObject);
+begin
+  Blink;
+end;
+
+procedure TCursorTimer.SetBlinkRate(const aValue: Word);
+begin
+  if FBlinkRate=aValue then Exit;
+  FBlinkRate:=aValue;
+  // Safety
+  if FBlinkRate<30 then
+    FBlinkRate:=30;
+  if Assigned(FTimer) then
+    FTimer.Interval:=FBlinkRate;
+end;
+
+procedure TCursorTimer.DisableTimer;
+begin
+  if assigned(FTimer) then
+    FTimer.Enabled:=False
+end;
+
+procedure TCursorTimer.EnableTimer;
+begin
+  if not Assigned(FTimer) then
+    begin
+    FTimer:=TFPTimer.Create(Nil);
+    FTimer.Interval:=FBlinkRate;
+    FTimer.OnTimer:=@HandleBlink;
+    end;
+  FTimer.Enabled:=True;
+end;
+
+procedure TCursorTimer.Blink;
+begin
+  if Assigned(FBlinkControl) then
+    FBlinkControl.Blink(FBlink);
+  FBlink:=Not FBlink;
+end;
+
+class constructor TCursorTimer.Init;
+begin
+  _Instance:=TCursorTimer.Create;
+end;
+
+class destructor TCursorTimer.Done;
+begin
+  FreeAndNil(_instance);
+end;
+
+constructor TCursorTimer.create;
+begin
+  FBlinkRate:=500;
+end;
+
+end.
+

+ 878 - 0
src/base/fresnel.edit.pp

@@ -0,0 +1,878 @@
+unit fresnel.edit;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, fpImage, fpCSSResParser, fpCSSTree, fresnel.CursorTimer,
+  Fresnel.TextLayouter, Fresnel.Classes, Fresnel.Dom, Fresnel.Keys, Fresnel.Controls,
+  FCL.events, Fresnel.Events, Utf8Utils;
+
+
+Type
+
+  { TEdit }
+
+  TEdit = Class(TReplacedElement, IBlinkControl)
+  const
+    NotWordChars = [#0..'/', ':'..'@', '['..'^', '`', '{'..'~'];
+    CursorWidth = 2;
+  private
+    class var _FresnelEditTypeID : TCSSNumericalID;
+    class constructor Init;
+    class function IsSpecialChar(aUnicodeChar: string): boolean;
+  private
+    FEnabled: Boolean;
+    FMaxLength: Integer;
+    FReadOnly: Boolean;
+    // Selection start, stop index - 0 based.
+    FSelectionEnd: Integer;
+    FSelectionStart: Integer;
+    // Value
+    FValue: TFresnelCaption;
+    // Text to show if value is empty
+    FPlaceHolder : TFresnelCaption;
+    // Actually visible text
+    FVisibleText : TFresnelCaption;
+    // Selection start/end position in pixels
+    FSelectionStartX  : TFresnelLength;
+    FSelectionEndX  : TFresnelLength;
+    // Cursor position in pixels
+    FCursorX : TFresnelLength;
+    // Cursor position, characters, 0 based
+    FCursorPos : Integer;
+    // Start drawing text at
+    FDrawOffset  : TFresnelLength;
+    // Visible text offset in pixels.
+    FTextOffset : TFresnelLength;
+    // Should we draw the cursor ?
+    FCursorVisible : Boolean;
+    // Do we need to recalculate the text parameters ?
+    FRecalcParams: Boolean;
+    procedure DoDeleteSelection; virtual;
+    function GetDrawText: TFresnelCaption;
+    function GetSelectionText: String;
+    function NextWordEndOffset: Integer;
+    function PrevWordStartOffset: Integer;
+    procedure SetEnabled(const aValue: Boolean);
+    procedure SetMaxLength(const aValue: Integer);
+    procedure SetPlaceHolder(const aValue: TFresnelCaption);
+    procedure SetReadOnly(const aValue: Boolean);
+    procedure SetSelectionEnd(const aValue: Integer);
+    procedure SetSelectionStart(const aValue: Integer);
+    Procedure NormalizeSelection;
+  protected
+    // char index -> X pos
+    function CalcXOffset(aCharPos: Integer; aUseOffset: Boolean=true; aUseDrawText : Boolean = False): TFresnelLength;
+    // X pos -> char index
+    function CalcCharOffset(aXOffset: TFresnelLength): Integer;
+    //
+    // Recalculate index etc.
+    //
+    // Indicate that params have changed: Size, cursor or selection pos.
+    procedure EditParamsChanged;
+    // If params need to be recalculated, then recalculate
+    procedure MaybeRecalcParams;
+    // Calculate offsets: cursor pos, text offset
+    procedure CalcOffsets; virtual;
+    // Calc selection
+    procedure CalcTextDrawInfo; virtual;
+    // Calculate character size.
+    class function CalcCharSize(aFont: IFresnelFont; aUnicodeChar: String): TFresnelLength;
+    // blink cursor
+    procedure Blink(aVisible : Boolean); virtual;
+    function DoDispatchEvent(aEvent: TAbstractEvent): Integer; override;
+    procedure HandleFocusChange(GotFocus: Boolean);
+    procedure HandleKeyDown(aEvent: TFresnelKeyEvent); virtual;
+    procedure HandleMouseDown(aEvent : TFresnelMouseEvent); virtual;
+    procedure HandleMouseMove(aEvent: TFresnelMouseEvent); virtual;
+    procedure HandleMouseUp(aEvent : TFresnelMouseEvent); virtual;
+    procedure SetValue(const aValue: TFresnelCaption); virtual;
+    procedure SetName(const aNewName: TComponentName); override;
+    procedure DoRender(aRenderer: IFresnelRenderer); override;
+  Public
+    class function HandleFocus: Boolean; override;
+    class function CSSTypeID: TCSSNumericalID; override;
+    class function CSSTypeName: TCSSString; override;
+    class function GetCSSTypeStyle: TCSSString; override;
+  public
+    constructor Create(aOwner : TComponent); override;
+    function GetIntrinsicContentSize(aMode: TFresnelLayoutMode; aMaxWidth: TFresnelLength; aMaxHeight: TFresnelLength): TFresnelPoint; override;
+    function CanFocus: Boolean; override;
+    procedure DeleteSelection;
+    procedure SetSelection(const aStart,aEnd : Integer);
+    property Value: TFresnelCaption read FValue write SetValue;
+    property ReadOnly : Boolean Read FReadOnly Write SetReadOnly;
+    property Enabled : Boolean Read FEnabled Write SetEnabled;
+    property PlaceHolder : TFresnelCaption Read FPlaceHolder Write SetPlaceHolder;
+    // Cursor position, 0 based, in characters (codepoints)
+    Property Curs : Integer Read FSelectionStart Write SetSelectionStart;
+    // SelectionStart, 0 based, in characters (codepoints)
+    Property SelectionStart : Integer Read FSelectionStart Write SetSelectionStart;
+    // SelectionEnd, 0 based, in characters (codepoints)
+    Property SelectionEnd : Integer Read FSelectionEnd Write SetSelectionEnd;
+    // Max length
+    Property MaxLength : Integer Read FMaxLength Write SetMaxLength;
+    // Get selection
+    Property SelectionText : String Read GetSelectionText;
+  end;
+
+
+implementation
+
+uses math;
+
+function TEdit.GetIntrinsicContentSize(aMode: TFresnelLayoutMode; aMaxWidth: TFresnelLength;
+  aMaxHeight: TFresnelLength): TFresnelPoint;
+
+var
+  lFont : IFresnelFont;
+  lSize : TFresnelPoint;
+  lVertPadding,lHorzPadding : TFresnelLength;
+begin
+  lFont:=GetFont;
+  // todo writing-mode
+  if IsNan(aMaxHeight) then ;
+  lHorzPadding:=GetComputedLength(fcaPaddingLeft)+GetComputedLength(fcaPaddingRight);
+  lVertPadding:=GetComputedLength(fcaPaddingTop)+GetComputedLength(fcaPaddingBottom);
+  Lsize:=LFont.TextSize('W');
+  case aMode of
+    flmMaxHeight,
+    flmMinHeight:
+      begin
+      LSize.Offset(lHorzPadding,lVertPadding);
+      Result:=LSize;
+      end;
+    flmMinWidth,
+    flmMaxWidth:
+      begin
+      if MaxLength<>0 then
+        lSize.X:=lSize.X*MaxLength
+      else
+        LSize.X:=aMaxWidth;
+      LSize.Offset(lHorzPadding,lVertPadding);
+      Result:=lSize;
+      end;
+  end;
+end;
+
+procedure TEdit.DeleteSelection;
+
+var
+  lPrev : TFresnelCaption;
+
+begin
+  if ReadOnly or (FSelectionStart = FselectionEnd) then
+    Exit;
+  lPrev:=FValue;
+  DoDeleteSelection;
+  if (lPrev <> FValue) then
+    EventDispatcher.DispatchEvent(evtChange);
+end;
+
+procedure TEdit.DoDeleteSelection;
+
+begin
+  if FSelectionStart=FSelectionEnd then
+    exit;
+  if FSelectionStart > FSelectionEnd then
+    begin
+    Delete(FValue,1 + FSelectionEnd, FSelectionStart-FSelectionEnd);
+    FSelectionStart:=FSelectionEnd;
+    end
+  else
+    begin
+    Delete(FValue,1 + FSelectionStart, FSelectionEnd-FSelectionStart);
+    FSelectionEnd:=FSelectionStart;
+    end;
+  EditParamsChanged;
+end;
+
+function TEdit.PrevWordStartOffset : Integer;
+begin
+  Result:=FSelectionEnd;
+  Result:=Result-1;
+  // word search. not_word all have ord <128, so we can use bytewise offset
+  while (Result>0) and (FValue[Result+1] in NotWordChars) do
+    Dec(Result);
+  while (Result>0) and not (FValue[Result+1] in NotWordChars) do
+    Dec(Result);
+end;
+
+procedure TEdit.SetEnabled(const aValue: Boolean);
+begin
+  if FEnabled=aValue then Exit;
+  FEnabled:=aValue;
+  DomChanged;
+end;
+
+procedure TEdit.SetMaxLength(const aValue: Integer);
+begin
+  if FMaxLength=aValue then Exit;
+  FMaxLength:=aValue;
+  DomChanged; // Min length may change
+end;
+
+function TEdit.NextWordEndOffset : Integer;
+// Offset in bytes
+var
+  lLen : Integer;
+begin
+  // word search. not_word all have ord <128 so we can use bytewise offset
+  lLen:=Length(FValue);
+  Result:=FSelectionEnd;
+  while (Result<lLen) and (FValue[Result+1] in NotWordChars) do
+    Inc(Result);
+  while (Result<lLen) and not (FValue[Result+1] in NotWordChars) do
+    Inc(Result);
+end;
+
+procedure TEdit.HandleKeyDown(aEvent: TFresnelKeyEvent);
+
+var
+  lKeyUTF8 : String;
+  lKeyLen : Integer;
+  lChanged : Boolean;
+  lOffset : Integer;
+
+begin
+  lChanged:=False;
+  // Printable character has numkey >0
+  if (aEvent.NumKey>0) and not (aEvent.CtrlKey) then
+    begin
+    if ReadOnly then
+      exit;
+    DoDeleteSelection;
+    lKeyUTF8:=UnicodeToUTF8(aEvent.NumKey);
+    Insert(lKeyUTF8,FValue,FCursorPos+1);
+    lKeyLen:=Length(lKeyUTF8);
+    if FCursorPos<=FSelectionEnd then
+      Inc(FSelectionEnd,lKeyLen);
+    if FCursorPos<=FSelectionStart then
+      Inc(FSelectionStart,lKeyLen);
+    Inc(FCursorPos,lKeyLen);
+    EventDispatcher.DispatchEvent(evtChange);
+    lChanged:=True;
+    end
+  else
+    begin
+    lChanged:=True;
+    Case aEvent.NumKey of
+      TKeyCodes.BackSpace:
+        begin
+        if FSelectionStart=FSelectionEnd then
+          begin
+          lOffset:=UTF8FindNearestCharStart(PChar(FValue),Length(FValue),FCursorPos);
+          lKeyLen:=UTF8CodepointSize(@FValue[lOffset+1]);
+          Delete(FValue,lOffSet+1,lKeyLen);
+          FCursorPos:=lOffSet;
+          FSelectionStart:=FCursorPos;
+          FSelectionEnd:=FCursorPos;
+          end
+        else
+          begin
+          lKeyLen:=Abs(FSelectionStart-FSelectionEnd);
+          DoDeleteSelection;
+          FSelectionEnd:=FSelectionStart;
+          FCursorPos:=FSelectionStart;
+          end;
+        end;
+      TKeyCodes.Delete:
+        begin
+        if FSelectionStart=FSelectionEnd then
+          begin
+          lKeyLen:=UTF8CodepointSize(@FValue[FCursorPos+1]);
+          Delete(FValue,FCursorPos+1,lKeyLen);
+          end
+        else
+          begin
+          lKeyLen:=Abs(FSelectionStart-FSelectionEnd);
+          DoDeleteSelection;
+          if FSelectionStart<FSelectionEnd then
+            FSelectionEnd:=FSelectionStart
+          else
+            FSelectionStart:=FSelectionEnd;
+          end;
+        FCursorPos:=FSelectionEnd;
+        end;
+      TKeyCodes.ArrowLeft :
+        begin
+        if FCursorPos>0 then
+          begin
+          if aEvent.CtrlKey then
+            lOffset:=PrevWordStartOffset-FCursorPos
+          else
+            lOffset:=-1; // Todo: calc length of codepoint in bytes
+          FCursorPos:=FCursorPos+lOffset;
+          FSelectionEnd:=FCursorPos;
+          if not aEvent.ShiftKey then
+            FSelectionStart:=FSelectionEnd;
+          end;
+        end;
+      TKeyCodes.ArrowRight :
+        begin
+        if FCursorPos<Length(FValue) then
+          begin
+          if aEvent.CtrlKey then
+            lOffset:=NextWordEndOffset-FCursorPos
+          else
+            lOffset:=1;
+          Inc(FCursorPos,lOffset);
+          FSelectionEnd:=FCursorPos;
+          if not aEvent.ShiftKey then
+            FSelectionStart:=FSelectionEnd;
+          end
+        end;
+      TKeyCodes.Home:
+        begin
+        FCursorPos:=0;
+        FSelectionEnd:=0;
+        if not aEvent.ShiftKey then
+          FSelectionStart:=0;
+        end;
+      TKeyCodes.End_:
+        begin
+        FCursorPos:=Length(FValue);
+        FSelectionEnd:=FCursorPos;
+        if not aEvent.ShiftKey then
+          FSelectionStart:=FCursorPos;
+        end;
+    else
+      lChanged:=False;
+    end;
+    end;
+  if lChanged then
+    EditParamsChanged;
+end;
+
+procedure TEdit.HandleMouseMove(aEvent : TFresnelMouseEvent);
+var
+  lNewPos : integer;
+begin
+  if not (mbMain in aEvent.Buttons) then
+    exit;
+  lNewPos:=CalcCharOffset(aEvent.ControlX);
+  if FCursorPos<>lNewPos then
+    begin
+    FCursorPos:=lNewPos;
+    FSelectionEnd:=FCursorPos;
+    EditParamsChanged;
+    end;
+end;
+
+procedure TEdit.HandleMouseUp(aEvent: TFresnelMouseEvent);
+begin
+  // Nothing for the moment;
+  // When popup menus are handled, then here we should show the popup if it was the right button.
+end;
+
+procedure TEdit.HandleMouseDown(aEvent: TFresnelMouseEvent);
+
+begin
+  MaybeRecalcParams;
+  FCursorPos:=CalcCharOffset(aEvent.ControlX);
+  if aEvent.ShiftKey then
+    begin
+    FSelectionEnd:=FCursorPos;
+    NormalizeSelection;
+    end
+  else
+    begin
+    FSelectionStart:=FCursorPos;
+    FSelectionEnd:=FCursorPos;
+    end;
+  EditParamsChanged;
+end;
+
+procedure TEdit.EditParamsChanged;
+
+begin
+  FRecalcParams:=True;
+  NormalizeSelection;
+  DomChanged;
+end;
+
+procedure TEdit.SetValue(const aValue: TFresnelCaption);
+begin
+  if FValue=aValue then Exit;
+  FValue:=aValue;
+  EditParamsChanged;
+end;
+
+procedure TEdit.SetName(const aNewName: TComponentName);
+var
+  ChangeValue: Boolean;
+begin
+  if Name=aNewName then exit;
+  ChangeValue :=
+    not (csLoading in ComponentState)
+    and (Name = Value)
+    and ((Owner = nil) or not (csLoading in Owner.ComponentState));
+  inherited SetName(aNewName);
+  if ChangeValue then
+    Value := aNewName;
+end;
+
+Function TEdit.GetDrawText : TFresnelCaption;
+
+begin
+  if FValue='' then
+    Result:=FPlaceHolder
+  else
+    Result:=FValue;
+end;
+
+function TEdit.GetSelectionText: String;
+begin
+  Result:=UTF8Copy(FValue,FSelectionStart,FSelectionEnd-FSelectionStart);
+end;
+
+procedure TEdit.Blink(aVisible: Boolean);
+begin
+  FCursorVisible:=IsFocused and aVisible;
+  DomChanged;
+end;
+
+class function TEdit.IsSpecialChar(aUnicodeChar : string) : boolean;
+
+begin
+  Result:=(aUnicodeChar=' ') or (aUnicodeChar='.');
+end;
+
+class function TEdit.CalcCharSize(aFont : IFresnelFont; aUnicodeChar: String) : TFresnelLength;
+
+begin
+  if IsSpecialChar(aUnicodeChar) then
+    Result:=aFont.TextSize('W'+aUnicodeChar+'W').x-aFont.TextSize('WW').X
+  else
+    Result:=aFont.TextSize(aUnicodeChar).x;
+end;
+
+procedure TEdit.CalcTextDrawInfo;
+
+// This combines various calculations in one.
+
+var
+  lUnicodeChar : string; // current character as UTF8 string
+  lCharNum : Integer; // Current character position (in codepoints)
+  lPos: integer;  // Current pos in string.
+  lPrevPos : integer;   // Previous pos in string.
+  lFirstVisibleIndex,lLastVisibleIndex: integer; // visible characters' start/end in utf-8 string, bytes
+  lText: string; // text to draw.
+  bestfx, bestlx: TFresnelLength;
+  lCharX: TFresnelLength;   // character X position relative to widget
+  lTotalWidth: TFresnelLength;    // total characters width, that becomes FCursorPx relative to the beginning of the text
+  lPreviousWidth: TFresnelLength;   // total width on the previous step
+  lVisibleStartX, lVisibleEndX: TFresnelLength;    // visible area start and end, pixels
+  lLeftSideMargin,lRightSideMargin : TFresnelLength;
+  lFont : IFresnelFont;
+
+begin
+  lFont:=GetFont;
+  lLeftSideMargin:=GetComputedLength(fcaPaddingLeft);
+  lRightSideMargin:=GetComputedLength(fcaPaddingRight);
+  lVisibleStartX  := lLeftSideMargin;
+  lVisibleEndX    := RenderedContentBox.Width - lRightSideMargin;
+  FSelectionStartX := lVisibleEndX; // because we stop the search
+  FSelectionEndX   := lVisibleEndX; // after last visible character is found
+  bestfx := -MaxInt + 1 + lVisibleStartX;
+  bestlx := MaxInt + 1 + lVisibleEndX;
+
+  lText := GetDrawText;
+  lUnicodeChar := '';
+  lTotalWidth := 0.0;
+  lPos  := 0;
+  lCharNum := 0;
+  FDrawOffset := 0;
+  while lPos <= Length(ltext) do
+  begin
+    lPrevPos := lPos;
+    lpos := UTF8CharAtBytePos(lText, lpos, lUnicodeChar);
+    lPreviousWidth := lTotalWidth;
+    lTotalWidth  := lTotalWidth + CalcCharSize(lFont, lUnicodeChar);
+    // Character position relative to edit margin. Text offset was calculated using cursor position.
+    lCharX := lTotalWidth - FTextOffset + lLeftSideMargin;
+
+    // Adjust selection coordinates
+    if lCharNum = FSelectionStart then
+      FSelectionStartX := lCharX;
+    if lCharNum = FSelectionEnd then
+      FSelectionEndX := lCharX;
+
+    // search for the first/last visible characters
+    if abs(lCharX - lVisibleStartX) < abs(bestfx - lVisibleStartX) then
+      begin
+      bestfx := lCharX;
+      lFirstVisibleIndex := lPrevPos;
+      FDrawOffset := lPreviousWidth;
+      end;
+    // in small edit field the same character can be both the first and the last, so no 'else' allowed
+    if abs(lCharX - lVisibleEndX) < abs(bestlx - lVisibleEndX) then
+      begin
+      bestlx := lCharX;
+      lLastVisibleIndex := UTF8CharAtBytePos(Ltext, lPos, lUnicodeChar); // plus one more character
+      end
+    else
+      begin
+      Writeln('Premature break at ',lCharNum);
+      break; // we can safely break after last visible character is found
+      end;
+    Inc(lCharNum);
+    end;
+
+  if FSelectionStartX < lVisibleStartX then
+    FSelectionStartX := lVisibleStartX;
+  if FSelectionEndX > lVisibleEndX then
+    FSelectionEndX := lVisibleEndX;
+
+  FVisibleText := Copy(lText, lFirstVisibleIndex, lLastVisibleIndex - lFirstVisibleIndex);
+  FDrawOffset := FTextOffset - FDrawOffset;
+  Write('Value : "',FValue,'", Visible : "',FVisibleText,'"');
+  Write(', Sel: [',FSelectionStart,' - ',FSelectionEnd,']');
+  Write(', SelX: [',FSelectionStartX,' - ',FSelectionEndX,']');
+  WriteLn(', Cur: ',FCursorPos,', Cur X:  [',FCursorX,']');
+
+end;
+
+Function TEdit.CalcCharOffset(aXOffset: TFresnelLength) : Integer;
+
+var
+  lText: string;
+  lChar: string;              // current character (UTF8)
+  lCharNum : Integer;         // Character index (in codepoints)
+  lClosestX: TFresnelLength;  // X position closest to cursor X)
+  lCurrWidth: TFresnelLength; // Running total character width
+  lPos: integer;  // Character loop pos
+  lFont : IFresnelFont;
+  lLeftSideMargin : TFresnelLength;
+  lCharWidth : TFresnelLength;
+begin
+  lLeftSideMargin:=GetComputedLength(fcaPaddingLeft);
+  lFont:=GetFont;
+  if aXOffset > 0 then
+    lClosestX := -MaxInt
+  else
+    lClosestX := +MaxInt;
+  lText := Value;
+  lChar := '';
+  lCurrWidth := 0;
+  lPos := 0;
+  lCharNum := 0;
+  // Correct for margin and text offset.
+  aXOffset:=aXOffset + FTextOffset - lLeftSideMargin;
+  while (lPos <= Length(lText)) do
+    begin
+    lPos:=UTF8CharAtBytePos(lText,lPos,lChar);
+    lCharWidth:=CalcCharSize(lFont,lChar);
+    lCurrWidth  := lCurrWidth + lCharWidth;
+    if abs(lCurrWidth - aXOffset) < abs(lClosestX - aXOffset) then
+      begin
+      // We're getting closer to the actual char.
+      lClosestX := lCurrWidth;
+      Result := lCharNum;
+      end
+    else
+      // We're moving away from the char, so break
+      break;
+    Inc(lCharNum);
+    end;
+end;
+
+Function TEdit.CalcXOffset(aCharPos: Integer; aUseOffset: Boolean = true; aUseDrawText : Boolean = False) : TFresnelLength;
+
+var
+  lText: string;
+  lChar: string;      // current character (UTF8)
+  lCharNum : Integer; // Character index (in codepoints)
+  lPos: integer;      // Character loop pos
+  lFont : IFresnelFont;
+  lCharWidth : TFresnelLength;
+
+begin
+  lFont := GetFont;
+  if aUseDrawText then
+    lText := GetDrawText
+  else
+    lText := Value;
+  lChar := '';
+  lPos := 0;
+  lCharNum := 0;
+  if aUseOffset then
+    Result:=GetComputedLength(fcaPaddingLeft)-FTextOffset
+  else
+    Result:=0;
+  While (lCharNum<=aCharPos) and (lPos<=Length(lText)) do
+    begin
+    lPos := UTF8CharAtBytePos(lText,lPos,lChar);
+    lCharWidth:=CalcCharSize(lFont,lChar);
+    Result:=Result+lCharWidth;
+    Inc(lCharNum);
+    end;
+end;
+
+procedure TEdit.CalcOffsets;
+{
+  FCursorPos -> FCursorX;
+  FCursorX -> FTextOffset
+}
+var
+  lVisibleWidth,
+  lLeftSideMargin,
+  lRightSideMargin,
+  lMaxWidth,
+  lCursorX: TFresnelLength;
+  r: TFresnelRect;
+
+begin
+  lLeftSideMargin:=GetComputedLength(fcaPaddingLeft);
+  lRightSideMargin:=GetComputedLength(fcaPaddingRight);
+  lCursorX:=CalcXOffset(FCursorPos,False,True);
+  r := RenderedContentBox;
+  lVisibleWidth := (r.Width - (lLeftSideMargin+lRightSideMargin));
+  lMaxWidth:=(lCursorX - (lVisibleWidth + CursorWidth) ); // 2 = Cursor width
+  if FTextOffset < lMaxWidth  then
+    FTextOffset := lMaxWidth
+  else if FTextOffset>lCursorX then
+    begin
+    FTextOffset := lCursorX;
+    if lCursorX <> 0 then
+      FTextOffset:=FTextOffset-CursorWidth;
+    end;
+  FCursorX := lCursorX - FTextOffset + lLeftSideMargin;
+end;
+
+
+
+procedure TEdit.SetPlaceHolder(const aValue: TFresnelCaption);
+begin
+  if FPlaceHolder=aValue then Exit;
+  FPlaceHolder:=aValue;
+  // only invalidate if the text is empty.
+  if (FValue='') then
+    DomChanged;
+end;
+
+procedure TEdit.SetReadOnly(const aValue: Boolean);
+begin
+  if FReadOnly=aValue then Exit;
+  FReadOnly:=aValue;
+  DomChanged; // CSS with Attribute selector can check for readonly
+end;
+
+procedure TEdit.SetSelectionEnd(const aValue: Integer);
+begin
+  if FSelectionEnd=aValue then Exit;
+  FSelectionEnd:=aValue;
+  if FSelectionEnd<0 then FSelectionEnd:=0;
+  if FSelectionEnd>Length(FValue) then FSelectionEnd:=Length(FValue);
+  EditParamsChanged;
+end;
+
+procedure TEdit.SetSelectionStart(const aValue: Integer);
+begin
+  if FSelectionStart=aValue then Exit;
+  if FSelectionStart<0 then FSelectionEnd:=0;
+  if FSelectionStart>Length(FValue) then FSelectionStart:=Length(FValue);
+  EditParamsChanged;
+end;
+
+procedure TEdit.NormalizeSelection;
+var
+  lTmp : Integer;
+begin
+  if FSelectionStart>FSelectionEnd then
+    begin
+    lTmp:=FSelectionStart;
+    FSelectionStart:=FSelectionEnd;
+    FSelectionEnd:=lTmp;
+    end;
+end;
+
+procedure TEdit.HandleFocusChange(GotFocus: Boolean);
+
+begin
+  if GotFocus then
+    CursorTimer.BlinkControl:=Self
+  else
+    CursorTimer.BlinkControl:=Nil;
+  DomChanged
+end;
+
+function TEdit.DoDispatchEvent(aEvent: TAbstractEvent): Integer;
+
+var
+  lFresnelEvt : TFresnelEvent absolute aEvent;
+  lKeyEvent : TFresnelKeyEvent absolute aEvent;
+  lMouseEvent : TFresnelMouseEvent absolute aEvent;
+
+begin
+  Result:=inherited DoDispatchEvent(aEvent);
+  if not (aEvent is TFresnelEvent) then
+    exit;
+  if lFresnelEvt.DefaultPrevented then
+    exit;
+  Case aEvent.EventID of
+   evtBlur,
+   evtFocus:
+     HandleFocusChange(aEvent.EventID=evtFocus);
+   evtKeyDown
+   {, evtKeyUp
+    , evtKeyPress
+   }:
+     if (aEvent is TFresnelKeyEvent) then
+       begin
+       if aEvent.EventID=evtKeyDown then
+         HandleKeyDown(lKeyEvent);
+       end;
+   evtMouseDown:
+     if (aEvent is TFresnelMouseEvent) then
+       HandleMouseDown(lMouseEvent);
+   evtMouseMove:
+     if (aEvent is TFresnelMouseEvent) then
+       HandleMouseMove(lMouseEvent);
+   evtMouseUp:
+     if (aEvent is TFresnelMouseEvent) then
+       HandleMouseUp(lMouseEvent);
+
+   else
+     // not handled here...
+   end;
+end;
+
+procedure TEdit.MaybeRecalcParams;
+
+begin
+  if not FRecalcParams then
+    exit;
+  CalcOffsets;
+  CalcTextDrawInfo;
+  FRecalcParams:=False;
+end;
+
+procedure TEdit.DoRender(aRenderer: IFresnelRenderer);
+
+var
+  lCaption : string;
+  lBackColor, lColorFP, lShadowColor: TFPColor;
+  lTopSideMargin, lLeftSideMargin, lRadius, lOffsetX, lOffsetY : TFresnelLength;
+  lHaveShadow : Boolean;
+  R,RSel : TFresnelRect;
+  SelWidth : TFresnelLength;
+
+  Procedure DrawCursor;
+  var
+    lCur : TFresnelRect;
+  begin
+    if not FCursorVisible then exit;
+    lCur:=RenderedContentBox;
+    lCur.Left:=lCur.Left+FCursorX; // CursorX has leftmargin
+    lCur.Right:=lCur.Left+CursorWidth;
+    aRenderer.FillRect(colBlack,lCur);
+  end;
+
+
+begin
+  // These will calculate FVisibleText etc.
+  MaybeRecalcParams;
+  // lRightSideMargin:=GetComputedLength(fcaPaddingRight);
+  lCaption:=FVisibleText;
+  if lCaption='' then
+    begin
+    DrawCursor;
+    exit;
+    end;
+  lLeftSideMargin:=GetComputedLength(fcaPaddingLeft);
+  lColorFP:=GetComputedColor(fcaColor,colTransparent);
+  lBackColor:=GetComputedColor(fcaBackgroundColor,colTransparent);
+  if lColorFP.Alpha=alphaTransparent then
+    exit;
+  R:=RenderedContentBox;
+  aRenderer.FillRect(lBackColor,R);
+  // Selection background.
+  RSel:=R;
+  SelWidth:=Abs(FSelectionStartX-FSelectionEndX);
+  RSel.Left:=RSel.Left+lLeftSideMargin+FSelectionStartX;
+  RSel.Right:=RSel.Left+SelWidth;
+  lBackColor:=fpimage.colDkBlue; // GetComputedColor(fcaSelectionBackGroundColor,colTransparent);
+  aRenderer.FillRect(fpimage.colDkBlue,RSel);
+  lHaveShadow:=GetComputedTextShadow(lOffsetX, lOffsetY, lRadius, lShadowColor);
+  if lHaveShadow then
+    aRenderer.AddTextShadow(lOffsetX,lOffsetY,lShadowColor,lRadius);
+  lLeftSideMargin:=GetComputedLength(fcaPaddingLeft);
+  lTopSideMargin:=GetComputedLength(fcaPaddingTop);
+  if FSelectionStart<>FSelectionEnd then
+    begin
+    if FSelectionStart>0 then
+      aRenderer.TextOut(R.Left+lLeftSideMargin,R.Top+lTopSideMargin,Font,lColorFP,Copy(lCaption,1,1+FSelectionStart));
+    aRenderer.TextOut(RSel.Left,RSel.Top+lTopSideMargin,Font,colWhite,Copy(lCaption,1+FSelectionStart,FSelectionEnd-FSelectionStart));
+    if FSelectionEnd<Length(lCaption) then
+      aRenderer.TextOut(RSel.Right,Rsel.Top+lTopSideMargin,Font,lColorFP,Copy(lCaption,1+FSelectionEnd));
+    end
+  else
+    begin
+    aRenderer.TextOut(R.Left+lLeftSideMargin,R.Top+lTopSideMargin,Font,lColorFP,lCaption);
+    end;
+
+  if FCursorVisible then
+    DrawCursor;
+end;
+
+procedure TEdit.SetSelection(const aStart, aEnd: Integer);
+var
+  lLen : Integer;
+begin
+  if (FSelectionEnd=aEnd) and (FSelectionStart=aStart) then Exit;
+  FSelectionEnd:=aEnd;
+  FSelectionStart:=aStart;
+  if FSelectionEnd<0 then FSelectionEnd:=0;
+  if FSelectionStart<0 then FSelectionStart:=0;
+  lLen:=Length(FValue);
+  if FSelectionEnd>lLen then FSelectionEnd:=lLen;
+  if FSelectionStart>lLen then FSelectionStart:=lLen;
+  EditParamsChanged;
+end;
+
+class function TEdit.HandleFocus: Boolean;
+begin
+  Result:=True;
+end;
+
+function TEdit.CanFocus: Boolean;
+begin
+  Result:=Enabled;
+end;
+
+class constructor TEdit.Init;
+
+begin
+  _FresnelEditTypeID:=CSSRegistry.AddType(CSSTypeName).Index;
+end;
+
+class function TEdit.CSSTypeID: TCSSNumericalID;
+begin
+  Result:=_FresnelEditTypeID;
+end;
+
+class function TEdit.CSSTypeName: TCSSString;
+begin
+  Result:='edit';
+end;
+
+class function TEdit.GetCSSTypeStyle: TCSSString;
+begin
+  // needs checking. Normally you cannot have controls inside an edit.
+  Result:='edit { padding: 3px; ' +
+          ' border: 1px solid black; ' +
+          ' display: inline flow; }';
+end;
+
+constructor TEdit.Create(aOwner: TComponent);
+begin
+  inherited Create(aOwner);
+  FEnabled:=True;
+end;
+
+
+end.
+

+ 8 - 0
src/base/fresnelbase.lpk

@@ -115,6 +115,14 @@
         <AddToUsesPkgSection Value="False"/>
         <UnitName Value="fpCSSResParser"/>
       </Item>
+      <Item>
+        <Filename Value="fresnel.edit.pp"/>
+        <UnitName Value="fresnel.edit"/>
+      </Item>
+      <Item>
+        <Filename Value="fresnel.cursortimer.pp"/>
+        <UnitName Value="fresnel.cursortimer"/>
+      </Item>
     </Files>
     <UsageOptions>
       <UnitPath Value="$(PkgOutDir)"/>

+ 3 - 3
src/base/fresnelbase.pas

@@ -8,9 +8,9 @@ unit FresnelBase;
 interface
 
 uses
-  Fresnel.Controls, Fresnel.DOM, Fresnel.Layouter, Fresnel.Renderer, FCL.Events, Fresnel.Events, 
-  Fresnel.Forms, Fresnel.WidgetSet, Fresnel.Resources, Fresnel.StrConsts, Fresnel.Classes, 
-  Fresnel.Images, UTF8Utils, Fresnel.AsyncCalls, Fresnel.TextLayouter, fresnel.keys;
+  Fresnel.Controls, Fresnel.DOM, Fresnel.Layouter, Fresnel.Renderer, FCL.Events, Fresnel.Events, Fresnel.Forms, Fresnel.WidgetSet, 
+  Fresnel.Resources, Fresnel.StrConsts, Fresnel.Classes, Fresnel.Images, UTF8Utils, Fresnel.AsyncCalls, Fresnel.TextLayouter, 
+  fresnel.keys, fresnel.edit, fresnel.cursortimer;
 
 implementation